1
0
Fork 0

feat(shared): Add dynamic OG images

This commit is contained in:
joostdecock 2023-11-03 15:36:09 +01:00
parent d36643d4a8
commit 2472ab1824
22 changed files with 141 additions and 169 deletions

View file

@ -28,16 +28,16 @@
</g>
<g>
<text class="gray-300 tiny italic center">
<tspan x="100" y="179">intro_1</tspan>
<tspan x="100" y="190">intro_2</tspan>
<tspan x="100" y="179">__intro_1__</tspan>
<tspan x="100" y="190">__intro_2__</tspan>
</text>
<text class="gray-200 big bold center">
<tspan x="100" y="95">1title_1</tspan>
<tspan x="100" y="90">2title_1</tspan>
<tspan x="100" dy="24">2title_2</tspan>
<tspan x="100" y="80">3title_1</tspan>
<tspan x="100" dy="24">3title_2</tspan>
<tspan x="100" dy="24">3title_3</tspan>
<tspan x="100" y="95">__1title_1__</tspan>
<tspan x="100" y="90">__2title_1__</tspan>
<tspan x="100" dy="24">__2title_2__</tspan>
<tspan x="100" y="80">__3title_1__</tspan>
<tspan x="100" dy="24">__3title_2__</tspan>
<tspan x="100" dy="24">__3title_3__</tspan>
</text>
<text class="bold" x="6" y="15">
<tspan dx="0" fill="#f87171">F</tspan>

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Before After
Before After

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Before After
Before After

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Before After
Before After

View file

@ -1,5 +1,5 @@
---
title: Motivation
title: My reasoning behind FreeSewing's Revenue Pledge
---
<Note>
@ -9,7 +9,7 @@ his motivations for [the FreeSewing revenue pledge](/docs/about/pledge/)
</Note>
You probably assume that we ask for money to keep the servers running. But that's not exactly true.
You probably assume that I ask for money to keep the servers running. But that's not exactly true.
I don't know if you're familiar with the phrase **noblesse oblige** but it essentially means that privilege entails responsibility.
@ -49,7 +49,7 @@ because FreeSewing is a force for good.
Here's the tricky part: People give less once they know the money goes to charity.
I wish it wasn't the case, but it is.
So we're presenting [our subscription options](/community/join) like you would see on a for-profit site.
So I'm presenting [the subscription options](/patrons/join) like you would see on a for-profit site.
It seems more intuitive this way, and also just works better.
Yes, everything is free, and the money doesn't actually go to paying the server bills

View file

@ -28,7 +28,7 @@ ${banner}
entryPoints: ['src/index.mjs'],
format: 'esm',
outfile: 'dist/index.mjs',
external: ['./local-config.mjs'],
external: ['./local-config.mjs', 'sharp'],
metafile: process.env.VERBOSE ? true : false,
minify: process.env.NO_MINIFY ? false : true,
sourcemap: true,

View file

@ -59,7 +59,11 @@ const introAsLines = (intro, type) => {
// Does it fit on one line?
if (intro.length <= imgConfig.templates.chars[type].intro) return [intro]
// Two lines it is
return splitLine(intro, imgConfig.templates.chars[type].intro)
const lines = splitLine(intro, imgConfig.templates.chars[type].intro)
if (lines[1].length > imgConfig.templates.chars[type].intro)
lines[1] = lines[1].slice(0, imgConfig.templates.chars[type].intro) + '…'
return lines
}
/* Hide unused placeholders */
@ -67,11 +71,11 @@ const hidePlaceholders = (list, type) => {
let svg = templates[type]
for (const i of list) {
svg = svg
.replace(`${i}title_1`, '')
.replace(`${i}title_2`, '')
.replace(`${i}title_3`, '')
.replace(`${i}title_4`, '')
.replace(`${i}title_5`, '')
.replace(`__${i}title_1__`, '')
.replace(`__${i}title_2__`, '')
.replace(`__${i}title_3__`, '')
.replace(`__${i}title_4__`, '')
.replace(`__${i}title_5__`, '')
}
return svg
@ -82,61 +86,70 @@ const decorateSvg = (data) => {
let svg
// Single title line
if (data.title.length === 1) {
svg = hidePlaceholders([2, 3, 4, 5], data.type).replace(`1title_1`, data.title[0])
svg = hidePlaceholders([2, 3, 4, 5], data.type).replace(`__1title_1__`, data.title[0])
}
// Double title line
else if (data.title.length === 2) {
svg = hidePlaceholders([1, 3, 4, 5], data.type)
.replace(`2title_1`, data.title[0])
.replace(`2title_2`, data.title[1])
.replace(`__2title_1__`, data.title[0])
.replace(`__2title_2__`, data.title[1])
}
// Triple title line
else if (data.title.length === 3) {
svg = hidePlaceholders([1, 2, 4, 5], data.type)
.replace(`3title_1`, data.title[0])
.replace(`3title_2`, data.title[1])
.replace(`3title_3`, data.title[2])
.replace(`__3title_1__`, data.title[0])
.replace(`__3title_2__`, data.title[1])
.replace(`__3title_3__`, data.title[2])
}
// Quadruple title line
else if (data.title.length === 4) {
svg = hidePlaceholders([1, 2, 3, 5], data.type)
.replace(`4title_1`, data.title[0])
.replace(`4title_2`, data.title[1])
.replace(`4title_3`, data.title[2])
.replace(`4title_4`, data.title[3])
.replace(`__4title_1__`, data.title[0])
.replace(`__4title_2__`, data.title[1])
.replace(`__4title_3__`, data.title[2])
.replace(`__4title_4__`, data.title[3])
}
// Quintuple title line
else if (data.title.length === 5) {
svg = hidePlaceholders([1, 2, 3, 4], data.type)
.replace(`5title_1`, data.title[0])
.replace(`5title_2`, data.title[1])
.replace(`5title_3`, data.title[2])
.replace(`5title_4`, data.title[3])
.replace(`5title_5`, data.title[4])
.replace(`__5title_1__`, data.title[0])
.replace(`__5title_2__`, data.title[1])
.replace(`__5title_3__`, data.title[2])
.replace(`__5title_4__`, data.title[3])
.replace(`__5title_5__`, data.title[4])
}
return svg
.replace(`intro_1`, data.intro[0] || '')
.replace(`intro_2`, data.intro[1] || '')
.replace('site', data.site || '')
.replace(`__intro_1__`, data.intro[0] || '')
.replace(`__intro_2__`, data.intro[1] || '')
.replace('__site__', data.site || '')
}
export function ImgController() {}
const parseInput = (req) => {
let input = {}
if (req.params.data) {
try {
input = JSON.parse(decodeURIComponent(req.params.data))
} catch (err) {
console.log(err)
}
} else input = req.body
return input
}
/*
* Generate an Open Graph image
* See: https://freesewing.dev/reference/backend/api
*/
ImgController.prototype.generate = async (req, res) => {
const input = parseInput(req)
/*
* Extract body parameters
*/
const {
site = false,
title = 'Please provide a title',
intro = 'Please provide an intro',
type = 'wide',
} = req.body
const { site = false, title = '', intro = '', type = 'wide' } = input
if (site && imgConfig.sites.indexOf(site) === -1)
return res.status(400).send({ error: 'invalidSite' })

View file

@ -7,4 +7,7 @@ export function imgRoutes(tools) {
// Generate an image
app.post('/img', (req, res) => Img.generate(req, res, tools))
// Generate an image
app.get('/img/:data', (req, res) => Img.generate(req, res, tools))
}

View file

@ -1,7 +1,6 @@
// Dependencies
import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
// Components
import Head from 'next/head'
import { PageWrapper, ns as pageNs } from 'shared/components/wrappers/page.mjs'
import { Robot } from 'shared/components/robot/index.mjs'
import { Popout } from 'shared/components/popout/index.mjs'
@ -12,23 +11,7 @@ import { NavLinks, MainSections } from 'shared/components/navigation/sitenav.mjs
const namespaces = [...pageNs]
const Page404 = () => (
<PageWrapper title="404: Page not found">
<Head>
<meta property="og:type" content="article" key="type" />
<meta
property="og:description"
content="There's nothing here. If you followed a link to get here, that link is broken"
key="description"
/>
<meta property="og:article:author" content="Joost De Cock" key="author" />
<meta property="og:image" content={`https://freesewing.dev/og/404/og.png`} key="image" />
<meta property="og:image:type" content="image/png" />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />
<meta property="og:url" content={`https://freesewing.dev/`} key="url" />
<meta property="og:locale" content="en_US" key="locale" />
<meta property="og:site_name" content="freesewing.dev" key="site" />
</Head>
<PageWrapper title="404: Page not found" intro="We could not find what you are looking for">
<BaseLayout>
<BaseLayoutLeft>
<MainSections />

View file

@ -3,8 +3,8 @@ import { pages } from 'site/prebuild/docs.en.mjs'
// Dependencies
import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
import { loadMdxAsStaticProps } from 'shared/mdx/load.mjs'
import { freeSewingConfig as config } from 'shared/config/freesewing.config.mjs'
// Components
import Head from 'next/head'
import { PageWrapper, ns } from 'shared/components/wrappers/page.mjs'
//import { components } from 'shared/components/mdx/index.mjs'
import { MdxWrapper } from 'shared/components/wrappers/mdx.mjs'
@ -27,24 +27,6 @@ import {
*/
const DocsPage = ({ page, slug, frontmatter, mdx, mdxSlug }) => (
<PageWrapper {...page} title={frontmatter.title} intro={frontmatter.intro}>
<Head>
<meta property="og:title" content={frontmatter.title} key="title" />
<meta property="og:type" content="article" key="type" />
<meta property="og:description" content={frontmatter.intro} key="type" />
<meta property="og:article:author" content="Joost De Cock" key="author" />
<meta
property="og:image"
content={`https://canary.backend.freesewing.org/og-img/en/org/${slug}}`}
key="image"
/>
<meta property="og:image:type" content="image/png" />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />
<meta property="og:url" content={`https://freesewing.dev/${slug}`} key="url" />
<meta property="og:locale" content="en" key="locale" />
<meta property="og:site_name" content="freesewing.dev" key="site" />
<title>{frontmatter.title}</title>
</Head>
<BaseLayout>
<BaseLayoutLeft>
<MainSections />

View file

@ -1,7 +1,6 @@
// Dependencies
import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
// Components
import Head from 'next/head'
import { PageWrapper } from 'shared/components/wrappers/page.mjs'
import { PageLink } from 'shared/components/link.mjs'
import { Highlight } from 'shared/components/mdx/highlight.mjs'
@ -28,25 +27,11 @@ const Card = ({ bg = 'bg-base-200', textColor = 'text-base-content', title, chil
* or set them manually.
*/
const HomePage = ({ page }) => (
<PageWrapper {...page}>
<Head>
<meta property="og:type" content="article" key="type" />
<meta
property="og:description"
content="Documentation and tutorials for FreeSewing developers and contributors"
key="description"
/>
<meta property="og:article:author" content="Joost De Cock" key="author" />
<meta property="og:image" content="https://freesewing.dev/og/og.png" key="image" />
<meta property="og:image:type" content="image/png" />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />
<meta property="og:url" content="https://freesewing.dev/" key="url" />
<meta property="og:locale" content="en_US" key="locale" />
<meta property="og:site_name" content="freesewing.dev" key="site" />
<title>{title}</title>
</Head>
<PageWrapper
{...page}
title="FreeSewing.dev"
intro="Documentation and tutorials for FreeSewing developers and contributors"
>
<div className="max-w-7xl m-auto px-0 mt-24 px-4">
<FreeSewingIcon className="h-36 w-36 m-auto" />
<h1 className="text-center font-heavy drop-shadow-md px-4">

View file

@ -25,7 +25,7 @@ const SearchPage = ({ page }) => {
)
return (
<PageWrapper {...page}>
<PageWrapper {...page} title="Search" intro="Use the FreeSewing.dev site search">
<BaseLayout>
<BaseLayoutLeft>
<MainSections />

View file

@ -75,7 +75,7 @@ prebuildRunner({
* Only prebuild the Open Graph (og) images in production
* Will be skipped in development mode to save time
*/
ogImages: true, //'productionOnly',
ogImages: false, // We currently don't prebuild these images
/*
* Only prebuild the patron info in production

View file

@ -3,7 +3,6 @@ import { nsMerge } from 'shared/utils.mjs'
// Hooks
import { useContext } from 'react'
// Components
import Head from 'next/head'
import {
BaseLayout,
BaseLayoutLeft,
@ -22,37 +21,11 @@ import { PrevNext } from 'shared/components/prev-next.mjs'
export const ns = nsMerge(navNs, 'docs', metaNs)
export const FrontmatterHead = ({ frontmatter, slug, locale }) => (
<Head>
<meta property="og:title" content={frontmatter.title} key="title" />
<meta property="og:type" content="article" key="type" />
<meta property="og:description" content={frontmatter.intro || frontmatter.title} key="type" />
<meta
property="og:article:author"
content={frontmatter.author || frontmatter.maker || 'Joost De Cock'}
key="author"
/>
<meta
property="og:image"
content={`https://canary.backend.freesewing.org/og-img/en/org/${slug}}`}
key="image"
/>
<meta property="og:image:type" content="image/png" />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />
<meta property="og:url" content={`https://freesewing.org/${slug}`} key="url" />
<meta property="og:locale" content={locale || 'en'} key="locale" />
<meta property="og:site_name" content="freesewing.org" key="site" />
<title>{frontmatter.title + '- FreeSewing.org'}</title>
</Head>
)
export const DocsLayout = ({ children = [], frontmatter }) => {
const { slug, locale } = useContext(NavigationContext)
return (
<>
<FrontmatterHead {...{ frontmatter, slug, locale }} />
<BaseLayout>
<BaseLayoutLeft>
<MainSections />

View file

@ -5,7 +5,6 @@ import { Lightbox } from 'shared/components/lightbox.mjs'
import { ImageWrapper } from 'shared/components/wrappers/img.mjs'
import { TimeAgo, ns as timeagoNs } from 'shared/components/timeago/index.mjs'
import { useTranslation } from 'next-i18next'
import { FrontmatterHead } from './docs.mjs'
import {
BaseLayout,
BaseLayoutLeft,
@ -81,7 +80,6 @@ export const PostLayout = ({ mdx, slug, frontmatter, locale, type, dir }) => {
return (
<BaseLayout>
<FrontmatterHead {...{ frontmatter, slug, locale }} />
<BaseLayoutLeft>
<MainSections />
<NavLinks />

View file

@ -11,6 +11,7 @@ const BlogPage = ({ dir, page, mdx, frontmatter }) => {
return (
<PageWrapper
{...page}
intro={frontmatter.intro}
title={frontmatter.title}
layout={(props) => (
<PostLayout

View file

@ -58,12 +58,13 @@ const HomePage = ({ page }) => {
}, [account.username])
return (
<PageWrapper {...page} layout={BareLayout}>
<PageWrapper
{...page}
layout={BareLayout}
title="FreeSewing.org"
intro={t('homepage:freePatterns')}
>
<ForceAccountCheck />
<Head>
<title>FreeSewing.org</title>
</Head>
<div className="text-center w-full m-auto">
<FreeSewingIcon className="w-36 h-36 mt-0 lg:mt-8 lg:w-56 lg:h-=56 mt-4 m-auto pr-6" />
<h1 className="font-bold -mt-8 lg:-mt-4" style={{ letterSpacing: '-0.1rem' }}>

View file

@ -12,6 +12,7 @@ const ShowcasePage = ({ dir, page, mdx, frontmatter }) => {
<PageWrapper
{...page}
title={frontmatter.title}
intro={frontmatter.intro}
layout={(props) => (
<PostLayout
{...props}

View file

@ -36,7 +36,7 @@ const ShowcaseIndexPage = ({ page }) => {
: Object.keys(posts[page.locale])
return (
<PageWrapper {...page} layout={BareLayout}>
<PageWrapper {...page} title={t('sections:showcase')} layout={BareLayout}>
<div className="p-4 m-auto">
<h1 className="text-center">FreeSewing - {t('sections:showcase')}</h1>
<div className="max-w-7xl m-auto">

View file

@ -85,7 +85,7 @@ prebuildRunner({
* Only prebuild the Open Graph (og) images in production
* Will be skipped in development mode to save time
*/
ogImages: 'productionOnly',
ogImages: false, // We do not currently prebuild these
/*
* Only prebuild the patron info in production

View file

@ -1,7 +1,8 @@
// __SDEFILE__ - This file is a dependency for the stand-alone environment
// Dependencies
import React, { useState, useEffect, useContext } from 'react'
import { nsMerge } from 'shared/utils.mjs'
import { ogUrl, nsMerge } from 'shared/utils.mjs'
import { siteConfig } from 'site/site.config.mjs'
// Context
import { LoadingStatusContext } from 'shared/context/loading-status-context.mjs'
// Hooks
@ -21,7 +22,14 @@ export const PageWrapper = (props) => {
/*
* Deconstruct props
*/
const { layout = DefaultLayout, footer = true, header = true, children = [], path = [] } = props
const {
layout = DefaultLayout,
footer = true,
header = true,
locale = 'en',
children = [],
path = [],
} = props
// Title is typically set in props.t but check props.title too
const pageTitle = props.t ? props.t : props.title ? props.title : null
@ -60,6 +68,25 @@ export const PageWrapper = (props) => {
{props.intro && (
<meta property="og:description" content={props.intro} key="description" />
)}
<meta property="og:title" content={pageTitle} key="title" />
<meta property="og:type" content="article" key="type" />
<meta property="og:article:author" content="Joost De Cock" key="author" />
<meta
property="og:image"
content={`${ogUrl({ site: siteConfig.tld, title: pageTitle, intro: props.intro })}`}
key="image"
/>
<meta property="og:image:type" content="image/png" />
<meta property="og:image:width" content="2400" />
<meta property="og:image:height" content="1260" />
<meta
property="og:url"
content={`https://FreeSewing.${siteConfig.tld}/${path.join('/')}`}
key="url"
/>
<meta property="og:locale" content={locale} key="locale" />
<meta property="og:site_name" content={`FreeSewing.${siteConfig.tld}`} key="site" />
<title>{pageTitle}</title>
</Head>
)}
<div

View file

@ -1,11 +1,10 @@
import axios from 'axios'
import { freeSewingConfig as config } from '../config/freesewing.config.mjs'
import { slugToOgImg } from '../utils.mjs'
import get from 'lodash.get'
import fs from 'fs'
import path from 'path'
const slugToImg = (slug, language) => `${language}_${slug.split('/').join('_')}.png`
export const generateImage = async ({ title, intro, site, slug, language }) => {
let result
try {
@ -14,7 +13,7 @@ export const generateImage = async ({ title, intro, site, slug, language }) => {
{ title, intro, site, type: 'wide' },
{ responseType: 'arraybuffer' }
)
const file = path.resolve('..', site, 'public', 'img', 'og', slugToImg(slug, language))
const file = path.resolve('..', site, 'public', 'img', 'og', slugToOgImg(slug, language))
await fs.promises.writeFile(file, result.data)
} catch (err) {
console.log(err)

View file

@ -7,6 +7,7 @@ import orderBy from 'lodash.orderby'
import unset from 'lodash.unset'
import { cloudflareConfig } from './config/cloudflare.mjs'
import { mergeOptions } from '@freesewing/core'
import { freeSewingConfig as config } from './config/freesewing.config.mjs'
const slugifyConfig = {
replacement: '-', // replace spaces with replacement character, defaults to `-`
@ -491,3 +492,8 @@ export const workbenchHash = ({ settings = {}, view = 'draft' }) =>
export const getSearchParam = (name = 'id') =>
typeof window === 'undefined' ? undefined : new URLSearchParams(window.location.search).get(name)
export const slugToOgImg = (slug, language) => `${language}_${slug.split('/').join('_')}.png`
export const ogUrl = ({ site = false, title, intro }) =>
`${config.backend}/img/${encodeURIComponent(JSON.stringify({ site, title, intro }))}`