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/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
|
||||||
|
|
|
@ -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)
|
||||||
})
|
})
|
||||||
|
|
|
@ -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
|
||||||
|
|
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 { 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,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue