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 baseUrl in configuration
  • reason: social images require absolute paths

config:

  • standard plugin entry:
quartz.config.yaml
plugins:
  - source: github:quartz-community/og-image
    enabled: true
    options:
      colorScheme: lightMode # "lightMode" or "darkMode"
      width: 1200
      height: 630
      excludeRoot: false

ts_override:

  • required_for: custom imageStructure
  • placement: before loadQuartzConfig()
quartz.ts (override)
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

OptionTypeDefaultDescription
colorSchemestring”lightMode”Image theme: "darkMode" or "lightMode"
widthnumber1200Generated image width in pixels
heightnumber630Generated image height in pixels
excludeRootbooleanfalseExclude root index page from auto-generated images
defaultTitlestring”Untitled”Fallback title when page title is missing
defaultDescriptionstring”No description provided”Fallback description when page description is missing
imageStructurecomponentdefaultImageCustom component used for image generation

Frontmatter Properties

frontmatter:

  • use these properties to customize link previews:
PropertyAliasSummary
socialDescriptiondescriptionPreview description
socialImageimage, coverPreview image link

socialImage:

  • accepts:
    • path relative to quartz/static
    • full URL
  • examples:
    • quartz/static/my-images/cover.png -> "my-images/cover.png"
    • "https://example.com/cover.png"

Info

image_priority:

  • frontmatter property
  • generated image if enabled
  • default image

default_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.header
    • theme.typography.body
  • config_file: quartz.config.yaml
  • format: satori font objects
  • CSS access: .name
  • body_font_example: fontFamily: fonts[1].name

header_font_example:

socialImage.tsx
export const myImage: SocialImageOptions["imageStructure"] = (...) => {
  return <p style={{ fontFamily: fonts[0].name }}>Cool Header!</p>
}

Examples

examples:

  • starting points for custom image components

Basic Example

output:

LightDark
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
custom-og.tsx
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