153 lines
4.1 KiB
JavaScript
153 lines
4.1 KiB
JavaScript
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 }) => {
|
|
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: capitalize(chunks[0]),
|
|
}
|
|
}
|
|
} else {
|
|
console.log('Unsupported data for OG', data)
|
|
}
|
|
}
|
|
|
|
/* 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, info) => {
|
|
if (err) console.log(err)
|
|
return await fs.writeFile(path.join(dir, 'og.png'), data)
|
|
})
|
|
}
|
|
|
|
/* This generates open graph images
|
|
*
|
|
* data holds {
|
|
* lang,
|
|
* site,
|
|
* title,
|
|
* intro,
|
|
* slug
|
|
* }
|
|
*/
|
|
|
|
export const generateOgImage = async (data) => {
|
|
// Inject into SVG
|
|
const meta = await getMetaData(data)
|
|
const svg = decorateSvg(meta)
|
|
await writeAsPng(svg, data.site, data.slug)
|
|
}
|