diff --git a/quartz/cfg.ts b/quartz/cfg.ts index 8371b5e2b..b77e3ccd1 100644 --- a/quartz/cfg.ts +++ b/quartz/cfg.ts @@ -1,4 +1,5 @@ import { ValidDateType } from "./components/Date" +import { SocialImageOptions } from "./components/Head" import { QuartzComponent } from "./components/types" import { PluginTypes } from "./plugins/types" import { Theme } from "./util/theme" @@ -33,6 +34,10 @@ export interface GlobalConfiguration { * Quartz will avoid using this as much as possible and use relative URLs most of the time */ baseUrl?: string + /** + * Wether to generate and use social images (Open Graph and Twitter standard) for link previews + */ + generateSocialImages: boolean | SocialImageOptions theme: Theme } diff --git a/quartz/components/Head.tsx b/quartz/components/Head.tsx index d4a4944bd..48bb0c84c 100644 --- a/quartz/components/Head.tsx +++ b/quartz/components/Head.tsx @@ -7,6 +7,13 @@ import { getSatoriFont } from "../util/fonts" import { GlobalConfiguration } from "../cfg" import sharp from "sharp" +export type SocialImageOptions = { + /** + * What color scheme to use for image generation (uses colors from config theme) + */ + colorScheme?: "lightMode" | "darkMode" +} + /** * Generates social image (OG/twitter standard) and saves it as `.web` inside the public folder * @param title what title to use @@ -22,13 +29,18 @@ async function generateSocialImage( fileName: string, fontsPromise: Promise, cfg: GlobalConfiguration, - colorScheme: "lightMode" | "darkMode", ) { const fonts = await fontsPromise // How many characters are allowed before switching to smaller font const fontBreakPoint = 22 const useSmallerFont = title.length > fontBreakPoint + + // Get color scheme preference from config (use lightMode by default) + let colorScheme: SocialImageOptions["colorScheme"] = "lightMode" + if (typeof cfg.generateSocialImages !== "boolean" && cfg.generateSocialImages.colorScheme) { + colorScheme = cfg.generateSocialImages.colorScheme + } const svg = await satori(
{ const title = fileData.frontmatter?.title ?? "Untitled" const description = fileData.description?.trim() ?? "No description provided" - generateSocialImage(title, description, filePath as string, fontsPromise, cfg, "lightMode") + if (cfg.generateSocialImages) { + // Generate folders for social images (if they dont exist yet) + if (!fs.existsSync(imageDir)) { + fs.mkdirSync(imageDir, { recursive: true }) + } - if (!fs.existsSync(imageDir)) { - fs.mkdirSync(imageDir, { recursive: true }) + // Generate social image (happens async) + generateSocialImage(title, description, filePath as string, fontsPromise, cfg) } + const { css, js } = externalResources const url = new URL(`https://${cfg.baseUrl ?? "example.com"}`) @@ -131,13 +148,16 @@ export default (() => { const baseDir = fileData.slug === "404" ? path : pathToRoot(fileData.slug!) const iconPath = joinSegments(baseDir, "static/icon.png") - const useDefaultOgImage = filePath === undefined + const ogImageDefaultPath = `https://${cfg.baseUrl}/static/og-image.png` const ogImageGeneratedPath = `https://${cfg.baseUrl}/${imageDir.replace( "public/", "", )}/${filePath}.${extension}` + // Use default og image if filePath doesnt exist (for autogenerated paths with no .md file) + const useDefaultOgImage = filePath === undefined || !cfg.generateSocialImages + const ogImagePath = useDefaultOgImage ? ogImageDefaultPath : ogImageGeneratedPath return ( @@ -145,25 +165,30 @@ export default (() => { {title} + {/* OG/Twitter meta tags */} - + + + - - - - - - {cfg.baseUrl && } - {cfg.baseUrl && } - - + + + {cfg.baseUrl && ( + <> + + + + + + + )}