2026-08-25 11:19:31 -04:00
|
|
|
import { defineConfig } from "blume";
|
2026-09-30 16:31:18 -04:00
|
|
|
import { hashText } from "blume/core/sources/cache.ts";
|
|
|
|
|
import type { ContentSource } from "blume/core/sources/types.ts";
|
2026-09-30 17:33:35 -04:00
|
|
|
import type { StandardSchema } from "blume/core/standard-schema.ts";
|
2026-09-25 14:15:44 -04:00
|
|
|
import { custom, filesystem } from "blume/sources";
|
|
|
|
|
import { githubReleasesSource } from "blume/sources/github-releases.ts";
|
2026-09-24 16:15:52 -04:00
|
|
|
import redirects from "./redirects";
|
|
|
|
|
|
2026-09-25 14:15:44 -04:00
|
|
|
/**
|
2026-09-30 16:31:18 -04:00
|
|
|
* GitHub releases → /changelog, dated by the real finish instant of the topic
|
|
|
|
|
* or `Review <N> — <Phase>` issue each release records.
|
2026-09-25 14:15:44 -04:00
|
|
|
*
|
|
|
|
|
* Blume dates a release entry by `published_at`, which the GitHub API cannot
|
2026-09-30 16:31:18 -04:00
|
|
|
* backdate — so the releases backfilled on one day (apps/cli/
|
2026-09-25 14:15:44 -04:00
|
|
|
* release-topics.ts) would all read that day. Their tags are timever
|
2026-09-30 16:31:18 -04:00
|
|
|
* (`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.
|
2026-09-25 14:15:44 -04:00
|
|
|
*/
|
|
|
|
|
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) {
|
2026-09-28 11:19:36 -04:00
|
|
|
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`;
|
2026-09-25 14:15:44 -04:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// 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 });
|
|
|
|
|
};
|
|
|
|
|
|
2026-09-30 16:31:18 -04:00
|
|
|
/**
|
|
|
|
|
* 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) {
|
2026-09-30 17:33:35 -04:00
|
|
|
const kind =
|
|
|
|
|
typeof entry.data.title === "string"
|
|
|
|
|
? entry.data.title.match(RELEASE_KIND)?.[1]
|
|
|
|
|
: undefined;
|
2026-09-30 16:31:18 -04:00
|
|
|
const changelog = entry.data.changelog;
|
2026-09-30 17:33:35 -04:00
|
|
|
if (
|
|
|
|
|
!kind ||
|
|
|
|
|
!changelog ||
|
|
|
|
|
typeof changelog !== "object" ||
|
|
|
|
|
!("category" in changelog)
|
|
|
|
|
)
|
|
|
|
|
continue;
|
2026-09-30 16:31:18 -04:00
|
|
|
changelog.category = kind;
|
|
|
|
|
if (entry.raw) {
|
|
|
|
|
entry.raw = entry.raw.replace(/^( {2}category: ).*$/m, `$1${kind}`);
|
|
|
|
|
entry.hash = hashText(entry.raw);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return result;
|
|
|
|
|
},
|
2026-09-30 17:33:35 -04:00
|
|
|
withContext:
|
|
|
|
|
source.withContext && ((ctx) => releasesByKind(source.withContext!(ctx))),
|
2026-09-30 16:31:18 -04:00
|
|
|
});
|
|
|
|
|
|
2026-09-24 16:07:17 -04:00
|
|
|
// 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: [
|
2026-09-24 16:26:12 -04:00
|
|
|
{
|
|
|
|
|
src: "./node_modules/@fontsource-variable/inter/files/inter-latin-wght-normal.woff2",
|
|
|
|
|
weight: "100..900",
|
|
|
|
|
},
|
2026-09-24 16:07:17 -04:00
|
|
|
],
|
|
|
|
|
};
|
|
|
|
|
const plexMono = {
|
|
|
|
|
name: "IBM Plex Mono",
|
|
|
|
|
variants: [
|
2026-09-24 16:26:12 -04:00
|
|
|
{
|
|
|
|
|
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,
|
|
|
|
|
},
|
2026-09-24 16:07:17 -04:00
|
|
|
],
|
|
|
|
|
};
|
|
|
|
|
|
2026-09-28 11:19:36 -04:00
|
|
|
/**
|
|
|
|
|
* 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 = <T>(
|
|
|
|
|
message: string,
|
|
|
|
|
valid: (value: unknown) => value is T,
|
|
|
|
|
): StandardSchema<unknown, T | undefined> => ({
|
|
|
|
|
"~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/<slug>/ URL",
|
|
|
|
|
(value): value is string =>
|
|
|
|
|
typeof value === "string" &&
|
|
|
|
|
/^https:\/\/leetcode\.com\/problem-list\/[\w-]+\/$/.test(value),
|
|
|
|
|
);
|
|
|
|
|
|
2026-08-25 11:19:31 -04:00
|
|
|
export default defineConfig({
|
2026-09-25 13:30:06 -04:00
|
|
|
title: "prdlk",
|
|
|
|
|
description: "Documentation for my LeetCode solutions",
|
|
|
|
|
logo: "./public/logo.svg",
|
2026-08-25 11:19:31 -04:00
|
|
|
deployment: {
|
2026-09-24 16:26:12 -04:00
|
|
|
site: "https://lc.prd.lk",
|
2026-08-25 11:19:31 -04:00
|
|
|
},
|
2026-09-25 13:36:50 -04:00
|
|
|
github: {
|
|
|
|
|
owner: "prdlk",
|
|
|
|
|
repo: "leetcode",
|
|
|
|
|
},
|
2026-09-24 15:57:38 -04:00
|
|
|
lastModified: "git",
|
2026-09-24 16:15:52 -04:00
|
|
|
redirects,
|
2026-09-28 11:19:36 -04:00
|
|
|
frontmatter: {
|
|
|
|
|
extend: { topics: topicsSchema, video: videoSchema, list: listSchema },
|
|
|
|
|
},
|
2026-09-30 15:19:01 -04:00
|
|
|
seo: {
|
|
|
|
|
og: { enabled: true },
|
|
|
|
|
rss: { enabled: true, types: ["changelog"] },
|
|
|
|
|
sitemap: true,
|
|
|
|
|
robots: true,
|
|
|
|
|
structuredData: true,
|
|
|
|
|
x: { handle: "@prdlk" },
|
|
|
|
|
},
|
2026-09-30 15:31:56 -04:00
|
|
|
// 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).
|
2026-09-30 15:27:52 -04:00
|
|
|
ai: {
|
|
|
|
|
assistant: {
|
|
|
|
|
enabled: true,
|
2026-09-30 15:31:56 -04:00
|
|
|
endpoint: "https://srs-api.prdlk.workers.dev/ask",
|
2026-09-30 15:27:52 -04:00
|
|
|
},
|
|
|
|
|
},
|
2026-09-30 15:19:01 -04:00
|
|
|
agents: {
|
|
|
|
|
llmsTxt: true,
|
|
|
|
|
mcp: { enabled: false },
|
|
|
|
|
},
|
2026-08-25 11:19:31 -04:00
|
|
|
content: {
|
2026-09-25 14:00:22 -04:00
|
|
|
sources: [
|
|
|
|
|
filesystem({ root: "content" }),
|
2026-09-25 14:15:44 -04:00
|
|
|
custom(
|
2026-09-30 16:31:18 -04:00
|
|
|
releasesByKind(
|
|
|
|
|
githubReleasesSource(
|
|
|
|
|
{
|
|
|
|
|
name: "changelog",
|
|
|
|
|
prefix: "changelog",
|
|
|
|
|
owner: "prdlk",
|
|
|
|
|
repo: "leetcode",
|
|
|
|
|
fetchImpl: releasesDatedByTag,
|
|
|
|
|
},
|
|
|
|
|
// What resolve.ts hands the built-in adapter: `.blume/cache/<name>/`,
|
|
|
|
|
// and a refresh on every load (the source reads nothing else).
|
|
|
|
|
{
|
|
|
|
|
projectRoot: import.meta.dirname,
|
|
|
|
|
cacheDir: `${import.meta.dirname}/.blume/cache/changelog`,
|
|
|
|
|
mode: "build",
|
|
|
|
|
},
|
|
|
|
|
),
|
2026-09-25 14:15:44 -04:00
|
|
|
),
|
|
|
|
|
),
|
2026-09-25 14:00:22 -04:00
|
|
|
],
|
2026-08-25 11:19:31 -04:00
|
|
|
},
|
2026-09-24 16:07:17 -04:00
|
|
|
theme: {
|
|
|
|
|
fonts: { display: inter, body: inter, mono: plexMono },
|
|
|
|
|
},
|
2026-09-24 15:57:38 -04:00
|
|
|
navigation: {
|
2026-09-30 17:33:35 -04:00
|
|
|
repo: "https://github.com/prdlk/leetcode",
|
2026-09-24 15:57:38 -04:00
|
|
|
sidebar: {
|
|
|
|
|
display: "flat", // "flat" | "group" | "page"
|
|
|
|
|
},
|
2026-09-30 17:38:42 -04:00
|
|
|
tabs: [
|
|
|
|
|
{ label: "Guides", path: "/guides" },
|
|
|
|
|
{ label: "Changelog", path: "/changelog" },
|
|
|
|
|
],
|
2026-09-24 15:57:38 -04:00
|
|
|
},
|
2026-08-25 11:19:31 -04:00
|
|
|
});
|