Jacky Zhao
2025-03-16 a7372079817fb1a1e69b2632405d759f9c5e913d
quartz/plugins/emitters/ogImage.tsx
@@ -4,10 +4,12 @@
import { FullSlug, getFileExtension } from "../../util/path"
import { ImageOptions, SocialImageOptions, defaultImage, getSatoriFonts } from "../../util/og"
import sharp from "sharp"
import satori from "satori"
import satori, { SatoriOptions } from "satori"
import { loadEmoji, getIconCode } from "../../util/emoji"
import { Readable } from "stream"
import { write } from "./helpers"
import { BuildCtx } from "../../util/ctx"
import { QuartzPluginData } from "../vfile"
const defaultOptions: SocialImageOptions = {
  colorScheme: "lightMode",
@@ -42,6 +44,41 @@
  return sharp(Buffer.from(svg)).webp({ quality: 40 })
}
async function processOgImage(
  ctx: BuildCtx,
  fileData: QuartzPluginData,
  fonts: SatoriOptions["fonts"],
  fullOptions: SocialImageOptions,
) {
  const cfg = ctx.cfg.configuration
  const slug = fileData.slug!
  const titleSuffix = cfg.pageTitleSuffix ?? ""
  const title =
    (fileData.frontmatter?.title ?? i18n(cfg.locale).propertyDefaults.title) + titleSuffix
  const description =
    fileData.frontmatter?.socialDescription ??
    fileData.frontmatter?.description ??
    unescapeHTML(fileData.description?.trim() ?? i18n(cfg.locale).propertyDefaults.description)
  const stream = await generateSocialImage(
    {
      title,
      description,
      fonts,
      cfg,
      fileData,
    },
    fullOptions,
  )
  return write({
    ctx,
    content: stream,
    slug: `${slug}-og-image` as FullSlug,
    ext: ".webp",
  })
}
export const CustomOgImagesEmitterName = "CustomOgImages"
export const CustomOgImages: QuartzEmitterPlugin<Partial<SocialImageOptions>> = (userOpts) => {
  const fullOptions = { ...defaultOptions, ...userOpts }
@@ -58,39 +95,23 @@
      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
        if (vfile.data.frontmatter?.socialImage !== undefined) continue
        yield processOgImage(ctx, vfile.data, fonts, fullOptions)
        }
        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,
        )
    async *partialEmit(ctx, _content, _resources, changeEvents) {
      const cfg = ctx.cfg.configuration
      const headerFont = cfg.theme.typography.header
      const bodyFont = cfg.theme.typography.body
      const fonts = await getSatoriFonts(headerFont, bodyFont)
        yield write({
          ctx,
          content: stream,
          slug: `${slug}-og-image` as FullSlug,
          ext: ".webp",
        })
      // find all slugs that changed or were added
      for (const changeEvent of changeEvents) {
        if (!changeEvent.file) continue
        if (changeEvent.file.data.frontmatter?.socialImage !== undefined) continue
        if (changeEvent.type === "add" || changeEvent.type === "change") {
          yield processOgImage(ctx, changeEvent.file.data, fonts, fullOptions)
        }
      }
    },
    externalResources: (ctx) => {