diff --git a/apps/docs/blume.config.ts b/apps/docs/blume.config.ts index 26eb312..4612dfc 100644 --- a/apps/docs/blume.config.ts +++ b/apps/docs/blume.config.ts @@ -1,4 +1,5 @@ import { defineConfig } from "blume"; +import type { StandardSchema } from "blume/core/standard-schema.ts"; import { custom, filesystem } from "blume/sources"; import { githubReleasesSource } from "blume/sources/github-releases.ts"; import redirects from "./redirects"; @@ -24,8 +25,12 @@ const releasesDatedByTag: typeof fetch = async (input, init) => { 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`; + 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 @@ -61,6 +66,49 @@ const plexMono = { ], }; +/** + * 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", @@ -74,6 +122,9 @@ export default defineConfig({ }, lastModified: "git", redirects, + frontmatter: { + extend: { topics: topicsSchema, video: videoSchema, list: listSchema }, + }, content: { sources: [ filesystem({ root: "content" }), @@ -104,6 +155,9 @@ export default defineConfig({ sidebar: { display: "flat", // "flat" | "group" | "page" }, - tabs: [{ label: "Changelog", path: "/changelog" }], + tabs: [ + { label: "Guides", path: "/guides" }, + { label: "Changelog", path: "/changelog" }, + ], }, });