overview:
- purpose: generate Open Graph/social preview images per page
- renderer: satori converts HTML/CSS/JSX to images
- result: consistent, theme-aware preview cards with optional custom layout
Note
plugin_management:
Features
features:
- automatic per-page social preview generation
- light/dark theme support
- frontmatter overrides
- default-image fallback
- custom component control via
imageStructure
Configuration
Info
requirements:
- set
baseUrlin configuration- reason: social images require absolute paths
config:
- standard plugin entry:
plugins:
- source: github:quartz-community/og-image
enabled: true
options:
colorScheme: lightMode # "lightMode" or "darkMode"
width: 1200
height: 630
excludeRoot: falsets_override:
- required_for: custom
imageStructure - placement: before
loadQuartzConfig()
import * as ExternalPlugin from "./.quartz/plugins"
import { defaultImage } from "./quartz/plugins/emitters/ogImage"
// Must be placed before loadQuartzConfig()
ExternalPlugin.CustomOgImages({
colorScheme: "lightMode",
width: 1200,
height: 630,
excludeRoot: false,
imageStructure: defaultImage,
})Configuration Options
| Option | Type | Default | Description |
|---|---|---|---|
colorScheme | string | ”lightMode” | Image theme: "darkMode" or "lightMode" |
width | number | 1200 | Generated image width in pixels |
height | number | 630 | Generated image height in pixels |
excludeRoot | boolean | false | Exclude root index page from auto-generated images |
defaultTitle | string | ”Untitled” | Fallback title when page title is missing |
defaultDescription | string | ”No description provided” | Fallback description when page description is missing |
imageStructure | component | defaultImage | Custom component used for image generation |
Frontmatter Properties
frontmatter:
- use these properties to customize link previews:
| Property | Alias | Summary |
|---|---|---|
socialDescription | description | Preview description |
socialImage | image, cover | Preview image link |
socialImage:
- accepts:
- path relative to
quartz/static - full URL
- path relative to
- examples:
quartz/static/my-images/cover.png->"my-images/cover.png""https://example.com/cover.png"
Info
image_priority:
frontmatter propertygenerated imageif enableddefault imagedefault_image:
- path:
quartz/static/og-image.png- used only when no higher-priority image exists
generated_image:
- when plugin enabled: becomes per-page default
- per-page override: set
socialImage
Customization
imageStructure:
- purpose: fully customize generated image design
- input: JSX + page metadata + config options
- renderer: satori
- prototype: online playground
Fonts
fonts:
- shape:
[header, body] - sources:
theme.typography.headertheme.typography.body
- config_file:
quartz.config.yaml - format:
satorifont objects - CSS access:
.name - body_font_example:
fontFamily: fonts[1].name
header_font_example:
export const myImage: SocialImageOptions["imageStructure"] = (...) => {
return <p style={{ fontFamily: fonts[0].name }}>Cool Header!</p>
}Local fonts
local_fonts:
- place font under
static- define matching
@font-faceincustom.scsscustom.scss @font-face { font-family: "Newsreader"; font-style: normal; font-weight: normal; font-display: swap; src: url("/static/Newsreader.woff2") format("woff2"); }satori_loading:
- add the font in
quartz/util/og.tsxquartz/util/og.tsx import { joinSegments, QUARTZ } from "../path" import fs from "fs" import path from "path" const newsreaderFontPath = joinSegments(QUARTZ, "static", "Newsreader.woff2") export async function getSatoriFonts(headerFont: FontSpecification, bodyFont: FontSpecification) { // ... rest of implementation remains same const fonts: SatoriOptions["fonts"] = [ ...headerFontData.map((data, idx) => ({ name: headerFontName, data, weight: headerWeights[idx], style: "normal" as const, })), ...bodyFontData.map((data, idx) => ({ name: bodyFontName, data, weight: bodyWeights[idx], style: "normal" as const, })), { name: "Newsreader", data: await fs.promises.readFile(path.resolve(newsreaderFontPath)), weight: 400, style: "normal" as const, }, ] return fonts }usage:
- reference
"Newsreader"in customimageStructure
Examples
examples:
- starting points for custom image components
Basic Example
output:
| Light | Dark |
|---|---|
![]() | ![]() |
import { SatoriOptions } from "satori/wasm"
import { GlobalConfiguration } from "../cfg"
import { SocialImageOptions, UserOpts } from "./imageHelper"
import { QuartzPluginData } from "../plugins/vfile"
export const customImage: SocialImageOptions["imageStructure"] = (
cfg: GlobalConfiguration,
userOpts: UserOpts,
title: string,
description: string,
fonts: SatoriOptions["fonts"],
fileData: QuartzPluginData,
) => {
// How many characters are allowed before switching to smaller font
const fontBreakPoint = 22
const useSmallerFont = title.length > fontBreakPoint
const { colorScheme } = userOpts
return (
<div
style={{
display: "flex",
flexDirection: "row",
justifyContent: "flex-start",
alignItems: "center",
height: "100%",
width: "100%",
}}
>
<div
style={{
display: "flex",
alignItems: "center",
justifyContent: "center",
height: "100%",
width: "100%",
backgroundColor: cfg.theme.colors[colorScheme].light,
flexDirection: "column",
gap: "2.5rem",
paddingTop: "2rem",
paddingBottom: "2rem",
}}
>
<p
style={{
color: cfg.theme.colors[colorScheme].dark,
fontSize: useSmallerFont ? 70 : 82,
marginLeft: "4rem",
textAlign: "center",
marginRight: "4rem",
fontFamily: fonts[0].name,
}}
>
{title}
</p>
<p
style={{
color: cfg.theme.colors[colorScheme].dark,
fontSize: 44,
marginLeft: "8rem",
marginRight: "8rem",
lineClamp: 3,
fontFamily: fonts[1].name,
}}
>
{description}
</p>
</div>
<div
style={{
height: "100%",
width: "2vw",
position: "absolute",
backgroundColor: cfg.theme.colors[colorScheme].tertiary,
opacity: 0.85,
}}
/>
</div>
)
}Advanced Example
features:
- custom background
- formatted date
- reading time metadata
export const og: SocialImageOptions["Component"] = (
cfg: GlobalConfiguration,
fileData: QuartzPluginData,
{ colorScheme }: Options,
title: string,
description: string,
fonts: SatoriOptions["fonts"],
) => {
let created: string | undefined
let reading: string | undefined
if (fileData.dates) {
created = formatDate(getDate(cfg, fileData)!, cfg.locale)
}
const { minutes, text: _timeTaken, words: _words } = readingTime(fileData.text!)
reading = i18n(cfg.locale).components.contentMeta.readingTime({
minutes: Math.ceil(minutes),
})
const Li = [created, reading]
return (
<div
style={{
position: "relative",
display: "flex",
flexDirection: "row",
alignItems: "flex-start",
height: "100%",
width: "100%",
backgroundImage: `url("https://${cfg.baseUrl}/static/og-image.jpeg")`,
backgroundSize: "100% 100%",
}}
>
<div
style={{
position: "absolute",
top: 0,
left: 0,
right: 0,
bottom: 0,
background: "radial-gradient(circle at center, transparent, rgba(0, 0, 0, 0.4) 70%)",
}}
/>
<div
style={{
display: "flex",
height: "100%",
width: "100%",
flexDirection: "column",
justifyContent: "flex-start",
alignItems: "flex-start",
gap: "1.5rem",
paddingTop: "4rem",
paddingBottom: "4rem",
marginLeft: "4rem",
}}
>
<img
src={`"https://${cfg.baseUrl}/static/icon.jpeg"`}
style={{
position: "relative",
backgroundClip: "border-box",
borderRadius: "6rem",
}}
width={80}
/>
<div
style={{
display: "flex",
flexDirection: "column",
textAlign: "left",
fontFamily: fonts[0].name,
}}
>
<h2
style={{
color: cfg.theme.colors[colorScheme].light,
fontSize: "3rem",
fontWeight: 700,
marginRight: "4rem",
fontFamily: fonts[0].name,
}}
>
{title}
</h2>
<ul
style={{
color: cfg.theme.colors[colorScheme].gray,
gap: "1rem",
fontSize: "1.5rem",
fontFamily: fonts[1].name,
}}
>
{Li.map((item, index) => {
if (item) {
return <li key={index}>{item}</li>
}
})}
</ul>
</div>
<p
style={{
color: cfg.theme.colors[colorScheme].light,
fontSize: "1.5rem",
overflow: "hidden",
marginRight: "8rem",
textOverflow: "ellipsis",
display: "-webkit-box",
WebkitLineClamp: 7,
WebkitBoxOrient: "vertical",
lineClamp: 7,
fontFamily: fonts[1].name,
}}
>
{description}
</p>
</div>
</div>
)
}API
api:
category: Emitter
function: ExternalPlugin.CustomOgImages()
source: quartz-community/og-image
install: npx quartz plugin add github:quartz-community/og-image

