diff --git a/quartz/components/Head.tsx b/quartz/components/Head.tsx index 09156c9ee..b6a7e8d07 100644 --- a/quartz/components/Head.tsx +++ b/quartz/components/Head.tsx @@ -1,7 +1,7 @@ import { i18n } from "../i18n" import { FullSlug, joinSegments, pathToRoot } from "../util/path" import { CSSResourceToStyleElement, JSResourceToScriptElement } from "../util/resources" -import { googleFontHref } from "../util/theme" +import { getFontSpecificationName, googleFontHref } from "../util/theme" import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types" import satori, { SatoriOptions } from "satori" import { loadEmoji, getIconCode } from "../util/emoji" @@ -77,7 +77,9 @@ export default (() => { // Memoize google fonts if (!fontsPromise && cfg.generateSocialImages) { - fontsPromise = getSatoriFont(cfg.theme.typography.header, cfg.theme.typography.body) + const headerFont = getFontSpecificationName(cfg.theme.typography.header) + const bodyFont = getFontSpecificationName(cfg.theme.typography.body) + fontsPromise = getSatoriFont(headerFont, bodyFont) } const slug = fileData.filePath diff --git a/quartz/util/theme.ts b/quartz/util/theme.ts index 0c903066f..06ddd8c43 100644 --- a/quartz/util/theme.ts +++ b/quartz/util/theme.ts @@ -15,11 +15,19 @@ interface Colors { darkMode: ColorScheme } +type FontSpecification = + | string + | { + name: string + weights?: number[] + includeItalic?: boolean + } + export interface Theme { typography: { - header: string - body: string - code: string + header: FontSpecification + body: FontSpecification + code: FontSpecification } cdnCaching: boolean colors: Colors @@ -32,9 +40,54 @@ const DEFAULT_SANS_SERIF = 'system-ui, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"' const DEFAULT_MONO = "ui-monospace, SFMono-Regular, SF Mono, Menlo, monospace" +export function getFontSpecificationName(spec: FontSpecification): string { + if (typeof spec === "string") { + return spec + } + + return spec.name +} + +function formatFontSpecification(type: "header" | "body" | "code", spec: FontSpecification) { + if (typeof spec === "string") { + spec = { name: spec } + } + + const defaultIncludeWeights = type === "header" ? [400, 700] : [400, 600] + const defaultIncludeItalic = type === "body" + const weights = spec.weights ?? defaultIncludeWeights + const italic = spec.includeItalic ?? defaultIncludeItalic + + const features: string[] = [] + if (italic) { + features.push("ital") + } + + if (weights.length > 1) { + const weightSpec = italic + ? weights + .flatMap((w) => [`0,${w}`, `1,${w}`]) + .sort() + .join(";") + : weights.join(";") + + features.push(`wght@${weightSpec}`) + } + + if (features.length > 0) { + return `${spec.name}:${features.join(",")}` + } + + return spec.name +} + export function googleFontHref(theme: Theme) { const { code, header, body } = theme.typography - return `https://fonts.googleapis.com/css2?family=${code}&family=${header}:wght@400;700&family=${body}:ital,wght@0,400;0,600;1,400;1,600&display=swap` + const headerFont = formatFontSpecification("header", header) + const bodyFont = formatFontSpecification("body", body) + const codeFont = formatFontSpecification("code", code) + + return `https://fonts.googleapis.com/css2?family=${bodyFont}&family=${headerFont}&family=${codeFont}&display=swap` } export function joinStyles(theme: Theme, ...stylesheet: string[]) {