import { defineConfig } from "blume"; import type { StandardSchema } from "blume/core/standard-schema.ts"; import { hashText } from "blume/core/sources/cache.ts"; import type { ContentSource } from "blume/core/sources/types.ts"; import { custom, filesystem } from "blume/sources"; import { githubReleasesSource } from "blume/sources/github-releases.ts"; import redirects from "./redirects"; /** * GitHub releases → /changelog, dated by the real finish instant of the topic * or `Review — ` issue each release records. * * Blume dates a release entry by `published_at`, which the GitHub API cannot * backdate — so the releases backfilled on one day (apps/cli/ * release-topics.ts) would all read that day. Their tags are timever * (`YYYYMMDD.HHMMSS.NNNNNNNNN`, UTC) of the moment the topic or review * finished, so this fetch rewrites each timever release's `published_at` to * that instant before Blume maps it; anything else passes through untouched. * The config-level `githubReleases()` accepts no `fetchImpl`, hence the * `custom()` wrap of the underlying source (the `blume/sources/*.ts` path the * Blume docs use for custom sources), with the context the built-in adapter * would receive. */ const TIMEVER = /^(\d{4})(\d{2})(\d{2})\.(\d{2})(\d{2})(\d{2})\.\d{9}$/; const releasesDatedByTag: typeof fetch = async (input, init) => { const res = await fetch(input, init); if (!res.ok) return res; const releases: unknown = await res.json(); if (Array.isArray(releases)) { for (const release of releases) { const m = typeof release?.tag_name === "string" ? release.tag_name.match(TIMEVER) : null; if (m) release.published_at = `${m[1]}-${m[2]}-${m[3]}T${m[4]}:${m[5]}:${m[6]}Z`; } } // Blume reads only `ok` and the JSON body, so the original headers (whose // content-length/encoding describe the upstream bytes) are not carried over. return Response.json(releases, { status: res.status }); }; /** * The changelog index tags every row with `changelog.category`, which the * GitHub source sets to "Release" for all of them. release-topics.ts names * each release after its issue, so the title prefix says which kind finished * — a topic's core set, or a Review day block — and the row is tagged * `Topic` / `Review` instead. The category lives both in `data` and in the * staged `raw` frontmatter Astro re-parses, so both are rewritten (and the * hash with them). Wrapped around the source Blume rebuilds via * `withContext`, so the rebuilt one is tagged too. */ const RELEASE_KIND = /^(Topic|Review) \d+ — /; const releasesByKind = (source: ContentSource): ContentSource => ({ ...source, load: async () => { const result = await source.load(); for (const entry of result.entries) { const kind = typeof entry.data.title === "string" ? entry.data.title.match(RELEASE_KIND)?.[1] : undefined; const changelog = entry.data.changelog; if (!kind || !changelog || typeof changelog !== "object" || !("category" in changelog)) continue; changelog.category = kind; if (entry.raw) { entry.raw = entry.raw.replace(/^( {2}category: ).*$/m, `$1${kind}`); entry.hash = hashText(entry.raw); } } return result; }, withContext: source.withContext && ((ctx) => releasesByKind(source.withContext!(ctx))), }); // Blume's defaults (`inter`, `ibm-plex-mono`) are fetched from fonts.gstatic.com // on every build, which has timed out on the Pages runner and, fully offline, // silently ships no fonts at all. The same two families from Fontsource, as // local files, keep the build network-free. Blume resolves `src` from this // directory; the workspace links these packages under ./node_modules. const inter = { name: "Inter", variants: [ { src: "./node_modules/@fontsource-variable/inter/files/inter-latin-wght-normal.woff2", weight: "100..900", }, ], }; const plexMono = { name: "IBM Plex Mono", variants: [ { src: "./node_modules/@fontsource/ibm-plex-mono/files/ibm-plex-mono-latin-400-normal.woff2", weight: 400, }, { src: "./node_modules/@fontsource/ibm-plex-mono/files/ibm-plex-mono-latin-500-normal.woff2", weight: 500, }, ], }; /** * Custom frontmatter, rendered as the badge row by * components/ProblemBadges.astro. Problem pages: `topics:` is LeetCode's topic * tags (written by apps/cli/sync.ts), `video:` the NeetCode walkthrough's * YouTube id (apps/cli/videos.ts). Guide pages (content/guides/, created by * sync): `list:` is the tag's LeetCode problem list. Blume rejects undeclared * keys and takes any Standard Schema; these are hand-rolled so the docs * workspace needs no zod. All optional — every other page carries none. */ const optional = ( message: string, valid: (value: unknown) => value is T, ): StandardSchema => ({ "~standard": { version: 1, vendor: "prdlk", validate: (value) => value === undefined || valid(value) ? { value } : { issues: [{ message }] }, }, }); const topicsSchema = optional( "expected a list of { name, slug } LeetCode topic tags", (value): value is { name: string; slug: string }[] => Array.isArray(value) && value.every( (t) => typeof t?.name === "string" && typeof t?.slug === "string", ), ); const videoSchema = optional( "expected an 11-character YouTube video id", (value): value is string => typeof value === "string" && /^[\w-]{11}$/.test(value), ); const listSchema = optional( "expected a https://leetcode.com/problem-list// URL", (value): value is string => typeof value === "string" && /^https:\/\/leetcode\.com\/problem-list\/[\w-]+\/$/.test(value), ); export default defineConfig({ title: "prdlk", description: "Documentation for my LeetCode solutions", logo: "./public/logo.svg", deployment: { site: "https://lc.prd.lk", }, github: { owner: "prdlk", repo: "leetcode", }, lastModified: "git", redirects, frontmatter: { extend: { topics: topicsSchema, video: videoSchema, list: listSchema }, }, seo: { og: { enabled: true }, rss: { enabled: true, types: ["changelog"] }, sitemap: true, robots: true, structuredData: true, x: { handle: "@prdlk" }, }, // Blume's own assistant route is server-rendered (it reads the provider key // per request), which a static Pages build cannot ship. The SRS Worker // answers instead (apps/api/src/ask.ts: OpenRouter, grounded on this site's // llms-full.txt, which `agents.llmsTxt` publishes). ai: { assistant: { enabled: true, endpoint: "https://srs-api.prdlk.workers.dev/ask", }, }, agents: { llmsTxt: true, mcp: { enabled: false }, }, content: { sources: [ filesystem({ root: "content" }), custom( releasesByKind( githubReleasesSource( { name: "changelog", prefix: "changelog", owner: "prdlk", repo: "leetcode", fetchImpl: releasesDatedByTag, }, // What resolve.ts hands the built-in adapter: `.blume/cache//`, // and a refresh on every load (the source reads nothing else). { projectRoot: import.meta.dirname, cacheDir: `${import.meta.dirname}/.blume/cache/changelog`, mode: "build", }, ), ), ), ], }, theme: { fonts: { display: inter, body: inter, mono: plexMono }, }, navigation: { sidebar: { display: "flat", // "flat" | "group" | "page" }, tabs: [ { label: "Guides", path: "/guides" }, { label: "Changelog", path: "/changelog" }, ], }, });