* checkpoint * make emitters async generators * fix * custom font spec * replace spinner, use disk cache for fonts * use readline instead * make og images look nice
134 lines
4.4 KiB
TypeScript
134 lines
4.4 KiB
TypeScript
import { QuartzEmitterPlugin } from "../types"
|
|
import { i18n } from "../../i18n"
|
|
import { unescapeHTML } from "../../util/escape"
|
|
import { FullSlug, getFileExtension } from "../../util/path"
|
|
import { ImageOptions, SocialImageOptions, defaultImage, getSatoriFonts } from "../../util/og"
|
|
import sharp from "sharp"
|
|
import satori from "satori"
|
|
import { loadEmoji, getIconCode } from "../../util/emoji"
|
|
import { Readable } from "stream"
|
|
import { write } from "./helpers"
|
|
|
|
const defaultOptions: SocialImageOptions = {
|
|
colorScheme: "lightMode",
|
|
width: 1200,
|
|
height: 630,
|
|
imageStructure: defaultImage,
|
|
excludeRoot: false,
|
|
}
|
|
|
|
/**
|
|
* Generates social image (OG/twitter standard) and saves it as `.webp` inside the public folder
|
|
* @param opts options for generating image
|
|
*/
|
|
async function generateSocialImage(
|
|
{ cfg, description, fonts, title, fileData }: ImageOptions,
|
|
userOpts: SocialImageOptions,
|
|
): Promise<Readable> {
|
|
const { width, height } = userOpts
|
|
const imageComponent = userOpts.imageStructure(cfg, userOpts, title, description, fonts, fileData)
|
|
const svg = await satori(imageComponent, {
|
|
width,
|
|
height,
|
|
fonts,
|
|
loadAdditionalAsset: async (languageCode: string, segment: string) => {
|
|
if (languageCode === "emoji") {
|
|
return `data:image/svg+xml;base64,${btoa(await loadEmoji(getIconCode(segment)))}`
|
|
}
|
|
return languageCode
|
|
},
|
|
})
|
|
|
|
return sharp(Buffer.from(svg)).webp({ quality: 40 })
|
|
}
|
|
|
|
export const CustomOgImagesEmitterName = "CustomOgImages"
|
|
export const CustomOgImages: QuartzEmitterPlugin<Partial<SocialImageOptions>> = (userOpts) => {
|
|
const fullOptions = { ...defaultOptions, ...userOpts }
|
|
|
|
return {
|
|
name: CustomOgImagesEmitterName,
|
|
getQuartzComponents() {
|
|
return []
|
|
},
|
|
async *emit(ctx, content, _resources) {
|
|
const cfg = ctx.cfg.configuration
|
|
const headerFont = cfg.theme.typography.header
|
|
const bodyFont = cfg.theme.typography.body
|
|
const fonts = await getSatoriFonts(headerFont, bodyFont)
|
|
|
|
for (const [_tree, vfile] of content) {
|
|
// if this file defines socialImage, we can skip
|
|
if (vfile.data.frontmatter?.socialImage !== undefined) {
|
|
continue
|
|
}
|
|
|
|
const slug = vfile.data.slug!
|
|
const titleSuffix = cfg.pageTitleSuffix ?? ""
|
|
const title =
|
|
(vfile.data.frontmatter?.title ?? i18n(cfg.locale).propertyDefaults.title) + titleSuffix
|
|
const description =
|
|
vfile.data.frontmatter?.socialDescription ??
|
|
vfile.data.frontmatter?.description ??
|
|
unescapeHTML(
|
|
vfile.data.description?.trim() ?? i18n(cfg.locale).propertyDefaults.description,
|
|
)
|
|
|
|
const stream = await generateSocialImage(
|
|
{
|
|
title,
|
|
description,
|
|
fonts,
|
|
cfg,
|
|
fileData: vfile.data,
|
|
},
|
|
fullOptions,
|
|
)
|
|
|
|
yield write({
|
|
ctx,
|
|
content: stream,
|
|
slug: `${slug}-og-image` as FullSlug,
|
|
ext: ".webp",
|
|
})
|
|
}
|
|
},
|
|
externalResources: (ctx) => {
|
|
if (!ctx.cfg.configuration.baseUrl) {
|
|
return {}
|
|
}
|
|
|
|
const baseUrl = ctx.cfg.configuration.baseUrl
|
|
return {
|
|
additionalHead: [
|
|
(pageData) => {
|
|
const isRealFile = pageData.filePath !== undefined
|
|
const userDefinedOgImagePath = pageData.frontmatter?.socialImage
|
|
const generatedOgImagePath = isRealFile
|
|
? `https://${baseUrl}/${pageData.slug!}-og-image.webp`
|
|
: undefined
|
|
const defaultOgImagePath = `https://${baseUrl}/static/og-image.png`
|
|
const ogImagePath = userDefinedOgImagePath ?? generatedOgImagePath ?? defaultOgImagePath
|
|
|
|
const ogImageMimeType = `image/${getFileExtension(ogImagePath) ?? "png"}`
|
|
return (
|
|
<>
|
|
{!userDefinedOgImagePath && (
|
|
<>
|
|
<meta property="og:image:width" content={fullOptions.width.toString()} />
|
|
<meta property="og:image:height" content={fullOptions.height.toString()} />
|
|
</>
|
|
)}
|
|
|
|
<meta property="og:image" content={ogImagePath} />
|
|
<meta property="og:image:url" content={ogImagePath} />
|
|
<meta name="twitter:image" content={ogImagePath} />
|
|
<meta property="og:image:type" content={ogImageMimeType} />
|
|
</>
|
|
)
|
|
},
|
|
],
|
|
}
|
|
},
|
|
}
|
|
}
|