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 { 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> = (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 && ( <> )} ) }, ], } }, } }