From c538c151c7462ad0395ff2c15c5e11e89e362aa8 Mon Sep 17 00:00:00 2001
From: Striven <sg.striven@cutecat.club>
Date: Sat, 04 Apr 2026 19:47:16 +0000
Subject: [PATCH] Initial commit
---
quartz/plugins/emitters/ogImage.tsx | 124 ++++++++++++++++++++++++++++------------
1 files changed, 86 insertions(+), 38 deletions(-)
diff --git a/quartz/plugins/emitters/ogImage.tsx b/quartz/plugins/emitters/ogImage.tsx
index 056976a..813d934 100644
--- a/quartz/plugins/emitters/ogImage.tsx
+++ b/quartz/plugins/emitters/ogImage.tsx
@@ -1,13 +1,17 @@
import { QuartzEmitterPlugin } from "../types"
import { i18n } from "../../i18n"
import { unescapeHTML } from "../../util/escape"
-import { FullSlug, getFileExtension } from "../../util/path"
+import { FullSlug, getFileExtension, isAbsoluteURL, joinSegments, QUARTZ } 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"
+import fs from "node:fs/promises"
+import { styleText } from "util"
const defaultOptions: SocialImageOptions = {
colorScheme: "lightMode",
@@ -26,15 +30,34 @@
userOpts: SocialImageOptions,
): Promise<Readable> {
const { width, height } = userOpts
- const imageComponent = userOpts.imageStructure(cfg, userOpts, title, description, fonts, fileData)
+ const iconPath = joinSegments(QUARTZ, "static", "icon.png")
+ let iconBase64: string | undefined = undefined
+ try {
+ const iconData = await fs.readFile(iconPath)
+ iconBase64 = `data:image/png;base64,${iconData.toString("base64")}`
+ } catch (err) {
+ console.warn(styleText("yellow", `Warning: Could not find icon at ${iconPath}`))
+ }
+
+ const imageComponent = userOpts.imageStructure({
+ cfg,
+ userOpts,
+ title,
+ description,
+ fonts,
+ fileData,
+ iconBase64,
+ })
+
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 await loadEmoji(getIconCode(segment))
}
+
return languageCode
},
})
@@ -42,6 +65,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 +116,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)
+ }
+ },
+ 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)
+
+ // 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)
}
-
- 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) => {
@@ -103,13 +145,19 @@
additionalHead: [
(pageData) => {
const isRealFile = pageData.filePath !== undefined
- const userDefinedOgImagePath = pageData.frontmatter?.socialImage
+ let userDefinedOgImagePath = pageData.frontmatter?.socialImage
+
+ if (userDefinedOgImagePath) {
+ userDefinedOgImagePath = isAbsoluteURL(userDefinedOgImagePath)
+ ? userDefinedOgImagePath
+ : `https://${baseUrl}/static/${userDefinedOgImagePath}`
+ }
+
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 (
<>
--
Gitblit v1.10.0