1
0
Fork 0

feat(shared): Add OG images to prebuild

This commit is contained in:
joostdecock 2023-11-03 13:34:47 +01:00
parent 2de8cee4ed
commit d36643d4a8
8 changed files with 45 additions and 162 deletions

1
.gitignore vendored
View file

@ -35,6 +35,7 @@ sites/*/public/feeds/*
packages/new-design/shared/public/locales/*/*.json packages/new-design/shared/public/locales/*/*.json
packages/new-design/shared/.gitignore packages/new-design/shared/.gitignore
packages/new-design/lib/banner.mjs packages/new-design/lib/banner.mjs
sites/*/public/img/og/*
# Lab auto-generated content # Lab auto-generated content
sites/lab/lib sites/lab/lib

View file

@ -127,7 +127,7 @@ export function ImgController() {}
* Generate an Open Graph image * Generate an Open Graph image
* See: https://freesewing.dev/reference/backend/api * See: https://freesewing.dev/reference/backend/api
*/ */
ImgController.prototype.generate = async (req, res, tools) => { ImgController.prototype.generate = async (req, res) => {
/* /*
* Extract body parameters * Extract body parameters
*/ */
@ -160,7 +160,7 @@ ImgController.prototype.generate = async (req, res, tools) => {
*/ */
sharp(Buffer.from(svg, 'utf-8')) sharp(Buffer.from(svg, 'utf-8'))
.resize({ width: imgConfig.templates.sizes[type] }) .resize({ width: imgConfig.templates.sizes[type] })
.toBuffer((err, data, info) => { .toBuffer((err, data) => {
if (err) console.log(err) if (err) console.log(err)
return res.type('png').send(data) return res.type('png').send(data)
}) })

View file

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

View file

View file

View file

@ -0,0 +1,39 @@
import axios from 'axios'
import { freeSewingConfig as config } from '../config/freesewing.config.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 {
result = await axios.post(
`${config.backend}/img`,
{ title, intro, site, type: 'wide' },
{ responseType: 'arraybuffer' }
)
const file = path.resolve('..', site, 'public', 'img', 'og', slugToImg(slug, language))
await fs.promises.writeFile(file, result.data)
} catch (err) {
console.log(err)
}
}
export const prebuildOgImages = async (store, mock) => {
if (mock) return
for (const [language, nav] of Object.entries(store.navigation.sitenav)) {
for (const slug of store.navigation.sluglut) {
await generateImage({
title: get(nav, slug.split('/'))?.t || 'FIXME: No title',
intro: '',
site: store.site,
slug,
language,
})
}
}
return
}

View file

@ -1,156 +0,0 @@
import fs_ from 'fs'
import path from 'path'
import { Buffer } from 'buffer'
import sharp from 'sharp'
import { capitalize } from '../../utils.mjs'
const fs = fs_.promises
const config = {
template: ['..', '..', 'artwork', 'og', 'template.svg'],
chars: {
title_1: 18,
title_2: 19,
title_3: 20,
intro: 34,
sub: 42,
},
}
// Load template once at startup
const template = fs_.readFileSync(path.resolve(...config.template), 'utf-8')
/* Find longest possible place to split a string */
const splitLine = (line, chars) => {
const words = line.split(' ')
if (words[0].length > chars) {
// Force a word break
return [line.slice(0, chars - 1) + '-', line.slice(chars - 1)]
}
// Glue chunks together until it's too long
let firstLine = ''
let max = false
for (const word of words) {
if (!max && `${firstLine}${word}`.length <= chars) firstLine += `${word} `
else max = true
}
return [firstLine, words.join(' ').slice(firstLine.length)]
}
/* Divide title into lines to fit on image */
const titleAsLines = (title) => {
// Does it fit on one line?
if (title.length <= config.chars.title_1) return [title]
// Does it fit on two lines?
let lines = splitLine(title, config.chars.title_1)
if (lines[1].length <= config.chars.title_2) return lines
// Three lines it is
return [lines[0], ...splitLine(lines[1], config.chars.title_2)]
}
/* Divive intro into lines to fit on image */
const introAsLines = (intro) => {
// Does it fit on one line?
if (intro.length <= config.chars.intro) return [intro]
// Two lines it is
return splitLine(intro, config.chars.intro)
}
// Get title and intro
const getMetaData = async ({ slug, title, intro, site, lead = false }) => {
const chunks = slug.split('/')
if (site === 'dev') {
// Home page
if (chunks.length === 1 && chunks[0] === '') {
return {
title: ['FreeSewing.dev'],
intro: introAsLines('FreeSewing documentation for developers and contributors'),
sub: ['With guides, tutorials,', "How-to's and more"],
lead: '.dev',
}
} else {
// MDX page
return {
title: titleAsLines(title),
intro: introAsLines(intro),
sub: ['https://freesewing.dev/', slug],
lead: lead || capitalize(chunks[0]),
}
}
}
}
/* Hide unused placeholders */
const hidePlaceholders = (list) => {
let svg = template
for (const i of list) {
svg = svg.replace(`${i}title_1`, '').replace(`${i}title_2`, '').replace(`${i}title_3`, '')
}
return svg
}
/* Place text in SVG template */
const decorateSvg = (data) => {
let svg
// Single title line
if (data.title.length === 1) {
svg = hidePlaceholders([2, 3]).replace(`1title_1`, data.title[0])
}
// Double title line
else if (data.title.length === 2) {
svg = hidePlaceholders([1, 3])
.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])
.replace(`3title_1`, data.title[0])
.replace(`3title_2`, data.title[1])
.replace(`3title_3`, data.title[2])
}
return svg
.replace('sub_1', data.sub[0] || '')
.replace('sub_2', data.sub[1] || '')
.replace(`intro_1`, data.intro[0] || '')
.replace(`intro_2`, data.intro[1] || '')
.replace('lead_1', data.lead || '')
}
const writeAsPng = async (svg, site, slug) => {
const dir = path.resolve(path.join('..', '..', 'sites', site, 'public', 'og', ...slug.split('/')))
await fs.mkdir(dir, { recursive: true })
// Turn into PNG
sharp(Buffer.from(svg, 'utf-8'))
.resize({ width: 1200 })
.toBuffer(async (err, data) => {
if (err) console.log(err)
if (data) return await fs.writeFile(path.join(dir, 'og.png'), data)
else console.log('No data for', slug)
})
}
/* This generates open graph images
*
* data holds {
* lang,
* site,
* title,
* intro,
* slug
* }
*/
export const prebuildOgImages = async (data) => {
// Inject into SVG
const meta = await getMetaData(data)
const svg = decorateSvg(meta)
try {
await writeAsPng(svg, data.site, data.slug)
} catch (err) {
console.log('Could not write PNG for', data, meta)
}
}

View file

@ -14,7 +14,7 @@ import { prebuildCrowdin as crowdin } from './crowdin.mjs'
import { prebuildOrg as orgPageTemplates } from './org.mjs' import { prebuildOrg as orgPageTemplates } from './org.mjs'
import { prebuildSearch as search } from './search.mjs' import { prebuildSearch as search } from './search.mjs'
//import { prebuildLab as lab} from './lab.mjs' //import { prebuildLab as lab} from './lab.mjs'
//import { prebuildOgImages as ogImages } from './og/index.mjs' import { prebuildOgImages as ogImages } from './og.mjs'
/* /*
* Are we running in production? * Are we running in production?
@ -37,8 +37,7 @@ const handlers = {
git, git,
'Page Templates': true, 'Page Templates': true,
search, search,
// FIXME: This needs work, but perhaps after v3 ogImages,
//ogImages,
} }
/* /*