fl0werpowers
2025-05-28 951d1dec24eb8e0bea4ec548cc79c5ce718bf02f
quartz/util/og.tsx
@@ -3,15 +3,17 @@
import { GlobalConfiguration } from "../cfg"
import { QuartzPluginData } from "../plugins/vfile"
import { JSXInternal } from "preact/src/jsx"
import { FontSpecification, ThemeKey } from "./theme"
import { FontSpecification, getFontSpecificationName, ThemeKey } from "./theme"
import path from "path"
import { QUARTZ } from "./path"
import { formatDate, getDate } from "../components/Date"
import readingTime from "reading-time"
import { i18n } from "../i18n"
import { styleText } from "util"
const defaultHeaderWeight = [700]
const defaultBodyWeight = [400]
export async function getSatoriFonts(headerFont: FontSpecification, bodyFont: FontSpecification) {
  // Get all weights for header and body fonts
  const headerWeights: FontWeight[] = (
@@ -26,29 +28,38 @@
  const headerFontName = typeof headerFont === "string" ? headerFont : headerFont.name
  const bodyFontName = typeof bodyFont === "string" ? bodyFont : bodyFont.name
  // Fetch fonts for all weights
  const headerFontPromises = headerWeights.map((weight) => fetchTtf(headerFontName, weight))
  const bodyFontPromises = bodyWeights.map((weight) => fetchTtf(bodyFontName, weight))
  // Fetch fonts for all weights and convert to satori format in one go
  const headerFontPromises = headerWeights.map(async (weight) => {
    const data = await fetchTtf(headerFontName, weight)
    if (!data) return null
    return {
      name: headerFontName,
      data,
      weight,
      style: "normal" as const,
    }
  })
  const [headerFontData, bodyFontData] = await Promise.all([
  const bodyFontPromises = bodyWeights.map(async (weight) => {
    const data = await fetchTtf(bodyFontName, weight)
    if (!data) return null
    return {
      name: bodyFontName,
      data,
      weight,
      style: "normal" as const,
    }
  })
  const [headerFonts, bodyFonts] = await Promise.all([
    Promise.all(headerFontPromises),
    Promise.all(bodyFontPromises),
  ])
  // Convert fonts to satori font format and return
  // Filter out any failed fetches and combine header and body fonts
  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,
    })),
    ...headerFonts.filter((font): font is NonNullable<typeof font> => font !== null),
    ...bodyFonts.filter((font): font is NonNullable<typeof font> => font !== null),
  ]
  return fonts
@@ -61,10 +72,11 @@
 * @returns `.ttf` file of google font
 */
export async function fetchTtf(
  fontName: string,
  rawFontName: string,
  weight: FontWeight,
): Promise<Buffer<ArrayBufferLike>> {
  const cacheKey = `${fontName.replaceAll(" ", "-")}-${weight}`
): Promise<Buffer<ArrayBufferLike> | undefined> {
  const fontName = rawFontName.replaceAll(" ", "+")
  const cacheKey = `${fontName}-${weight}`
  const cacheDir = path.join(QUARTZ, ".quartz-cache", "fonts")
  const cachePath = path.join(cacheDir, cacheKey)
@@ -87,20 +99,20 @@
  const match = urlRegex.exec(css)
  if (!match) {
    throw new Error("Could not fetch font")
    console.log(
      styleText(
        "yellow",
        `\nWarning: Failed to fetch font ${rawFontName} with weight ${weight}, got ${cssResponse.statusText}`,
      ),
    )
    return
  }
  // fontData is an ArrayBuffer containing the .ttf file data
  const fontResponse = await fetch(match[1])
  const fontData = Buffer.from(await fontResponse.arrayBuffer())
  try {
    await fs.mkdir(cacheDir, { recursive: true })
    await fs.writeFile(cachePath, fontData)
  } catch (error) {
    console.warn(`Failed to cache font: ${error}`)
    // Continue even if caching fails
  }
  await fs.mkdir(cacheDir, { recursive: true })
  await fs.writeFile(cachePath, fontData)
  return fontData
}
@@ -124,21 +136,12 @@
  excludeRoot: boolean
  /**
   * JSX to use for generating image. See satori docs for more info (https://github.com/vercel/satori)
   * @param cfg global quartz config
   * @param userOpts options that can be set by user
   * @param title title of current page
   * @param description description of current page
   * @param fonts global font that can be used for styling
   * @param fileData full fileData of current page
   * @returns prepared jsx to be used for generating image
   */
  imageStructure: (
    cfg: GlobalConfiguration,
    userOpts: UserOpts,
    title: string,
    description: string,
    fonts: SatoriOptions["fonts"],
    fileData: QuartzPluginData,
    options: ImageOptions & {
      userOpts: UserOpts
      iconBase64?: string
    },
  ) => JSXInternal.Element
}
@@ -168,17 +171,17 @@
}
// This is the default template for generated social image.
export const defaultImage: SocialImageOptions["imageStructure"] = (
  cfg: GlobalConfiguration,
  { colorScheme }: UserOpts,
  title: string,
  description: string,
  fonts: SatoriOptions["fonts"],
  fileData: QuartzPluginData,
) => {
export const defaultImage: SocialImageOptions["imageStructure"] = ({
  cfg,
  userOpts,
  title,
  description,
  fileData,
  iconBase64,
}) => {
  const { colorScheme } = userOpts
  const fontBreakPoint = 32
  const useSmallerFont = title.length > fontBreakPoint
  const iconPath = `https://${cfg.baseUrl}/static/icon.png`
  // Format date if available
  const rawDate = getDate(cfg, fileData)
@@ -192,6 +195,8 @@
  // Get tags if available
  const tags = fileData.frontmatter?.tags ?? []
  const bodyFont = getFontSpecificationName(cfg.theme.typography.body)
  const headerFont = getFontSpecificationName(cfg.theme.typography.header)
  return (
    <div
@@ -202,7 +207,7 @@
        width: "100%",
        backgroundColor: cfg.theme.colors[colorScheme].light,
        padding: "2.5rem",
        fontFamily: fonts[1].name,
        fontFamily: bodyFont,
      }}
    >
      {/* Header Section */}
@@ -214,20 +219,22 @@
          marginBottom: "0.5rem",
        }}
      >
        <img
          src={iconPath}
          width={56}
          height={56}
          style={{
            borderRadius: "50%",
          }}
        />
        {iconBase64 && (
          <img
            src={iconBase64}
            width={56}
            height={56}
            style={{
              borderRadius: "50%",
            }}
          />
        )}
        <div
          style={{
            display: "flex",
            fontSize: 32,
            color: cfg.theme.colors[colorScheme].gray,
            fontFamily: fonts[1].name,
            fontFamily: bodyFont,
          }}
        >
          {cfg.baseUrl}
@@ -246,7 +253,7 @@
          style={{
            margin: 0,
            fontSize: useSmallerFont ? 64 : 72,
            fontFamily: fonts[0].name,
            fontFamily: headerFont,
            fontWeight: 700,
            color: cfg.theme.colors[colorScheme].dark,
            lineHeight: 1.2,
@@ -254,6 +261,7 @@
            WebkitBoxOrient: "vertical",
            WebkitLineClamp: 2,
            overflow: "hidden",
            textOverflow: "ellipsis",
          }}
        >
          {title}
@@ -275,8 +283,9 @@
            margin: 0,
            display: "-webkit-box",
            WebkitBoxOrient: "vertical",
            WebkitLineClamp: 4,
            WebkitLineClamp: 5,
            overflow: "hidden",
            textOverflow: "ellipsis",
          }}
        >
          {description}