feat(shared): Add OG images to prebuild
This commit is contained in:
parent
2de8cee4ed
commit
d36643d4a8
8 changed files with 45 additions and 162 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -35,6 +35,7 @@ sites/*/public/feeds/*
|
|||
packages/new-design/shared/public/locales/*/*.json
|
||||
packages/new-design/shared/.gitignore
|
||||
packages/new-design/lib/banner.mjs
|
||||
sites/*/public/img/og/*
|
||||
|
||||
# Lab auto-generated content
|
||||
sites/lab/lib
|
||||
|
|
|
@ -127,7 +127,7 @@ export function ImgController() {}
|
|||
* Generate an Open Graph image
|
||||
* See: https://freesewing.dev/reference/backend/api
|
||||
*/
|
||||
ImgController.prototype.generate = async (req, res, tools) => {
|
||||
ImgController.prototype.generate = async (req, res) => {
|
||||
/*
|
||||
* Extract body parameters
|
||||
*/
|
||||
|
@ -160,7 +160,7 @@ ImgController.prototype.generate = async (req, res, tools) => {
|
|||
*/
|
||||
sharp(Buffer.from(svg, 'utf-8'))
|
||||
.resize({ width: imgConfig.templates.sizes[type] })
|
||||
.toBuffer((err, data, info) => {
|
||||
.toBuffer((err, data) => {
|
||||
if (err) console.log(err)
|
||||
return res.type('png').send(data)
|
||||
})
|
||||
|
|
|
@ -75,7 +75,7 @@ prebuildRunner({
|
|||
* Only prebuild the Open Graph (og) images in production
|
||||
* Will be skipped in development mode to save time
|
||||
*/
|
||||
ogImages: 'productionOnly',
|
||||
ogImages: true, //'productionOnly',
|
||||
|
||||
/*
|
||||
* Only prebuild the patron info in production
|
||||
|
|
0
sites/dev/public/img/og/.gitkeep
Normal file
0
sites/dev/public/img/og/.gitkeep
Normal file
0
sites/org/public/img/og/.gitkeep
Normal file
0
sites/org/public/img/og/.gitkeep
Normal file
39
sites/shared/prebuild/og.mjs
Normal file
39
sites/shared/prebuild/og.mjs
Normal 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
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -14,7 +14,7 @@ import { prebuildCrowdin as crowdin } from './crowdin.mjs'
|
|||
import { prebuildOrg as orgPageTemplates } from './org.mjs'
|
||||
import { prebuildSearch as search } from './search.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?
|
||||
|
@ -37,8 +37,7 @@ const handlers = {
|
|||
git,
|
||||
'Page Templates': true,
|
||||
search,
|
||||
// FIXME: This needs work, but perhaps after v3
|
||||
//ogImages,
|
||||
ogImages,
|
||||
}
|
||||
|
||||
/*
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue