Custom OG Images(自定义 OG 图片)发射器插件会为你的页面自动生成社交媒体预览图片。它使用 satori 将 HTML/CSS 转换为图片,让你为内容创建美观且一致的社交媒体预览卡片。

Note

关于如何添加、移除或配置插件,请参阅 配置 页面。

功能特性

  • 自动为每个页面生成社交媒体预览图片
  • 支持亮色和暗色主题
  • 可通过 frontmatter 属性自定义
  • 需要时可回退到默认图片
  • 通过自定义组件完全控制图片设计

配置

信息

你的 configuration 中必须正确设置 baseUrl 属性,社交图片需要绝对路径才能正常工作。

该插件支持以下配置选项:

quartz.config.ts
import { CustomOgImages } from "./quartz/plugins/emitters/ogImage"
 
const config: QuartzConfig = {
  plugins: {
    emitters: [
      CustomOgImages({
        colorScheme: "lightMode", // 生成图片时使用的配色方案,与主题色一致,可选 "darkMode" 或 "lightMode"
        width: 1200, // 生成图片的宽度(像素)
        height: 630, // 生成图片的高度(像素)
        excludeRoot: false, // 是否排除 "/" 首页不自动生成图片(false=自动,true=使用默认 og 图片)
        imageStructure: defaultImage, // 使用的自定义图片组件
      }),
    ],
  },
}

配置选项

选项类型默认值描述
colorSchemestring”lightMode”生成图片时使用的主题(“darkMode” 或 “lightMode”)
widthnumber1200生成图片的宽度(像素)
heightnumber630生成图片的高度(像素)
excludeRootbooleanfalse是否排除首页不自动生成图片
imageStructurecomponentdefaultImage用于生成图片的自定义组件

Frontmatter 属性

以下属性可用于自定义你的链接预览:

属性别名说明
socialDescriptiondescription用于预览的描述。
socialImageimage, cover预览图片的链接。

socialImage 属性应包含一张图片的链接,可以是相对于 quartz/static 的路径,也可以是完整的 URL。如果你将所有图片放在 quartz/static/my-images 文件夹下,socialImage 示例为 "my-images/cover.png"。也可以直接使用完整 URL,如 "https://example.com/cover.png"

信息

封面图片的优先级如下:frontmatter 属性 > 自动生成图片(如启用)> 默认图片

默认图片(quartz/static/og-image.png)仅在没有其他设置时作为回退。如果启用了 Custom OG Images 插件,它会成为每页的新默认图片,但可以通过为该页面设置 socialImage frontmatter 属性覆盖。

自定义

你可以通过传递自定义组件给 imageStructure,完全自定义生成图片的外观。该组件接收 JSX 及部分页面元数据/配置选项,并通过 satori 转换为图片。Vercel 提供了一个在线预览工具,可用于预览你的 JSX 设计,非常适合原型设计。

字体

你还会收到一个包含标题和正文字体的数组(第一个为标题,第二个为正文)。字体与 quartz.config.tstheme.typography.headertheme.typography.body 选择的字体一致,并以 satori 所需格式传递。要在 CSS 中使用,使用 .name 属性(如 fontFamily: fonts[1].name 使用“正文”字体)。

以下是一个使用标题字体的组件示例:

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

示例

以下是一些可用作起点的图片组件示例:

基础示例

该示例生成的图片如下所示:

亮色暗色
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,
) => {
  // 超过多少字符切换为小号字体
  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>
  )
}

高级示例

以下示例包含自定义背景和格式化日期的社交图片:

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>
  )
}