quartz.config.ts

    theme: {
      fontOrigin: "local",
      cdnCaching: false,
      typography: {
        header: "LINE Seed JP",
        body: "LINE Seed JP",
        code: "Juisee",
      },
 

custom.scss

@use "./base.scss";
 
// put your custom CSS here!
 
// ローカルフォントの定義
@font-face {
  font-family: 'LINE Seed JP';
  src: url('/static/font/LINESeedJP_OTF_Rg.otf') format('opentype');
  font-weight: normal;
  font-style: normal;
  font-display: swap;
}
 
@font-face {
  font-family: 'LINE Seed JP';
  src: url('/static/font/LINESeedJP_OTF_Bd.otf') format('opentype');
  font-weight: bold;
  font-style: normal;
  font-display: swap;
}
 
@font-face {
  font-family: 'LINE Seed JP';
  src: url('/static/font/LINESeedJP_OTF_Eb.otf') format('opentype');
  font-weight: 800;
  font-style: normal;
  font-display: swap;
}
 
@font-face {
  font-family: 'LINE Seed JP';
  src: url('/static/font/LINESeedJP_OTF_Th.otf') format('opentype');
  font-weight: 300;
  font-style: normal;
  font-display: swap;
}
 
@font-face {
  font-family: 'Juisee';
  src: url('/static/font/Juisee-Regular.ttf') format('truetype');
  font-weight: normal;
  font-style: normal;
  font-display: swap;
}
 
@font-face {
  font-family: 'Juisee';
  src: url('/static/font/Juisee-Bold.ttf') format('truetype');
  font-weight: bold;
  font-style: normal;
  font-display: swap;
}
 
@font-face {
  font-family: 'Juisee';
  src: url('/static/font/Juisee-RegularItalic.ttf') format('truetype');
  font-weight: normal;
  font-style: italic;
  font-display: swap;
}
 
@font-face {
  font-family: 'Juisee';
  src: url('/static/font/Juisee-BoldItalic.ttf') format('truetype');
  font-weight: bold;
  font-style: italic;
  font-display: swap;
}
 
 

OGPの設定をいじる
Custom OG Images

og.tsx

import { promises as fs } from "fs"
import { FontWeight, SatoriOptions } from "satori/wasm"
import { GlobalConfiguration } from "../cfg"
import { QuartzPluginData } from "../plugins/vfile"
import { JSXInternal } from "preact/src/jsx"
import { FontSpecification, getFontSpecificationName, ThemeKey } from "./theme"
import path from "path"
import { QUARTZ, joinSegments } from "./path"
import { formatDate, getDate } from "../components/Date"
import readingTime from "reading-time"
import { i18n } from "../i18n"
import chalk from "chalk"
 
const defaultHeaderWeight = [700]
const defaultBodyWeight = [400]
 
// LINE Seed JP フォントのパス
const lineSeedRegularPath = joinSegments(QUARTZ, "static", "fonts", "LINESeedJP_OTF_Rg.otf")
const lineSeedBoldPath = joinSegments(QUARTZ, "static", "fonts", "LINESeedJP_OTF_Bd.otf")
// Juisee フォントのパス
const juiseeRegularPath = joinSegments(QUARTZ, "static", "fonts", "Juisee-Regular.ttf")
const juiseeBoldPath = joinSegments(QUARTZ, "static", "fonts", "Juisee-Bold.ttf")
 
export async function getSatoriFonts(headerFont: FontSpecification, bodyFont: FontSpecification) {
  // Get all weights for header and body fonts
  const headerWeights: FontWeight[] = (
    typeof headerFont === "string"
      ? defaultHeaderWeight
      : (headerFont.weights ?? defaultHeaderWeight)
  ) as FontWeight[]
  const bodyWeights: FontWeight[] = (
    typeof bodyFont === "string" ? defaultBodyWeight : (bodyFont.weights ?? defaultBodyWeight)
  ) as FontWeight[]
 
  const headerFontName = typeof headerFont === "string" ? headerFont : headerFont.name
  const bodyFontName = typeof bodyFont === "string" ? bodyFont : bodyFont.name
 
  // フォントの設定
  let fonts: SatoriOptions["fonts"] = []
  
  // ローカルフォントを直接チェック - fontOriginはcfg.configuration.theme.fontOriginから取得します
  try {
    // LINE Seed JP (ヘッダーとボディ用)
    if (headerFontName === "LINE Seed JP" || bodyFontName === "LINE Seed JP") {
      fonts.push({
        name: "LINE Seed JP",
        data: await fs.readFile(path.resolve(lineSeedRegularPath)),
        weight: 400,
        style: "normal" as const,
      })
      fonts.push({
        name: "LINE Seed JP",
        data: await fs.readFile(path.resolve(lineSeedBoldPath)),
        weight: 700,
        style: "normal" as const,
      })
    }
    
    // Juisee (コード用)
    if (headerFontName === "Juisee" || bodyFontName === "Juisee") {
      fonts.push({
        name: "Juisee",
        data: await fs.readFile(path.resolve(juiseeRegularPath)),
        weight: 400,
        style: "normal" as const,
      })
      fonts.push({
        name: "Juisee",
        data: await fs.readFile(path.resolve(juiseeBoldPath)),
        weight: 700,
        style: "normal" as const,
      })
    }
    
    // ローカルフォントが正常に読み込まれた場合は、ここで関数を終了
    if (fonts.length > 0) {
      return fonts
    }
  } catch (error) {
    console.log(chalk.yellow(`\nWarning: Failed to load local fonts: ${error}`))
    // ローカルフォントの読み込みに失敗した場合は、Google Fontsにフォールバック
  }
 
  // 以下は元のGoogle Fontsからの読み込みコード(フォールバック用)
  // 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 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),
  ])
 
  // Filter out any failed fetches and combine header and body fonts
  fonts = [
    ...headerFonts.filter((font): font is NonNullable<typeof font> => font !== null),
    ...bodyFonts.filter((font): font is NonNullable<typeof font> => font !== null),
  ]
 
  return fonts
}
 
/**
 * Get the `.ttf` file of a google font
 * @param fontName name of google font
 * @param weight what font weight to fetch font
 * @returns `.ttf` file of google font
 */
export async function fetchTtf(
  rawFontName: string,
  weight: FontWeight,
): 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)
 
  // Check if font exists in cache
  try {
    await fs.access(cachePath)
    return fs.readFile(cachePath)
  } catch (error) {
    // ignore errors and fetch font
  }
 
  // Get css file from google fonts
  const cssResponse = await fetch(
    `https://fonts.googleapis.com/css2?family=${fontName}:wght@${weight}`,
  )
  const css = await cssResponse.text()
 
  // Extract .ttf url from css file
  const urlRegex = /url\((https:\/\/fonts.gstatic.com\/s\/.*?.ttf)\)/g
  const match = urlRegex.exec(css)
 
  if (!match) {
    console.log(
      chalk.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())
  await fs.mkdir(cacheDir, { recursive: true })
  await fs.writeFile(cachePath, fontData)
 
  return fontData
}
 
export type SocialImageOptions = {
  /**
   * What color scheme to use for image generation (uses colors from config theme)
   */
  colorScheme: ThemeKey
  /**
   * Height to generate image with in pixels (should be around 630px)
   */
  height: number
  /**
   * Width to generate image with in pixels (should be around 1200px)
   */
  width: number
  /**
   * Whether to use the auto generated image for the root path ("/", when set to false) or the default og image (when set to true).
   */
  excludeRoot: boolean
  /**
   * JSX to use for generating image. See satori docs for more info (https://github.com/vercel/satori)
   */
  imageStructure: (
    options: ImageOptions & {
      userOpts: UserOpts
      iconBase64?: string
    },
  ) => JSXInternal.Element
}
 
export type UserOpts = Omit<SocialImageOptions, "imageStructure">
 
export type ImageOptions = {
  /**
   * what title to use as header in image
   */
  title: string
  /**
   * what description to use as body in image
   */
  description: string
  /**
   * header + body font to be used when generating satori image (as promise to work around sync in component)
   */
  fonts: SatoriOptions["fonts"]
  /**
   * `GlobalConfiguration` of quartz (used for theme/typography)
   */
  cfg: GlobalConfiguration
  /**
   * full file data of current page
   */
  fileData: QuartzPluginData
}
 
// This is the default template for generated social image.
export const defaultImage: SocialImageOptions["imageStructure"] = ({
  cfg,
  userOpts,
  title,
  description,
  fileData,
  iconBase64,
}) => {
  const { colorScheme } = userOpts
  const fontBreakPoint = 32
  const useSmallerFont = title.length > fontBreakPoint
 
  // Format date if available
  const rawDate = getDate(cfg, fileData)
  const date = rawDate ? formatDate(rawDate, cfg.locale) : null
 
  // Calculate reading time
  const { minutes } = readingTime(fileData.text ?? "")
  const readingTimeText = i18n(cfg.locale).components.contentMeta.readingTime({
    minutes: Math.ceil(minutes),
  })
 
  // 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
      style={{
        display: "flex",
        flexDirection: "column",
        height: "100%",
        width: "100%",
        backgroundColor: cfg.theme.colors[colorScheme].light,
        padding: "2.5rem",
        fontFamily: bodyFont,
      }}
    >
      {/* Header Section */}
      <div
        style={{
          display: "flex",
          alignItems: "center",
          gap: "1rem",
          marginBottom: "0.5rem",
        }}
      >
        {iconBase64 && (
          <img
            src={iconBase64}
            width={56}
            height={56}
            style={{
              borderRadius: "50%",
            }}
          />
        )}
        <div
          style={{
            display: "flex",
            fontSize: 32,
            color: cfg.theme.colors[colorScheme].gray,
            fontFamily: bodyFont,
          }}
        >
          {cfg.baseUrl}
        </div>
      </div>
 
      {/* Title Section */}
      <div
        style={{
          display: "flex",
          marginTop: "1rem",
          marginBottom: "1.5rem",
        }}
      >
        <h1
          style={{
            margin: 0,
            fontSize: useSmallerFont ? 64 : 72,
            fontFamily: headerFont,
            fontWeight: 700,
            color: cfg.theme.colors[colorScheme].dark,
            lineHeight: 1.2,
            display: "-webkit-box",
            WebkitBoxOrient: "vertical",
            WebkitLineClamp: 2,
            overflow: "hidden",
            textOverflow: "ellipsis",
          }}
        >
          {title}
        </h1>
      </div>
 
      {/* Description Section */}
      <div
        style={{
          display: "flex",
          flex: 1,
          fontSize: 36,
          color: cfg.theme.colors[colorScheme].darkgray,
          lineHeight: 1.4,
        }}
      >
        <p
          style={{
            margin: 0,
            display: "-webkit-box",
            WebkitBoxOrient: "vertical",
            WebkitLineClamp: 5,
            overflow: "hidden",
            textOverflow: "ellipsis",
          }}
        >
          {description}
        </p>
      </div>
 
      {/* Footer with Metadata */}
      <div
        style={{
          display: "flex",
          alignItems: "center",
          justifyContent: "space-between",
          marginTop: "2rem",
          paddingTop: "2rem",
          borderTop: `1px solid ${cfg.theme.colors[colorScheme].lightgray}`,
        }}
      >
        {/* Left side - Date and Reading Time */}
        <div
          style={{
            display: "flex",
            alignItems: "center",
            gap: "2rem",
            color: cfg.theme.colors[colorScheme].gray,
            fontSize: 28,
          }}
        >
          {date && (
            <div style={{ display: "flex", alignItems: "center" }}>
              <svg
                style={{ marginRight: "0.5rem" }}
                width="28"
                height="28"
                viewBox="0 0 24 24"
                fill="none"
                stroke="currentColor"
              >
                <rect x="3" y="4" width="18" height="18" rx="2" ry="2"></rect>
                <line x1="16" y1="2" x2="16" y2="6"></line>
                <line x1="8" y1="2" x2="8" y2="6"></line>
                <line x1="3" y1="10" x2="21" y2="10"></line>
              </svg>
              {date}
            </div>
          )}
          <div style={{ display: "flex", alignItems: "center" }}>
            <svg
              style={{ marginRight: "0.5rem" }}
              width="28"
              height="28"
              viewBox="0 0 24 24"
              fill="none"
              stroke="currentColor"
            >
              <circle cx="12" cy="12" r="10"></circle>
              <polyline points="12 6 12 12 16 14"></polyline>
            </svg>
            {readingTimeText}
          </div>
        </div>
 
        {/* Right side - Tags */}
        <div
          style={{
            display: "flex",
            gap: "0.5rem",
            flexWrap: "wrap",
            justifyContent: "flex-end",
            maxWidth: "60%",
          }}
        >
          {tags.slice(0, 3).map((tag: string) => (
            <div
              style={{
                display: "flex",
                padding: "0.5rem 1rem",
                backgroundColor: cfg.theme.colors[colorScheme].highlight,
                color: cfg.theme.colors[colorScheme].secondary,
                borderRadius: "10px",
                fontSize: 24,
              }}
            >
              #{tag}
            </div>
          ))}
        </div>
      </div>
    </div>
  )
}