@docubook/plugin-og-image 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +66 -2
- package/dist/cards.d.ts +54 -0
- package/dist/index.d.ts +40 -0
- package/dist/index.js +1008 -0
- package/dist/index.js.map +1 -0
- package/dist/layouts.d.ts +27 -0
- package/dist/options.d.ts +58 -0
- package/dist/publish.d.ts +39 -0
- package/dist/renderer.d.ts +26 -0
- package/dist/site.d.ts +78 -0
- package/package.json +61 -3
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../src/options.ts","../src/site.ts","../src/cards.ts","../src/layouts.ts","../src/renderer.ts","../src/publish.ts","../src/index.ts"],"sourcesContent":["/**\n * Plugin options from `docu.json` and their validation. Pure: no file system\n * and no flame imports, so a typo stops `flame build` and `flame dev` before\n * anything else runs.\n */\n\nexport const LOG_PREFIX = \"[plugin:og-image]\";\n\nexport type Layout = \"card\" | \"minimal\";\n\nexport const LAYOUTS: readonly Layout[] = [\"card\", \"minimal\"];\n\nexport interface FontOption {\n /** Font file (TTF, OTF, WOFF or WOFF2), relative to the project root. */\n path: string;\n /** Weight the file provides. Default: read from the file. */\n weight?: number;\n /** Style the file provides. Default: read from the file. */\n style?: \"normal\" | \"italic\";\n}\n\nexport interface PagesOption {\n /** Docs pages. Default `true`. */\n docs?: boolean;\n /** The docs index (`docs/index.mdx`). Default `true`. */\n index?: boolean;\n /** The landing page, when `meta.ogImage` does not already set its image. Default `true`. */\n home?: boolean;\n}\n\nexport interface OgImageOptions {\n /** Card layout. Default `card`. */\n layout?: Layout;\n /**\n * Logo file: a path relative to the project root, or a site path such as\n * `/docs/assets/images/logo.svg`. SVG, PNG, JPEG or WebP, at most 1 MB.\n * `false` renders no logo. Default: `navbar.logo.src`.\n */\n logo?: string | false;\n /** Name in the card's top row. Default: `navbar.logoText`, then `meta.title`. */\n siteName?: string;\n /** Which pages get a card. */\n pages?: PagesOption;\n /**\n * Fonts tried before the built-in Geist, which covers Latin-1 only. Add one\n * for titles in other scripts.\n */\n fonts?: FontOption[];\n}\n\nexport interface ResolvedOptions {\n layout: Layout;\n /** `undefined` means \"use `navbar.logo.src`\". */\n logo: string | false | undefined;\n siteName: string | undefined;\n pages: Required<PagesOption>;\n fonts: FontOption[];\n}\n\nconst KNOWN_KEYS = new Set([\"layout\", \"logo\", \"siteName\", \"pages\", \"fonts\"]);\nconst PAGE_KEYS = new Set([\"docs\", \"index\", \"home\"]);\n\nconst EXAMPLE = `Example:\n \"plugins\": [[\"@docubook/plugin-og-image\", { \"layout\": \"card\" }]]`;\n\nfunction fail(message: string): never {\n throw new Error(`${LOG_PREFIX} ${message}\\n${EXAMPLE}`);\n}\n\n/**\n * A value that names something on another host or inline data rather than a\n * local file: `https://…`, `//cdn…`, `data:…`. Single-letter schemes are left\n * alone so a Windows drive path (`C:\\…`) is not mistaken for a URL.\n */\nexport function isRemoteReference(value: string): boolean {\n return /^[a-z][a-z\\d+.-]+:/i.test(value) || value.startsWith(\"//\");\n}\n\nfunction isPlainObject(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\nexport function resolveOptions(input: unknown): ResolvedOptions {\n if (input === undefined || input === null) input = {};\n if (!isPlainObject(input)) fail(\"options must be an object.\");\n\n for (const key of Object.keys(input)) {\n if (!KNOWN_KEYS.has(key)) {\n fail(`unknown option \"${key}\". Valid options: ${[...KNOWN_KEYS].join(\", \")}.`);\n }\n }\n\n const layout = input.layout ?? \"card\";\n if (typeof layout !== \"string\" || !LAYOUTS.includes(layout as Layout)) {\n fail(`\"layout\" must be one of ${LAYOUTS.map((l) => `\"${l}\"`).join(\", \")}.`);\n }\n\n const logo = input.logo;\n if (logo !== undefined && logo !== false) {\n if (typeof logo !== \"string\" || logo.trim() === \"\") {\n fail(`\"logo\" must be a file path or false.`);\n }\n if (isRemoteReference(logo)) {\n fail(`\"logo\" must be a local file, not \"${logo}\". The build never downloads files.`);\n }\n }\n\n const siteName = input.siteName;\n if (siteName !== undefined && (typeof siteName !== \"string\" || siteName.trim() === \"\")) {\n fail(`\"siteName\" must be a non-empty string.`);\n }\n\n const pages = { docs: true, index: true, home: true };\n if (input.pages !== undefined) {\n if (!isPlainObject(input.pages)) fail(`\"pages\" must be an object such as { \"home\": false }.`);\n for (const [key, value] of Object.entries(input.pages)) {\n if (!PAGE_KEYS.has(key)) fail(`unknown key \"pages.${key}\". Valid keys: docs, index, home.`);\n if (typeof value !== \"boolean\") fail(`\"pages.${key}\" must be true or false.`);\n pages[key as keyof typeof pages] = value;\n }\n }\n\n const fonts: FontOption[] = [];\n if (input.fonts !== undefined) {\n if (!Array.isArray(input.fonts)) fail(`\"fonts\" must be an array of { \"path\": \"…\" }.`);\n input.fonts.forEach((font, i) => {\n if (!isPlainObject(font) || typeof font.path !== \"string\" || font.path.trim() === \"\") {\n fail(`\"fonts[${i}].path\" must be a file path.`);\n }\n if (isRemoteReference(font.path)) {\n fail(`\"fonts[${i}].path\" must be a local file, not \"${font.path}\".`);\n }\n if (\n font.weight !== undefined &&\n (typeof font.weight !== \"number\" || font.weight < 1 || font.weight > 1000)\n ) {\n fail(`\"fonts[${i}].weight\" must be a number from 1 to 1000.`);\n }\n if (font.style !== undefined && font.style !== \"normal\" && font.style !== \"italic\") {\n fail(`\"fonts[${i}].style\" must be \"normal\" or \"italic\".`);\n }\n fonts.push({\n path: font.path,\n ...(font.weight !== undefined && { weight: font.weight as number }),\n ...(font.style !== undefined && { style: font.style as \"normal\" | \"italic\" }),\n });\n });\n }\n\n return {\n layout: layout as Layout,\n logo: logo as string | false | undefined,\n siteName: siteName as string | undefined,\n pages,\n fonts,\n };\n}\n","/**\n * What the plugin needs to know about the site, derived from `docu.json` the\n * same way flame derives it. `@docubook/flame` has no runtime exports, so the\n * base-path rules are mirrored here; `__tests__/site.test.ts` pins them to\n * flame's own cases.\n */\n\nimport { existsSync, statSync } from \"node:fs\";\nimport { extname, isAbsolute, join, relative, resolve } from \"node:path\";\nimport type { DocuConfig } from \"@docubook/flame\";\nimport type { ResolvedOptions } from \"./options\";\nimport { isRemoteReference } from \"./options\";\n\nexport const DEFAULT_BASE_PATH = \"/docs\";\n\n/** Output directory of the cards, below the docs output directory. */\nexport const CARD_DIR = \"assets/og\";\n\nexport interface Site {\n /** Project root: the directory that holds `docu.json`. */\n root: string;\n /** `<root>/docs`. */\n docsDir: string;\n /** Docs URL prefix: `/docs` by default, `\"\"` at the deployment root. */\n prefix: string;\n /** `meta.baseURL` without a trailing slash, `\"\"` when unset. */\n baseURL: string;\n /** Host and deployment path shown on the card, e.g. `docubook.pro`. */\n domain: string | undefined;\n /** Name in the card's top row. */\n siteName: string;\n /** Flame renders a landing page (`experimental.pages` is off). */\n hasLanding: boolean;\n /** Flame writes the docs index page. */\n hasDocsIndex: boolean;\n /** Sidebar routes from `docu.json`, for the section label. */\n routes: DocuConfig[\"routes\"];\n}\n\n/**\n * Strip slashes from the end (and the start, unless `endOnly`) in one linear\n * pass. A `/\\/+$/` regex backtracks polynomially on long runs of `/`.\n */\nexport function trimSlashes(value: string, endOnly = false): string {\n let start = 0;\n let end = value.length;\n if (!endOnly) while (start < end && value[start] === \"/\") start++;\n while (end > start && value[end - 1] === \"/\") end--;\n return value.slice(start, end);\n}\n\n/** Walk up from `start` to the directory holding `docu.json`, as flame does. */\nexport function findProjectRoot(start: string = process.cwd()): string {\n let dir = start;\n for (let i = 0; i < 6; i++) {\n if (existsSync(join(dir, \"docu.json\"))) return dir;\n const parent = resolve(dir, \"..\");\n if (parent === dir) break;\n dir = parent;\n }\n return start;\n}\n\n/** Mirror of flame's `normalizeBasePath`. */\nexport function normalizeBasePath(value: string | undefined | null): string {\n if (typeof value !== \"string\") return DEFAULT_BASE_PATH;\n let path = value.trim();\n if (path.length === 0 || path === \"/\") return \"\";\n try {\n if (/^[a-zA-Z][a-zA-Z\\d+.-]*:\\/\\//.test(path)) path = new URL(path).pathname;\n } catch {\n // malformed URL: treat the raw value as a path\n }\n path = `/${trimSlashes(path)}`;\n return path === \"/\" ? \"\" : path;\n}\n\n/** Mirror of flame's `canonicalBasePath`. */\nexport function canonicalBasePath(value: string): string {\n const structural = normalizeBasePath(value);\n if (structural === \"\") return \"\";\n const cleaned = structural\n .replace(/\\s+/g, \"-\")\n .replace(/[^A-Za-z0-9\\-._~/]+/g, \"\")\n .split(\"/\")\n .filter((segment) => segment.length > 0 && segment !== \".\" && segment !== \"..\")\n .join(\"/\")\n .replace(/-{2,}/g, \"-\")\n .toLowerCase();\n return cleaned.length > 0 ? `/${cleaned}` : \"\";\n}\n\n/** Mirror of flame's `resolveBasePath`. */\nexport function resolveBasePath(config: DocuConfig): string {\n const basePath = config.meta?.basePath;\n if (typeof basePath !== \"string\") return DEFAULT_BASE_PATH;\n if (normalizeBasePath(basePath) === \"\") return \"\";\n return canonicalBasePath(basePath) || DEFAULT_BASE_PATH;\n}\n\n/** Mirror of flame's `resolveDeployPath`: the path part of `meta.baseURL`. */\nexport function resolveDeployPath(config: DocuConfig): string {\n const raw = config.meta?.baseURL;\n if (typeof raw !== \"string\") return \"\";\n try {\n return normalizeBasePath(new URL(raw).pathname);\n } catch {\n return raw.startsWith(\"/\") ? normalizeBasePath(raw) : \"\";\n }\n}\n\nfunction domainOf(baseURL: string): string | undefined {\n try {\n const url = new URL(baseURL);\n return `${url.host}${trimSlashes(url.pathname, true)}`;\n } catch {\n return undefined;\n }\n}\n\nexport function resolveSite(config: DocuConfig, options: ResolvedOptions, root: string): Site {\n const prefix = resolveBasePath(config);\n const ownPages = config.experimental?.pages === true;\n const baseURL = trimSlashes((config.meta?.baseURL ?? \"\").trim(), true);\n const navbarName = config.navbar?.logoText?.trim();\n return {\n root,\n docsDir: join(root, \"docs\"),\n prefix,\n baseURL,\n domain: baseURL ? domainOf(baseURL) : undefined,\n siteName: options.siteName ?? (navbarName || config.meta?.title?.trim() || \"Docs\"),\n hasLanding: !ownPages,\n // At the deployment root without own pages the landing page owns `/`, so\n // flame skips the docs index (see build.ts `docsIndexWritable`).\n hasDocsIndex: prefix !== \"\" || ownPages,\n routes: Array.isArray(config.routes) ? config.routes : [],\n };\n}\n\n/**\n * Title of the top-level sidebar group a docs page sits in, e.g. \"Getting\n * Started\" for `getting-started/configuration`. Top-level pages have none.\n */\nexport function sectionFor(routes: Site[\"routes\"], slug: string): string | undefined {\n const segments = slug.split(\"/\");\n if (segments.length < 2) return undefined;\n const route = routes.find(\n (r) => typeof r.href === \"string\" && trimSlashes(r.href) === segments[0]\n );\n const title = route?.title?.trim();\n return title || undefined;\n}\n\n/** Absolute directory flame writes the docs pages to. */\nexport function docsOutputDir(outDir: string, prefix: string): string {\n return prefix === \"\" ? outDir : join(outDir, prefix.slice(1));\n}\n\n/**\n * Absolute URL of a card, as flame's SEO resolver builds it for a relative\n * `frontmatter.image`. Root-relative when `meta.baseURL` is unset.\n */\nexport function cardUrl(site: Site, file: string): string {\n return `${site.baseURL}${site.prefix}/${CARD_DIR}/${file}`;\n}\n\n/** True when `file` is inside `root` (or is `root`). */\nexport function isInside(root: string, file: string): boolean {\n const rel = relative(root, file);\n return rel === \"\" || (!rel.startsWith(\"..\") && !isAbsolute(rel));\n}\n\nexport const LOGO_TYPES: Record<string, string> = {\n \".svg\": \"image/svg+xml\",\n \".png\": \"image/png\",\n \".jpg\": \"image/jpeg\",\n \".jpeg\": \"image/jpeg\",\n \".webp\": \"image/webp\",\n};\n\nexport type LogoLookup =\n { file: string } | { reason: \"remote\" | \"outside\" | \"missing\" | \"type\"; value: string };\n\n/**\n * Map a logo reference to a file in the project.\n *\n * `navbar.logo.src` is a site path (`/docs/assets/images/logo.svg`), while the\n * `logo` option may also be a project path (`brand/logo.svg`). Site paths\n * under the docs prefix, or under the default `/docs` spelling flame re-bases,\n * map to `docs/`; anything else is tried relative to the project root, then\n * to `docs/`.\n */\nexport function findLogoFile(value: string, site: Site, deployPath: string): LogoLookup {\n if (isRemoteReference(value)) return { reason: \"remote\", value };\n let path = value.split(/[?#]/)[0];\n const candidates: string[] = [];\n if (path.startsWith(\"/\")) {\n if (deployPath && path.startsWith(`${deployPath}/`)) path = path.slice(deployPath.length);\n for (const prefix of [site.prefix, DEFAULT_BASE_PATH]) {\n if (prefix && path.startsWith(`${prefix}/`)) {\n candidates.push(join(site.docsDir, path.slice(prefix.length + 1)));\n }\n }\n if (site.prefix === \"\") candidates.push(join(site.docsDir, path.slice(1)));\n candidates.push(join(site.root, path.slice(1)));\n } else {\n candidates.push(resolve(site.root, path), resolve(site.docsDir, path));\n }\n\n let outside = false;\n for (const candidate of candidates) {\n if (!isInside(site.root, candidate)) {\n outside = true;\n continue;\n }\n try {\n if (statSync(candidate).isFile()) {\n if (!(extname(candidate).toLowerCase() in LOGO_TYPES)) return { reason: \"type\", value };\n return { file: candidate };\n }\n } catch {\n // not there, try the next candidate\n }\n }\n return { reason: outside ? \"outside\" : \"missing\", value };\n}\n","/**\n * Turning a page into a card: what text it shows, whether it is skipped, and\n * the file name it is published under. Pure functions over frontmatter.\n */\n\nimport { createHash } from \"node:crypto\";\nimport { readFileSync } from \"node:fs\";\nimport { readFile } from \"node:fs/promises\";\nimport { join } from \"node:path\";\nimport matter from \"@11ty/gray-matter\";\nimport type { DocuConfig } from \"@docubook/flame\";\nimport type { ResolvedOptions } from \"./options\";\nimport { CARD_DIR, sectionFor, type Site } from \"./site\";\n\nexport type CardKind = \"docs\" | \"index\" | \"home\";\n\nexport interface Card {\n kind: CardKind;\n /** Docs slug; `\"\"` for the docs index and the landing page. */\n slug: string;\n title: string;\n description: string;\n /** Top-level sidebar group, docs pages only. */\n section: string | undefined;\n}\n\nexport interface PlannedCard {\n card: Card;\n /** File name below `assets/og/`, e.g. `guide/routing.1a2b3c4d.png`. */\n file: string;\n /** Hash of everything flame's page cache also sees; its first 8 chars name the file. */\n nameHash: string;\n}\n\nexport type SkipReason = \"disabled\" | \"author-image\" | \"opt-out\";\n\n/** Frontmatter value as flame's schema coerces it: primitives become strings. */\nfunction text(value: unknown): string {\n if (typeof value === \"string\") return value.trim();\n if (typeof value === \"number\" || typeof value === \"boolean\") return String(value);\n return \"\";\n}\n\nfunction overrides(frontmatter: Record<string, unknown>): Record<string, unknown> {\n const og = frontmatter.og;\n return typeof og === \"object\" && og !== null && !Array.isArray(og)\n ? (og as Record<string, unknown>)\n : {};\n}\n\n/**\n * Why a page gets no card, or `undefined` when it gets one. An image the\n * author set always wins; `og: false` opts the page out.\n */\nexport function skipReason(\n kind: CardKind,\n frontmatter: Record<string, unknown>,\n options: ResolvedOptions\n): SkipReason | undefined {\n if (!options.pages[kind]) return \"disabled\";\n if (frontmatter.og === false) return \"opt-out\";\n if (text(frontmatter.image)) return \"author-image\";\n return undefined;\n}\n\n/** Card text for a docs page or the docs index. Mirrors flame's title fallback. */\nexport function docsCard(\n kind: \"docs\" | \"index\",\n slug: string,\n frontmatter: Record<string, unknown>,\n site: Site\n): Card {\n const og = overrides(frontmatter);\n return {\n kind,\n slug,\n title: text(og.title) || text(frontmatter.title) || slug || \"Docs\",\n description: text(og.description) || text(frontmatter.description),\n section: kind === \"docs\" ? sectionFor(site.routes, slug) : undefined,\n };\n}\n\n/** Card text for the landing page: the hero headline and the site description. */\nexport function homeCard(config: DocuConfig): Card {\n const hero = (config as { home?: { hero?: { headline?: unknown } } }).home?.hero;\n return {\n kind: \"home\",\n slug: \"\",\n title: text(hero?.headline) || text(config.meta?.title) || \"Docs\",\n description: text(config.meta?.description),\n section: undefined,\n };\n}\n\nexport function sha256(...parts: Array<string | Uint8Array>): string {\n const hash = createHash(\"sha256\");\n for (const part of parts) hash.update(part);\n return hash.digest(\"hex\");\n}\n\n/**\n * Plan a card's file. The name hash covers only inputs that also invalidate\n * flame's page cache (plugin version, `docu.json`, the page's source), so a\n * page served from that cache always points at a file this build writes.\n */\nexport function planCard(\n card: Card,\n site: Site,\n options: ResolvedOptions,\n version: string\n): PlannedCard {\n const nameHash = sha256(\n JSON.stringify([\n version,\n options.layout,\n options.logo ?? null,\n options.fonts,\n site.siteName,\n site.domain ?? null,\n card.kind,\n card.title,\n card.description,\n card.section ?? null,\n ])\n );\n const stem = card.kind === \"docs\" ? card.slug : card.kind;\n return { card, file: `${stem}.${nameHash.slice(0, 8)}.png`, nameHash };\n}\n\n/** The value set as `frontmatter.image`; flame resolves it against the docs prefix. */\nexport function imageRef(file: string): string {\n return `./${CARD_DIR}/${file}`;\n}\n\n/** Source file candidates for a slug, in the order flame's scanner would find them. */\nexport function sourceCandidates(docsDir: string, slug: string): string[] {\n if (slug === \"\") return [join(docsDir, \"index.mdx\"), join(docsDir, \"index.md\")];\n return [\n join(docsDir, `${slug}.mdx`),\n join(docsDir, `${slug}.md`),\n join(docsDir, slug, \"index.mdx\"),\n join(docsDir, slug, \"index.md\"),\n ];\n}\n\n/** Frontmatter of a source file. Parsed with the same library `@docubook/core` uses. */\nfunction parseFrontmatter(raw: string): Record<string, unknown> {\n // An options object keeps gray-matter from caching every file's content.\n return matter(raw, {}).data as Record<string, unknown>;\n}\n\n/**\n * Frontmatter of a docs page as written in its source file, or `undefined`\n * when no source exists. Every card is planned from this, never from the\n * frontmatter other plugins hand on, so the page phase and `onEnd` always\n * agree on the file name.\n */\nexport async function readFrontmatter(\n docsDir: string,\n slug: string\n): Promise<Record<string, unknown> | undefined> {\n for (const file of sourceCandidates(docsDir, slug)) {\n let raw: string;\n try {\n raw = await readFile(file, \"utf8\");\n } catch {\n continue;\n }\n return parseFrontmatter(raw);\n }\n return undefined;\n}\n\n/** {@link readFrontmatter} for hooks that must return synchronously. */\nexport function readFrontmatterSync(\n docsDir: string,\n slug: string\n): Record<string, unknown> | undefined {\n for (const file of sourceCandidates(docsDir, slug)) {\n let raw: string;\n try {\n raw = readFileSync(file, \"utf8\");\n } catch {\n continue;\n }\n return parseFrontmatter(raw);\n }\n return undefined;\n}\n","/**\n * The two card layouts as Takumi node trees. Page text only ever lands in\n * `text` nodes, so frontmatter is drawn as text and never parsed as markup.\n */\n\nimport type { Node } from \"@takumi-rs/core\";\nimport type { Card } from \"./cards\";\nimport type { Layout } from \"./options\";\n\nexport const WIDTH = 1200;\nexport const HEIGHT = 630;\n\n/** Theme colours as flame's presets store them: HSL triplets such as `210 81% 56%`. */\nexport interface Palette {\n background: string;\n foreground: string;\n primary: string;\n muted: string;\n border: string;\n}\n\nexport interface Brand {\n siteName: string;\n domain: string | undefined;\n /** Logo bytes (SVG, PNG, JPEG or WebP), or none. */\n logo: Uint8Array | undefined;\n palette: Palette;\n /** CSS font-family stack: configured fonts, then Geist. */\n fontFamily: string;\n}\n\n/** CSS declarations a Takumi node accepts. */\ntype Style = NonNullable<Node[\"style\"]>;\n\nconst hsl = (triplet: string, alpha?: number): string =>\n alpha === undefined ? `hsl(${triplet})` : `hsl(${triplet} / ${alpha})`;\n\nconst box = (style: Style, children: Node[] = []): Node => ({\n type: \"container\",\n style,\n children,\n});\n\nconst label = (value: string, style: Style): Node => ({\n type: \"text\",\n text: value,\n style,\n});\n\n/** Two lines at most, then an ellipsis. */\nconst clamp: Style = { lineClamp: 2, textOverflow: \"ellipsis\", overflow: \"hidden\" };\n\nfunction logoImage(brand: Brand, size: number, radius = 0): Node[] {\n if (!brand.logo) return [];\n return [\n {\n type: \"image\",\n src: brand.logo,\n width: size,\n height: size,\n style: { width: size, height: size, borderRadius: radius, objectFit: \"contain\" },\n },\n ];\n}\n\nfunction identityRow(card: Card, brand: Brand, fontSize: number): Node {\n const { palette } = brand;\n return box(\n { display: \"flex\", alignItems: \"center\", gap: 18, fontSize, color: hsl(palette.muted) },\n [\n ...logoImage(brand, 56, 14),\n label(brand.siteName, { fontWeight: 600, color: hsl(palette.foreground) }),\n ...(card.section ? [label(\"›\", { opacity: 0.6 }), label(card.section, {})] : []),\n ]\n );\n}\n\nfunction textBlock(card: Card, brand: Brand, maxWidth: number): Node {\n return box({ display: \"flex\", flexDirection: \"column\", gap: 22, maxWidth }, [\n label(card.title, {\n fontSize: 64,\n fontWeight: 700,\n lineHeight: 1.1,\n letterSpacing: -1.5,\n color: hsl(brand.palette.foreground),\n ...clamp,\n }),\n ...(card.description\n ? [\n label(card.description, {\n fontSize: 30,\n lineHeight: 1.35,\n color: hsl(brand.palette.muted),\n ...clamp,\n }),\n ]\n : []),\n ]);\n}\n\n/**\n * Default layout: identity row, title, description, a page-kind chip with the\n * domain, the logo in a tinted tile on the right, and a brand bar along the\n * bottom edge over a faint grid.\n */\nfunction cardLayout(card: Card, brand: Brand): Node {\n const { palette } = brand;\n const kind = card.kind === \"home\" ? \"Home\" : \"Docs\";\n return box(\n {\n display: \"flex\",\n flexDirection: \"column\",\n position: \"relative\",\n overflow: \"hidden\",\n width: WIDTH,\n height: HEIGHT,\n backgroundColor: hsl(palette.background),\n color: hsl(palette.foreground),\n fontFamily: brand.fontFamily,\n },\n [\n box({\n position: \"absolute\",\n top: 0,\n left: 0,\n right: 0,\n bottom: 0,\n backgroundImage: `linear-gradient(${hsl(palette.border)} 1px, transparent 1px), linear-gradient(90deg, ${hsl(palette.border)} 1px, transparent 1px)`,\n backgroundSize: \"40px 40px\",\n opacity: 0.45,\n }),\n box({\n position: \"absolute\",\n top: -220,\n right: -160,\n width: 720,\n height: 720,\n borderRadius: 9999,\n backgroundImage: `radial-gradient(circle, ${hsl(palette.primary, 0.2)} 0%, ${hsl(palette.primary, 0)} 65%)`,\n }),\n box(\n {\n display: \"flex\",\n flexDirection: \"column\",\n justifyContent: \"space-between\",\n flexGrow: 1,\n position: \"relative\",\n padding: \"72px 80px 60px 80px\",\n },\n [\n identityRow(card, brand, 28),\n // Leave a 60 px gap before the logo tile, which starts at x = 900.\n textBlock(card, brand, brand.logo ? 760 : 1040),\n box(\n {\n display: \"flex\",\n alignItems: \"center\",\n gap: 20,\n fontSize: 24,\n color: hsl(palette.muted),\n },\n [\n box(\n {\n display: \"flex\",\n alignItems: \"center\",\n gap: 10,\n padding: \"8px 18px\",\n borderRadius: 9999,\n backgroundColor: hsl(palette.primary, 0.1),\n color: hsl(palette.primary),\n fontWeight: 600,\n },\n [\n box({\n width: 10,\n height: 10,\n borderRadius: 9999,\n backgroundColor: hsl(palette.primary),\n }),\n label(kind, {}),\n ]\n ),\n ...(brand.domain ? [label(brand.domain, {})] : []),\n ]\n ),\n ]\n ),\n ...(brand.logo\n ? [\n box(\n {\n position: \"absolute\",\n right: 80,\n top: 200,\n width: 220,\n height: 220,\n borderRadius: 48,\n backgroundColor: hsl(palette.primary, 0.08),\n display: \"flex\",\n alignItems: \"center\",\n justifyContent: \"center\",\n },\n logoImage(brand, 140)\n ),\n ]\n : []),\n box({ height: 12, flexShrink: 0, backgroundColor: hsl(palette.primary) }),\n ]\n );\n}\n\n/** Plain layout: identity row, title, description, domain. Safe for square crops. */\nfunction minimalLayout(card: Card, brand: Brand): Node {\n const { palette } = brand;\n return box(\n {\n display: \"flex\",\n flexDirection: \"column\",\n justifyContent: \"space-between\",\n width: WIDTH,\n height: HEIGHT,\n padding: 80,\n backgroundColor: hsl(palette.background),\n color: hsl(palette.foreground),\n fontFamily: brand.fontFamily,\n },\n [\n box({ display: \"flex\", alignItems: \"center\", gap: 18, fontSize: 28 }, [\n ...logoImage(brand, 56, 14),\n label(brand.siteName, { fontWeight: 600 }),\n ]),\n textBlock(card, brand, 980),\n label(brand.domain ?? \"\", { fontSize: 24, color: hsl(palette.muted) }),\n ]\n );\n}\n\nexport function buildTree(layout: Layout, card: Card, brand: Brand): Node {\n return layout === \"minimal\" ? minimalLayout(card, brand) : cardLayout(card, brand);\n}\n","/**\n * The only module that talks to Takumi. `render()` never fetches anything:\n * the logo and fonts arrive as bytes, and Takumi's `prepare()`, which does\n * fetch, is never called.\n */\n\nimport type { Node } from \"@takumi-rs/core\";\nimport { HEIGHT, WIDTH } from \"./layouts\";\nimport { LOG_PREFIX, type FontOption } from \"./options\";\n\n/** Family name Takumi gives its built-in font. */\nexport const BUILTIN_FAMILY = \"Geist\";\n\nexport interface LoadedFont extends FontOption {\n data: Uint8Array;\n}\n\nexport interface Engine {\n /** CSS font-family stack: configured fonts first, then Geist. */\n fontFamily: string;\n /** Render a tree to a 1200×630 PNG. */\n render(tree: Node): Promise<Uint8Array>;\n /** The characters of `chars` that no loaded font can draw. */\n missingGlyphs(chars: Iterable<string>): Promise<string[]>;\n}\n\ntype TakumiModule = typeof import(\"@takumi-rs/core\");\n\nasync function loadTakumi(): Promise<TakumiModule> {\n try {\n return await import(\"@takumi-rs/core\");\n } catch (err) {\n throw new Error(\n `${LOG_PREFIX} could not load the Takumi renderer (@takumi-rs/core). ` +\n `Its native binary is installed per platform: run the install again on the machine that builds the site.\\n` +\n `Cause: ${err instanceof Error ? err.message : String(err)}`,\n { cause: err }\n );\n }\n}\n\n/** Width and height from a PNG's IHDR chunk, or `undefined` for anything else. */\nexport function pngSize(bytes: Uint8Array): { width: number; height: number } | undefined {\n const signature = [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a];\n if (bytes.length < 24 || signature.some((b, i) => bytes[i] !== b)) return undefined;\n const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);\n return { width: view.getUint32(16), height: view.getUint32(20) };\n}\n\n/** A code point no font maps, so Takumi draws its \"missing glyph\" box. */\nconst NOTDEF_PROBE = \"\\u{10FFFD}\";\n\nexport async function createEngine(fonts: LoadedFont[]): Promise<Engine> {\n const { Renderer } = await loadTakumi();\n const renderer = new Renderer();\n\n const families: string[] = [];\n for (const font of fonts) {\n const registered = await renderer.registerFont({\n data: font.data,\n ...(font.weight !== undefined && { weight: font.weight }),\n ...(font.style !== undefined && { style: font.style }),\n });\n for (const family of registered) {\n if (!families.includes(family.name)) families.push(family.name);\n }\n }\n const fontFamily = [...families, BUILTIN_FAMILY].map((name) => JSON.stringify(name)).join(\", \");\n\n // A glyph is missing when it renders exactly like the probe's box.\n const glyphCache = new Map<string, boolean>();\n let notdef: Buffer | undefined;\n const drawGlyph = (char: string) =>\n renderer.render(\n {\n type: \"container\",\n style: { display: \"flex\", width: 48, height: 48, backgroundColor: \"#fff\", fontFamily },\n children: [{ type: \"text\", text: char, style: { fontSize: 32, color: \"#000\" } }],\n },\n { width: 48, height: 48, format: \"raw\" }\n );\n\n return {\n fontFamily,\n async render(tree) {\n const png = await renderer.render(tree, { width: WIDTH, height: HEIGHT, format: \"png\" });\n return new Uint8Array(png.buffer, png.byteOffset, png.byteLength);\n },\n async missingGlyphs(chars) {\n const missing: string[] = [];\n for (const char of chars) {\n // ASCII is covered by every font worth loading; whitespace draws nothing.\n if (char.codePointAt(0)! < 0x7f || /\\s/u.test(char)) continue;\n let isMissing = glyphCache.get(char);\n if (isMissing === undefined) {\n notdef ??= await drawGlyph(NOTDEF_PROBE);\n isMissing = notdef.equals(await drawGlyph(char));\n glyphCache.set(char, isMissing);\n }\n if (isMissing) missing.push(char);\n }\n return missing;\n },\n };\n}\n","/**\n * Writing the cards in `onEnd`. The output directory is the cache: a manifest\n * records the render hash of every file on disk, so a build renders only the\n * cards whose inputs changed and writes nothing when none did.\n */\n\nimport { mkdir, readFile, rename, rmdir, stat, unlink, writeFile } from \"node:fs/promises\";\nimport { dirname, join } from \"node:path\";\nimport type { PlannedCard } from \"./cards\";\nimport { pngSize } from \"./renderer\";\nimport { HEIGHT, WIDTH } from \"./layouts\";\nimport { isInside } from \"./site\";\n\nexport const MANIFEST_VERSION = 1;\n\n/** WhatsApp drops previews above roughly this size. */\nexport const SIZE_WARNING_BYTES = 300 * 1024;\n\n/** In-flight renders. A third one bought nothing on a 4-vCPU machine. */\nexport const RENDER_CONCURRENCY = 2;\n\nexport interface Manifest {\n version: number;\n /** Card file below `assets/og/` → render hash of the bytes on disk. */\n files: Record<string, string>;\n}\n\nexport interface PublishJob extends PlannedCard {\n /** Name hash plus everything else that changes the pixels. */\n renderHash: string;\n}\n\nexport interface PublishResult {\n rendered: string[];\n unchanged: string[];\n removed: string[];\n oversized: Array<{ file: string; bytes: number }>;\n}\n\nexport async function readManifest(path: string): Promise<Manifest> {\n try {\n const parsed = JSON.parse(await readFile(path, \"utf8\")) as Manifest;\n if (parsed?.version === MANIFEST_VERSION && typeof parsed.files === \"object\" && parsed.files) {\n return parsed;\n }\n } catch {\n // missing or unreadable: start over\n }\n return { version: MANIFEST_VERSION, files: {} };\n}\n\n/** Write through a temp file so an interrupted build never leaves half a file. */\nexport async function writeAtomic(path: string, data: string | Uint8Array): Promise<void> {\n await mkdir(dirname(path), { recursive: true });\n const temp = `${path}.${process.pid}.tmp`;\n try {\n await writeFile(temp, data);\n await rename(temp, path);\n } catch (err) {\n await unlink(temp).catch(() => {});\n throw err;\n }\n}\n\nasync function exists(path: string): Promise<boolean> {\n try {\n return (await stat(path)).isFile();\n } catch {\n return false;\n }\n}\n\nasync function mapLimit<T>(items: T[], limit: number, fn: (item: T) => Promise<void>) {\n let next = 0;\n const worker = async () => {\n while (next < items.length) await fn(items[next++]);\n };\n await Promise.all(Array.from({ length: Math.min(limit, items.length) }, worker));\n}\n\n/** Remove now-empty parent directories of a removed card, up to `stop`. */\nasync function removeEmptyParents(file: string, stop: string): Promise<void> {\n for (let dir = dirname(file); dir.startsWith(stop) && dir !== stop; dir = dirname(dir)) {\n try {\n await rmdir(dir);\n } catch {\n return;\n }\n }\n}\n\n/**\n * Bring `cardDir` in line with `jobs`: render cards that are missing or out\n * of date, delete files an earlier build wrote and this one no longer needs,\n * and save the manifest. Only files listed in the previous manifest are ever\n * deleted, so anything else in the directory is left alone.\n */\nexport async function publishCards(\n jobs: PublishJob[],\n cardDir: string,\n manifestPath: string,\n render: (job: PublishJob) => Promise<Uint8Array>\n): Promise<PublishResult> {\n const previous = await readManifest(manifestPath);\n const next: Manifest = { version: MANIFEST_VERSION, files: {} };\n const result: PublishResult = { rendered: [], unchanged: [], removed: [], oversized: [] };\n\n const pending: PublishJob[] = [];\n for (const job of jobs) {\n next.files[job.file] = job.renderHash;\n if (previous.files[job.file] === job.renderHash && (await exists(join(cardDir, job.file)))) {\n result.unchanged.push(job.file);\n } else {\n pending.push(job);\n }\n }\n\n await mapLimit(pending, RENDER_CONCURRENCY, async (job) => {\n const png = await render(job);\n const size = pngSize(png);\n if (!size || size.width !== WIDTH || size.height !== HEIGHT) {\n throw new Error(\n `card for \"${job.file}\" is ${size ? `${size.width}×${size.height}` : \"not a PNG\"}, expected ${WIDTH}×${HEIGHT}`\n );\n }\n if (png.byteLength > SIZE_WARNING_BYTES) {\n result.oversized.push({ file: job.file, bytes: png.byteLength });\n }\n await writeAtomic(join(cardDir, job.file), png);\n result.rendered.push(job.file);\n });\n\n for (const file of Object.keys(previous.files)) {\n if (file in next.files) continue;\n const path = join(cardDir, file);\n // The manifest is a plain file in `.docu/`; never follow it out of `cardDir`.\n if (!isInside(cardDir, path) || path === cardDir) continue;\n try {\n await unlink(path);\n result.removed.push(file);\n await removeEmptyParents(path, cardDir);\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code !== \"ENOENT\") throw err;\n }\n }\n\n await writeAtomic(manifestPath, `${JSON.stringify(next, null, 2)}\\n`);\n return result;\n}\n","/**\n * @docubook/plugin-og-image\n *\n * Gives every page a 1200×630 social preview card, rendered at build time\n * from the page's frontmatter with Takumi. Authors write nothing extra:\n *\n * - `transformFrontmatter` points `frontmatter.image` at the page's card, and\n * flame's own SEO code turns it into an absolute `og:image`;\n * - `injectHead` adds the card's size, type and alt text, and an `og:image`\n * on the landing page when `meta.ogImage` does not set one;\n * - `onEnd` renders the cards into `<docs>/assets/og/`, skipping the ones\n * whose inputs did not change;\n * - `handleRequest` renders a card on demand during `flame dev`.\n *\n * An `image` in frontmatter always wins, and `og: false` opts a page out.\n */\n\nimport { readFile, stat } from \"node:fs/promises\";\nimport { dirname, join, resolve } from \"node:path\";\nimport type { DocuBookPlugin, DocuConfig, PageContext } from \"@docubook/flame\";\nimport { presetRegistry, resolveTheme, type ThemeConfig } from \"@docubook/themes-colors\";\nimport {\n docsCard,\n homeCard,\n imageRef,\n planCard,\n readFrontmatter,\n readFrontmatterSync,\n sha256,\n skipReason,\n type Card,\n type PlannedCard,\n} from \"./cards\";\nimport { buildTree, type Brand, type Palette } from \"./layouts\";\nimport { LOG_PREFIX, resolveOptions, type OgImageOptions, type ResolvedOptions } from \"./options\";\nimport { publishCards, type PublishJob } from \"./publish\";\nimport { createEngine, type Engine, type LoadedFont } from \"./renderer\";\nimport {\n CARD_DIR,\n cardUrl,\n docsOutputDir,\n findLogoFile,\n findProjectRoot,\n isInside,\n resolveDeployPath,\n resolveSite,\n type Site,\n} from \"./site\";\n\nexport type { FontOption, Layout, OgImageOptions, PagesOption } from \"./options\";\n\ndeclare const __PLUGIN_VERSION__: string;\n\n/** Logos above this size are skipped: a card shows them at 140 px. */\nexport const MAX_LOGO_BYTES = 1024 * 1024;\n\n/** Cards kept in memory by the dev server. */\nconst DEV_CACHE_SIZE = 50;\n\nexport interface Logger {\n info(message: string): void;\n warn(message: string): void;\n}\n\nexport interface PluginInternals {\n /** @internal Project root instead of the `docu.json` lookup from `cwd` (tests). */\n root?: string;\n /** @internal Log sink (tests). */\n log?: Logger;\n /** @internal Renderer factory (tests). */\n createEngine?: (fonts: LoadedFont[]) => Promise<Engine>;\n /** @internal Plugin version that goes into card names (tests). */\n version?: string;\n}\n\ninterface Runtime {\n engine: Engine;\n brand: Brand;\n /** Hash of the pixels' inputs that are not in the card name. */\n fingerprint: string;\n}\n\nconst consoleLogger: Logger = {\n info: (message) => console.log(`${LOG_PREFIX} ${message}`),\n warn: (message) => console.warn(`${LOG_PREFIX} ${message}`),\n};\n\nfunction escapeAttr(value: string): string {\n return value\n .replace(/&/g, \"&\")\n .replace(/\"/g, \""\")\n .replace(/</g, \"<\")\n .replace(/>/g, \">\");\n}\n\nfunction sizeTags(card: Card): string[] {\n return [\n `<meta property=\"og:image:width\" content=\"1200\">`,\n `<meta property=\"og:image:height\" content=\"630\">`,\n `<meta property=\"og:image:type\" content=\"image/png\">`,\n `<meta property=\"og:image:alt\" content=\"${escapeAttr(card.title)}\">`,\n ];\n}\n\n/** Light-mode colours of the active theme, as flame resolves it. */\nexport function resolvePalette(config: DocuConfig): Palette {\n const themeConfig = (process.env.FLAME_THEME || config.themes?.colors) as ThemeConfig | undefined;\n const vars = resolveTheme(themeConfig, presetRegistry).variables.root;\n return {\n background: vars.background ?? \"0 0% 100%\",\n foreground: vars.foreground ?? \"220 30% 15%\",\n primary: vars.primary ?? \"210 81% 56%\",\n muted: vars[\"muted-foreground\"] ?? \"220 15% 38%\",\n border: vars[\"border-color\"] ?? \"210 20% 88%\",\n };\n}\n\nasync function loadLogo(\n config: DocuConfig,\n site: Site,\n options: ResolvedOptions,\n log: Logger\n): Promise<Uint8Array | undefined> {\n if (options.logo === false) return undefined;\n const value = options.logo ?? config.navbar?.logo?.src;\n if (!value) return undefined;\n const source = options.logo ? `\"logo\" option` : `navbar.logo.src`;\n const found = findLogoFile(value, site, resolveDeployPath(config));\n if (\"reason\" in found) {\n const why = {\n remote: \"is a URL, and the build never downloads files\",\n outside: \"points outside the project\",\n missing: \"was not found in the project\",\n type: \"is not an SVG, PNG, JPEG or WebP file\",\n }[found.reason];\n log.warn(\n `${source} \"${value}\" ${why}. Cards are rendered without a logo; set the \"logo\" option to a local file.`\n );\n return undefined;\n }\n const { size } = await stat(found.file);\n if (size > MAX_LOGO_BYTES) {\n log.warn(\n `${source} \"${value}\" is ${Math.round(size / 1024)} KB, above the 1 MB limit. Cards are rendered without a logo.`\n );\n return undefined;\n }\n return new Uint8Array(await readFile(found.file));\n}\n\nasync function loadRuntime(\n config: DocuConfig,\n site: Site,\n options: ResolvedOptions,\n log: Logger,\n makeEngine: (fonts: LoadedFont[]) => Promise<Engine>\n): Promise<Runtime> {\n const fonts: LoadedFont[] = [];\n for (const font of options.fonts) {\n const path = resolve(site.root, font.path);\n let data: Uint8Array;\n try {\n data = new Uint8Array(await readFile(path));\n } catch (err) {\n throw new Error(\n `${LOG_PREFIX} font \"${font.path}\" could not be read: ${(err as Error).message}`,\n {\n cause: err,\n }\n );\n }\n fonts.push({ ...font, data });\n }\n\n const logo = await loadLogo(config, site, options, log);\n const engine = await makeEngine(fonts);\n const palette = resolvePalette(config);\n return {\n engine,\n brand: {\n siteName: site.siteName,\n domain: site.domain,\n logo,\n palette,\n fontFamily: engine.fontFamily,\n },\n fingerprint: sha256(\n JSON.stringify(palette),\n logo ? sha256(logo) : \"no-logo\",\n ...fonts.map((font) => sha256(font.data))\n ),\n };\n}\n\nfunction describe(list: string[], limit = 5): string {\n const shown = list.slice(0, limit).join(\", \");\n return list.length > limit ? `${shown} (+${list.length - limit} more)` : shown;\n}\n\nexport default function pluginOgImage(\n options?: OgImageOptions,\n internals: PluginInternals = {}\n): DocuBookPlugin {\n const resolved = resolveOptions(options);\n const version =\n internals.version ?? (typeof __PLUGIN_VERSION__ === \"string\" ? __PLUGIN_VERSION__ : \"dev\");\n const log = internals.log ?? consoleLogger;\n\n return {\n name: \"og-image\",\n setup(build) {\n const config = build.config;\n const root = internals.root ?? findProjectRoot();\n for (const [label, path] of [\n ...(typeof resolved.logo === \"string\" && !resolved.logo.startsWith(\"/\")\n ? [[\"logo\", resolved.logo]]\n : []),\n ...resolved.fonts.map((font, i) => [`fonts[${i}].path`, font.path]),\n ]) {\n if (!isInside(root, resolve(root, path))) {\n throw new Error(`${LOG_PREFIX} \"${label}\" must point inside the project, not \"${path}\".`);\n }\n }\n\n const site = resolveSite(config, resolved, root);\n const plan = (card: Card): PlannedCard => planCard(card, site, resolved, version);\n const wantsHome = () => site.hasLanding && resolved.pages.home && !config.meta?.ogImage;\n\n let runtime: Promise<Runtime> | undefined;\n const ready = () =>\n (runtime ??= loadRuntime(\n config,\n site,\n resolved,\n log,\n internals.createEngine ?? createEngine\n ));\n\n build.onStart(async () => {\n if (!site.baseURL) {\n log.warn(\n `meta.baseURL is not set, so og:image stays a relative URL that social sites ignore.`\n );\n }\n await ready();\n });\n\n // Card each docs page links to, by slug, for `injectHead` on the same render.\n const linked = new Map<string, PlannedCard>();\n\n build.transformFrontmatter((frontmatter, ctx) => {\n linked.delete(ctx.slug);\n const kind = ctx.slug === \"\" ? \"index\" : \"docs\";\n if (kind === \"index\" && !site.hasDocsIndex) return;\n // An image set by the author or an earlier plugin wins.\n if (skipReason(kind, frontmatter, resolved)) return;\n // Plan from the source file, as `onEnd` does: other plugins may have\n // changed the title by now (in `onLoad` or their own\n // `transformFrontmatter`), and the name must match the file written.\n let source: Record<string, unknown> | undefined;\n try {\n source = readFrontmatterSync(site.docsDir, ctx.slug);\n } catch {\n return;\n }\n if (!source || skipReason(kind, source, resolved)) return;\n const planned = plan(docsCard(kind, ctx.slug, source, site));\n linked.set(ctx.slug, planned);\n return { ...frontmatter, image: imageRef(planned.file) };\n });\n\n build.injectHead((ctx: PageContext) => {\n if (ctx.pageType === \"docs\") {\n const planned = linked.get(ctx.slug);\n // Tags describe the card only while the page still shows it: a\n // later plugin may have replaced the image.\n return planned && ctx.frontmatter.image === imageRef(planned.file)\n ? sizeTags(planned.card)\n : [];\n }\n if (ctx.pageType === \"home\" && wantsHome()) {\n const planned = plan(homeCard(config));\n return [\n `<meta property=\"og:image\" content=\"${escapeAttr(cardUrl(site, planned.file))}\">`,\n ...sizeTags(planned.card),\n ];\n }\n return [];\n });\n\n build.onEnd(async (_config, pages, { outDir }) => {\n const started = performance.now();\n const rt = await ready();\n const cards: PlannedCard[] = [];\n let skipped = 0;\n\n const addDocs = async (kind: \"docs\" | \"index\", slug: string) => {\n let frontmatter: Record<string, unknown> | undefined;\n try {\n frontmatter = await readFrontmatter(site.docsDir, slug);\n } catch (err) {\n log.warn(\n `skipped \"${slug || \"index\"}\": its frontmatter could not be read (${(err as Error).message}).`\n );\n return;\n }\n if (!frontmatter) return;\n if (skipReason(kind, frontmatter, resolved)) {\n skipped++;\n return;\n }\n cards.push(plan(docsCard(kind, slug, frontmatter, site)));\n };\n\n if (resolved.pages.docs) {\n for (const page of pages) await addDocs(\"docs\", page.slug);\n }\n if (site.hasDocsIndex && resolved.pages.index) await addDocs(\"index\", \"\");\n if (wantsHome()) cards.push(plan(homeCard(config)));\n\n const missing = await rt.engine.missingGlyphs(\n new Set(\n cards\n .flatMap(({ card }) => [...`${card.title}${card.description}${card.section ?? \"\"}`])\n .concat([...site.siteName])\n )\n );\n if (missing.length > 0) {\n const affected = cards\n .filter(({ card }) =>\n missing.some((char) =>\n `${card.title}${card.description}${card.section ?? \"\"}`.includes(char)\n )\n )\n .map(({ card }) => (card.kind === \"docs\" ? card.slug : card.kind));\n log.warn(\n `no loaded font can draw ${describe(\n missing.map((char) => `\"${char}\"`),\n 10\n )}, so cards show boxes in their place. ` +\n `Add a font that covers them with the \"fonts\" option.` +\n (affected.length > 0 ? ` Pages: ${describe(affected)}.` : \"\")\n );\n }\n\n const jobs: PublishJob[] = cards.map((planned) => ({\n ...planned,\n renderHash: sha256(planned.nameHash, rt.fingerprint),\n }));\n const cardDir = join(docsOutputDir(outDir, site.prefix), CARD_DIR);\n let result;\n try {\n result = await publishCards(\n jobs,\n cardDir,\n join(dirname(outDir), \"og-image.json\"),\n (job) => rt.engine.render(buildTree(resolved.layout, job.card, rt.brand))\n );\n } catch (err) {\n throw new Error(`${LOG_PREFIX} ${(err as Error).message}`, { cause: err });\n }\n\n for (const { file, bytes } of result.oversized) {\n log.warn(\n `${file} is ${Math.round(bytes / 1024)} KB; some apps (WhatsApp) skip previews above 300 KB.`\n );\n }\n const parts = [\n `${result.rendered.length} rendered`,\n `${result.unchanged.length} unchanged`,\n ];\n if (result.removed.length > 0) parts.push(`${result.removed.length} removed`);\n if (skipped > 0) parts.push(`${skipped} pages use their own image`);\n log.info(\n `${jobs.length} cards: ${parts.join(\", \")} (${Math.round(performance.now() - started)}ms)`\n );\n });\n\n // ─── flame dev ────────────────────────────────────────\n const devCache = new Map<string, Uint8Array>();\n const base = `${site.prefix}/${CARD_DIR}/`;\n\n build.handleRequest(async (req) => {\n const { pathname } = new URL(req.url);\n if (!pathname.startsWith(base)) return;\n const notFound = () => new Response(\"Not found\", { status: 404 });\n\n let relative: string;\n try {\n relative = decodeURIComponent(pathname.slice(base.length));\n } catch {\n return notFound();\n }\n const match = /^(.+)\\.([0-9a-f]{8})\\.png$/.exec(relative);\n if (!match) return notFound();\n const [, stem, hash] = match;\n if (stem.split(\"/\").some((s) => !/^[\\w@-][\\w.@-]*$/.test(s) || s.includes(\"..\"))) {\n return notFound();\n }\n\n try {\n // `home` and `index` may also be docs slugs; the hash tells them apart.\n const candidates: PlannedCard[] = [];\n if (stem === \"home\" && wantsHome()) candidates.push(plan(homeCard(config)));\n if (stem === \"index\" && site.hasDocsIndex && resolved.pages.index) {\n const frontmatter = await readFrontmatter(site.docsDir, \"\");\n if (frontmatter) candidates.push(plan(docsCard(\"index\", \"\", frontmatter, site)));\n }\n if (resolved.pages.docs) {\n const frontmatter = await readFrontmatter(site.docsDir, stem);\n if (frontmatter) candidates.push(plan(docsCard(\"docs\", stem, frontmatter, site)));\n }\n // A stale hash still shows the page's current card.\n const chosen = candidates.find((c) => c.nameHash.startsWith(hash)) ?? candidates[0];\n if (!chosen) return notFound();\n\n const rt = await ready();\n const key = sha256(chosen.nameHash, rt.fingerprint);\n let png = devCache.get(key);\n if (!png) {\n png = await rt.engine.render(buildTree(resolved.layout, chosen.card, rt.brand));\n devCache.set(key, png);\n if (devCache.size > DEV_CACHE_SIZE) devCache.delete(devCache.keys().next().value!);\n }\n return new Response(png as Uint8Array<ArrayBuffer>, {\n headers: { \"Content-Type\": \"image/png\", \"Cache-Control\": \"no-cache\" },\n });\n } catch (err) {\n return new Response(`${LOG_PREFIX} ${(err as Error).message}`, { status: 500 });\n }\n });\n },\n };\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The two card layouts as Takumi node trees. Page text only ever lands in
|
|
3
|
+
* `text` nodes, so frontmatter is drawn as text and never parsed as markup.
|
|
4
|
+
*/
|
|
5
|
+
import type { Node } from "@takumi-rs/core";
|
|
6
|
+
import type { Card } from "./cards";
|
|
7
|
+
import type { Layout } from "./options";
|
|
8
|
+
export declare const WIDTH = 1200;
|
|
9
|
+
export declare const HEIGHT = 630;
|
|
10
|
+
/** Theme colours as flame's presets store them: HSL triplets such as `210 81% 56%`. */
|
|
11
|
+
export interface Palette {
|
|
12
|
+
background: string;
|
|
13
|
+
foreground: string;
|
|
14
|
+
primary: string;
|
|
15
|
+
muted: string;
|
|
16
|
+
border: string;
|
|
17
|
+
}
|
|
18
|
+
export interface Brand {
|
|
19
|
+
siteName: string;
|
|
20
|
+
domain: string | undefined;
|
|
21
|
+
/** Logo bytes (SVG, PNG, JPEG or WebP), or none. */
|
|
22
|
+
logo: Uint8Array | undefined;
|
|
23
|
+
palette: Palette;
|
|
24
|
+
/** CSS font-family stack: configured fonts, then Geist. */
|
|
25
|
+
fontFamily: string;
|
|
26
|
+
}
|
|
27
|
+
export declare function buildTree(layout: Layout, card: Card, brand: Brand): Node;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plugin options from `docu.json` and their validation. Pure: no file system
|
|
3
|
+
* and no flame imports, so a typo stops `flame build` and `flame dev` before
|
|
4
|
+
* anything else runs.
|
|
5
|
+
*/
|
|
6
|
+
export declare const LOG_PREFIX = "[plugin:og-image]";
|
|
7
|
+
export type Layout = "card" | "minimal";
|
|
8
|
+
export declare const LAYOUTS: readonly Layout[];
|
|
9
|
+
export interface FontOption {
|
|
10
|
+
/** Font file (TTF, OTF, WOFF or WOFF2), relative to the project root. */
|
|
11
|
+
path: string;
|
|
12
|
+
/** Weight the file provides. Default: read from the file. */
|
|
13
|
+
weight?: number;
|
|
14
|
+
/** Style the file provides. Default: read from the file. */
|
|
15
|
+
style?: "normal" | "italic";
|
|
16
|
+
}
|
|
17
|
+
export interface PagesOption {
|
|
18
|
+
/** Docs pages. Default `true`. */
|
|
19
|
+
docs?: boolean;
|
|
20
|
+
/** The docs index (`docs/index.mdx`). Default `true`. */
|
|
21
|
+
index?: boolean;
|
|
22
|
+
/** The landing page, when `meta.ogImage` does not already set its image. Default `true`. */
|
|
23
|
+
home?: boolean;
|
|
24
|
+
}
|
|
25
|
+
export interface OgImageOptions {
|
|
26
|
+
/** Card layout. Default `card`. */
|
|
27
|
+
layout?: Layout;
|
|
28
|
+
/**
|
|
29
|
+
* Logo file: a path relative to the project root, or a site path such as
|
|
30
|
+
* `/docs/assets/images/logo.svg`. SVG, PNG, JPEG or WebP, at most 1 MB.
|
|
31
|
+
* `false` renders no logo. Default: `navbar.logo.src`.
|
|
32
|
+
*/
|
|
33
|
+
logo?: string | false;
|
|
34
|
+
/** Name in the card's top row. Default: `navbar.logoText`, then `meta.title`. */
|
|
35
|
+
siteName?: string;
|
|
36
|
+
/** Which pages get a card. */
|
|
37
|
+
pages?: PagesOption;
|
|
38
|
+
/**
|
|
39
|
+
* Fonts tried before the built-in Geist, which covers Latin-1 only. Add one
|
|
40
|
+
* for titles in other scripts.
|
|
41
|
+
*/
|
|
42
|
+
fonts?: FontOption[];
|
|
43
|
+
}
|
|
44
|
+
export interface ResolvedOptions {
|
|
45
|
+
layout: Layout;
|
|
46
|
+
/** `undefined` means "use `navbar.logo.src`". */
|
|
47
|
+
logo: string | false | undefined;
|
|
48
|
+
siteName: string | undefined;
|
|
49
|
+
pages: Required<PagesOption>;
|
|
50
|
+
fonts: FontOption[];
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* A value that names something on another host or inline data rather than a
|
|
54
|
+
* local file: `https://…`, `//cdn…`, `data:…`. Single-letter schemes are left
|
|
55
|
+
* alone so a Windows drive path (`C:\…`) is not mistaken for a URL.
|
|
56
|
+
*/
|
|
57
|
+
export declare function isRemoteReference(value: string): boolean;
|
|
58
|
+
export declare function resolveOptions(input: unknown): ResolvedOptions;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Writing the cards in `onEnd`. The output directory is the cache: a manifest
|
|
3
|
+
* records the render hash of every file on disk, so a build renders only the
|
|
4
|
+
* cards whose inputs changed and writes nothing when none did.
|
|
5
|
+
*/
|
|
6
|
+
import type { PlannedCard } from "./cards";
|
|
7
|
+
export declare const MANIFEST_VERSION = 1;
|
|
8
|
+
/** WhatsApp drops previews above roughly this size. */
|
|
9
|
+
export declare const SIZE_WARNING_BYTES: number;
|
|
10
|
+
/** In-flight renders. A third one bought nothing on a 4-vCPU machine. */
|
|
11
|
+
export declare const RENDER_CONCURRENCY = 2;
|
|
12
|
+
export interface Manifest {
|
|
13
|
+
version: number;
|
|
14
|
+
/** Card file below `assets/og/` → render hash of the bytes on disk. */
|
|
15
|
+
files: Record<string, string>;
|
|
16
|
+
}
|
|
17
|
+
export interface PublishJob extends PlannedCard {
|
|
18
|
+
/** Name hash plus everything else that changes the pixels. */
|
|
19
|
+
renderHash: string;
|
|
20
|
+
}
|
|
21
|
+
export interface PublishResult {
|
|
22
|
+
rendered: string[];
|
|
23
|
+
unchanged: string[];
|
|
24
|
+
removed: string[];
|
|
25
|
+
oversized: Array<{
|
|
26
|
+
file: string;
|
|
27
|
+
bytes: number;
|
|
28
|
+
}>;
|
|
29
|
+
}
|
|
30
|
+
export declare function readManifest(path: string): Promise<Manifest>;
|
|
31
|
+
/** Write through a temp file so an interrupted build never leaves half a file. */
|
|
32
|
+
export declare function writeAtomic(path: string, data: string | Uint8Array): Promise<void>;
|
|
33
|
+
/**
|
|
34
|
+
* Bring `cardDir` in line with `jobs`: render cards that are missing or out
|
|
35
|
+
* of date, delete files an earlier build wrote and this one no longer needs,
|
|
36
|
+
* and save the manifest. Only files listed in the previous manifest are ever
|
|
37
|
+
* deleted, so anything else in the directory is left alone.
|
|
38
|
+
*/
|
|
39
|
+
export declare function publishCards(jobs: PublishJob[], cardDir: string, manifestPath: string, render: (job: PublishJob) => Promise<Uint8Array>): Promise<PublishResult>;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The only module that talks to Takumi. `render()` never fetches anything:
|
|
3
|
+
* the logo and fonts arrive as bytes, and Takumi's `prepare()`, which does
|
|
4
|
+
* fetch, is never called.
|
|
5
|
+
*/
|
|
6
|
+
import type { Node } from "@takumi-rs/core";
|
|
7
|
+
import { type FontOption } from "./options";
|
|
8
|
+
/** Family name Takumi gives its built-in font. */
|
|
9
|
+
export declare const BUILTIN_FAMILY = "Geist";
|
|
10
|
+
export interface LoadedFont extends FontOption {
|
|
11
|
+
data: Uint8Array;
|
|
12
|
+
}
|
|
13
|
+
export interface Engine {
|
|
14
|
+
/** CSS font-family stack: configured fonts first, then Geist. */
|
|
15
|
+
fontFamily: string;
|
|
16
|
+
/** Render a tree to a 1200×630 PNG. */
|
|
17
|
+
render(tree: Node): Promise<Uint8Array>;
|
|
18
|
+
/** The characters of `chars` that no loaded font can draw. */
|
|
19
|
+
missingGlyphs(chars: Iterable<string>): Promise<string[]>;
|
|
20
|
+
}
|
|
21
|
+
/** Width and height from a PNG's IHDR chunk, or `undefined` for anything else. */
|
|
22
|
+
export declare function pngSize(bytes: Uint8Array): {
|
|
23
|
+
width: number;
|
|
24
|
+
height: number;
|
|
25
|
+
} | undefined;
|
|
26
|
+
export declare function createEngine(fonts: LoadedFont[]): Promise<Engine>;
|
package/dist/site.d.ts
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the plugin needs to know about the site, derived from `docu.json` the
|
|
3
|
+
* same way flame derives it. `@docubook/flame` has no runtime exports, so the
|
|
4
|
+
* base-path rules are mirrored here; `__tests__/site.test.ts` pins them to
|
|
5
|
+
* flame's own cases.
|
|
6
|
+
*/
|
|
7
|
+
import type { DocuConfig } from "@docubook/flame";
|
|
8
|
+
import type { ResolvedOptions } from "./options";
|
|
9
|
+
export declare const DEFAULT_BASE_PATH = "/docs";
|
|
10
|
+
/** Output directory of the cards, below the docs output directory. */
|
|
11
|
+
export declare const CARD_DIR = "assets/og";
|
|
12
|
+
export interface Site {
|
|
13
|
+
/** Project root: the directory that holds `docu.json`. */
|
|
14
|
+
root: string;
|
|
15
|
+
/** `<root>/docs`. */
|
|
16
|
+
docsDir: string;
|
|
17
|
+
/** Docs URL prefix: `/docs` by default, `""` at the deployment root. */
|
|
18
|
+
prefix: string;
|
|
19
|
+
/** `meta.baseURL` without a trailing slash, `""` when unset. */
|
|
20
|
+
baseURL: string;
|
|
21
|
+
/** Host and deployment path shown on the card, e.g. `docubook.pro`. */
|
|
22
|
+
domain: string | undefined;
|
|
23
|
+
/** Name in the card's top row. */
|
|
24
|
+
siteName: string;
|
|
25
|
+
/** Flame renders a landing page (`experimental.pages` is off). */
|
|
26
|
+
hasLanding: boolean;
|
|
27
|
+
/** Flame writes the docs index page. */
|
|
28
|
+
hasDocsIndex: boolean;
|
|
29
|
+
/** Sidebar routes from `docu.json`, for the section label. */
|
|
30
|
+
routes: DocuConfig["routes"];
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Strip slashes from the end (and the start, unless `endOnly`) in one linear
|
|
34
|
+
* pass. A `/\/+$/` regex backtracks polynomially on long runs of `/`.
|
|
35
|
+
*/
|
|
36
|
+
export declare function trimSlashes(value: string, endOnly?: boolean): string;
|
|
37
|
+
/** Walk up from `start` to the directory holding `docu.json`, as flame does. */
|
|
38
|
+
export declare function findProjectRoot(start?: string): string;
|
|
39
|
+
/** Mirror of flame's `normalizeBasePath`. */
|
|
40
|
+
export declare function normalizeBasePath(value: string | undefined | null): string;
|
|
41
|
+
/** Mirror of flame's `canonicalBasePath`. */
|
|
42
|
+
export declare function canonicalBasePath(value: string): string;
|
|
43
|
+
/** Mirror of flame's `resolveBasePath`. */
|
|
44
|
+
export declare function resolveBasePath(config: DocuConfig): string;
|
|
45
|
+
/** Mirror of flame's `resolveDeployPath`: the path part of `meta.baseURL`. */
|
|
46
|
+
export declare function resolveDeployPath(config: DocuConfig): string;
|
|
47
|
+
export declare function resolveSite(config: DocuConfig, options: ResolvedOptions, root: string): Site;
|
|
48
|
+
/**
|
|
49
|
+
* Title of the top-level sidebar group a docs page sits in, e.g. "Getting
|
|
50
|
+
* Started" for `getting-started/configuration`. Top-level pages have none.
|
|
51
|
+
*/
|
|
52
|
+
export declare function sectionFor(routes: Site["routes"], slug: string): string | undefined;
|
|
53
|
+
/** Absolute directory flame writes the docs pages to. */
|
|
54
|
+
export declare function docsOutputDir(outDir: string, prefix: string): string;
|
|
55
|
+
/**
|
|
56
|
+
* Absolute URL of a card, as flame's SEO resolver builds it for a relative
|
|
57
|
+
* `frontmatter.image`. Root-relative when `meta.baseURL` is unset.
|
|
58
|
+
*/
|
|
59
|
+
export declare function cardUrl(site: Site, file: string): string;
|
|
60
|
+
/** True when `file` is inside `root` (or is `root`). */
|
|
61
|
+
export declare function isInside(root: string, file: string): boolean;
|
|
62
|
+
export declare const LOGO_TYPES: Record<string, string>;
|
|
63
|
+
export type LogoLookup = {
|
|
64
|
+
file: string;
|
|
65
|
+
} | {
|
|
66
|
+
reason: "remote" | "outside" | "missing" | "type";
|
|
67
|
+
value: string;
|
|
68
|
+
};
|
|
69
|
+
/**
|
|
70
|
+
* Map a logo reference to a file in the project.
|
|
71
|
+
*
|
|
72
|
+
* `navbar.logo.src` is a site path (`/docs/assets/images/logo.svg`), while the
|
|
73
|
+
* `logo` option may also be a project path (`brand/logo.svg`). Site paths
|
|
74
|
+
* under the docs prefix, or under the default `/docs` spelling flame re-bases,
|
|
75
|
+
* map to `docs/`; anything else is tried relative to the project root, then
|
|
76
|
+
* to `docs/`.
|
|
77
|
+
*/
|
|
78
|
+
export declare function findLogoFile(value: string, site: Site, deployPath: string): LogoLookup;
|
package/package.json
CHANGED
|
@@ -1,6 +1,64 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@docubook/plugin-og-image",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Social preview cards for DocuBook Flame: a 1200×630 OpenGraph image for every page, rendered at build time from its frontmatter with Takumi.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.js",
|
|
12
|
+
"default": "./dist/index.js"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist",
|
|
17
|
+
"README.md"
|
|
18
|
+
],
|
|
19
|
+
"keywords": [
|
|
20
|
+
"docubook",
|
|
21
|
+
"flame",
|
|
22
|
+
"plugin",
|
|
23
|
+
"opengraph",
|
|
24
|
+
"og-image",
|
|
25
|
+
"social-card",
|
|
26
|
+
"seo",
|
|
27
|
+
"takumi"
|
|
28
|
+
],
|
|
29
|
+
"homepage": "https://github.com/DocuBook/docubook/tree/main/plugins/og-image",
|
|
30
|
+
"repository": {
|
|
31
|
+
"type": "git",
|
|
32
|
+
"url": "git+https://github.com/DocuBook/docubook.git",
|
|
33
|
+
"directory": "plugins/og-image"
|
|
34
|
+
},
|
|
35
|
+
"publishConfig": {
|
|
36
|
+
"registry": "https://registry.npmjs.org/",
|
|
37
|
+
"access": "public"
|
|
38
|
+
},
|
|
39
|
+
"author": "wildan.nrs",
|
|
40
|
+
"author-url": "https://wildan.dev",
|
|
41
|
+
"license": "MIT",
|
|
42
|
+
"dependencies": {
|
|
43
|
+
"@11ty/gray-matter": "^3.0.0",
|
|
44
|
+
"@docubook/themes-colors": "^2.2.1",
|
|
45
|
+
"@takumi-rs/core": "~2.19.0"
|
|
46
|
+
},
|
|
47
|
+
"peerDependencies": {
|
|
48
|
+
"@docubook/flame": "^2.6.0"
|
|
49
|
+
},
|
|
50
|
+
"devDependencies": {
|
|
51
|
+
"@docubook/flame": "^2.6.0",
|
|
52
|
+
"@types/node": "^22.20.5",
|
|
53
|
+
"typescript": "^7.0.2",
|
|
54
|
+
"vite": "^8.3.5",
|
|
55
|
+
"vitest": "^5.0.0"
|
|
56
|
+
},
|
|
57
|
+
"scripts": {
|
|
58
|
+
"build": "pnpm run clean && vite build && tsc -p tsconfig.build.json",
|
|
59
|
+
"test": "vitest run",
|
|
60
|
+
"lint": "oxlint .",
|
|
61
|
+
"typecheck": "tsc --noEmit",
|
|
62
|
+
"clean": "rm -rf dist"
|
|
63
|
+
}
|
|
6
64
|
}
|