Compare commits

..
3 Commits
41 changed files with 8183 additions and 353 deletions

No files matched your search

+5 -9
View File
@@ -2,7 +2,7 @@
## Project Overview
LeetCode interview-prep campaign (2026-08-17 → Dec 5: eight weeks of topics, the Review-issue phase, a two-week break, then a 30-day interview sprint from Fri Nov 6 (`SPRINT_START`); the end is the last date in `apps/api/data/schedule.json` — `CAMPAIGN_END`/`CAMPAIGN_DAYS`/`SPRINT_DAYS` in `apps/api/src/srs.ts` are derived from it, never declared) run as a repo: solution files under `work/`, a Blume docs site under `apps/docs/`, GitHub Actions that reconcile issues and publish docs, and a Cloudflare Worker (`apps/api/`) that runs the spaced-repetition system (SRS) — daily digest email, one-tap logging, weekly review issues, live charts. Issues model the curriculum: ~24 `topic` issues (`Topic NN — Name`) own ~161 `problem` sub-issues titled `LC <num> · <Name>`, with difficulty and set carried **only** by labels (`set:core|optional|deferred`, `diff:*`) — never by the title; milestones are phases; a user-level GitHub Project ("Interview Prep", #2) mirrors review state.
LeetCode interview-prep campaign (2026-08-17 → Nov 3: eight weeks of topics, then the Review-issue phase; the end is the last date in `apps/api/data/schedule.json` — `CAMPAIGN_END`/`CAMPAIGN_DAYS` in `apps/api/src/srs.ts` are derived from it, never declared) run as a repo: solution files under `work/`, a Blume docs site under `apps/docs/`, GitHub Actions that reconcile issues and publish docs, and a Cloudflare Worker (`apps/api/`) that runs the spaced-repetition system (SRS) — daily digest email, one-tap logging, weekly review issues, live charts. Issues model the curriculum: ~24 `topic` issues (`Topic NN — Name`) own ~161 `problem` sub-issues titled `LC <num> · <Name>`, with difficulty and set carried **only** by labels (`set:core|optional|deferred`, `diff:*`) — never by the title; milestones are phases; a user-level GitHub Project ("Interview Prep", #2) mirrors review state.
## Architecture & Data Flow
@@ -40,7 +40,7 @@ flowchart LR
- **Chained workflows:** `GITHUB_TOKEN`-driven issue closes fire no `issues` events, so `close-topics.yml` chains off `workflow_run` of Close Solved instead. The Worker's webhook uses its own `GH_PAT`, so its events flow normally.
- **One "finished topic" rule, two consumers.** `apps/cli/topics.ts` (`coreSet()`: every `set:core` sub-issue closed, zero core = never) is read by `close-topics.ts` (closes the topic issue) and `release-topics.ts` (the `release` job of `close-solved.yml`). The release reads the sub-issues, not the topic issue, so the push that finishes a topic releases it in the same run instead of waiting for Close Topics to chain after it. Version = [timever](https://gist.github.com/twolfson/de1b004dd22536b8e668) (`YYYYMMDD.HHMMSS.NNNNNNNNN`, UTC) of the instant the last core problem closed; the tag is annotated and backdated to that instant, on the last first-parent main commit before it; the body is a changelog snapshot at that instant (a one-sentence summary first, then the problems closed on the finish day / earlier, with solution links at the tag — problems still open at the finish are left out). A release is found by its `<!-- release-topics: #N -->` body marker, never by tag name, so re-closing a core problem never mints a second one; an existing release keeps its tag and has its name (the topic title) and body rewritten whenever the render changes. The docs `/changelog` is ONE custom page, `apps/docs/pages/changelog.astro` (a NeonUI-style timeline: date left, a card per release right, no per-release subpages, so nothing of it enters the sidebar or prev/next), linked from the header by a `navigation.tabs` entry (beside `/appendix`'s). `apps/docs/lib/releases.ts` fetches the releases at build (`GITHUB_TOKEN` in `deploy.yml`; a failed fetch fails the build), dates each by its timever tag, not `published_at` (which the API cannot backdate), tags it `Topic` or `Review` from the release name, and lists the rows of its `### Closed …` tables — so the tag format and that table shape are the page's contract with `render()`. There is no changelog RSS feed.
- **Review issues re-parent problems; topics still own them.** The transfer phase's 20 `Review <N> — <Phase>` issues (label `review`, milestone `Review — Transfer & Mocks`; not the Worker's `Review — Week N` gate issues) hold open optional/deferred problems as sub-issues in solve order, moved there with `replace_parent`. `apps/api/src/reviews.ts` is the ONE recovery of ownership — `readReviews()` (one GraphQL request per 50 `review` issues, cheap enough for the Worker's cron subrequest budget) reads each moved problem's topic off its last `parent_issue_removed` event naming a topic, `movedFromTopics()` groups them — shared by the Worker's catalog reconcile (a moved problem's D1 row keeps syncing its labels and `topic_issue`) and `apps/cli/topics.ts` (`readProblems()` returns a topic's sub-issues plus its moved problems, so the finished rule and existing release bodies do not change when a problem moves). A Review issue finishes by `completion()` in `topics.ts` — every sub-issue closed, any set — which `close-topics.ts` closes it on and `release-topics.ts` releases it on, exactly like a topic: timever of the last close, same `<!-- release-topics: #N -->` marker (issue numbers never collide), body fields from the review's `**Scheduled:**`/`**Phase:**`/`**Load:**` lines.
- **Review days are plan days.** `schedule.json` books each Review issue on its own date — Mon/Tue/Thu/Fri only; Reviews 1–6 ran Oct 1–9, Review 4 closed out Nov 2 after the break, and Reviews 7–20 run through the sprint (Nov 6 → Dec 1, Thanksgiving left unbooked), followed by two sweep days and a `gate: []` Saturday that marks the last day — as `review: { issue, title, problems }` (solve order). The digest renders it as a linked "Today's review" section with the issue's day rules (`REVIEW_RULES` in `email.tsx`, one sentence for HTML and text), its problems join `RESERVED` so drills and sampled gates never draw them early, and `gate: []` skips a Saturday's checkpoint (Oct 3, Dec 5). The issue's sub-issues stay the truth: the catalog reconcile compares every booked review's title and problem list with GitHub and returns any mismatch as `reviewDrift` (also logged), so a re-arranged Review issue shows up the next sync instead of silently disagreeing with the email. Move a review by editing its `schedule.json` date AND the issue's `**Scheduled:**` line (the release body reads it).
- **Review days are plan days.** `schedule.json` books each Review issue on its own date — Mon/Tue/Thu/Fri only, Oct 1 → Nov 3 — as `review: { issue, title, problems }` (solve order). The digest renders it as a linked "Today's review" section with the issue's day rules (`REVIEW_RULES` in `email.tsx`, one sentence for HTML and text), its problems join `RESERVED` so drills and sampled gates never draw them early, and `gate: []` skips a Saturday's checkpoint (Oct 3). The issue's sub-issues stay the truth: the catalog reconcile compares every booked review's title and problem list with GitHub and returns any mismatch as `reviewDrift` (also logged), so a re-arranged Review issue shows up the next sync instead of silently disagreeing with the email. Move a review by editing its `schedule.json` date AND the issue's `**Scheduled:**` line (the release body reads it).
## Key Directories
@@ -71,11 +71,7 @@ bun run test # run ONE solution against LeetCode's judge (not a test
# files git sees as modified/untracked first, so Enter runs the
# one in progress; then most recently tested; stamped on exit 0)
bun run submit # submit ONE solution to LeetCode (uncommitted first, then newest
# scaffold, nothing hidden; last_submitted stamped on exit 0).
# Both fold in the signed-in profile's submission history once
# a day (leetcode-cli's keychain login; GraphQL only — LeetCode's
# REST /api/* answers 403), so a lost db or fresh clone recovers
# both orderings
# scaffold, nothing hidden; last_submitted stamped on exit 0)
bun run sync # work/1 → apps/docs/content/ pages + apps/docs/redirects.ts (/<num> short
# links; re-solves are never published)
bun run videos # docs pages with no `video:` frontmatter → NeetCode video per page
@@ -113,10 +109,10 @@ curl -X POST -H "Authorization: Bearer $LINK_KEY" \
## Important Files
- `apps/api/src/srs.ts` — SRS domain: the ladder (`new → +3 → +7 → retired`; fail resets to `+3`; first-ever log enters at `+3`), `WINDOWS`/`STAGES`/`TEMPERATURES`/`DIFFICULTIES`/`SETS` (the one vocabulary shared by the charts, the `work/<n>` buckets, the review issues and the stats dashboard), ET date math, `workingDay()` (no scheduled date is ever a Sunday), `temperatureOf()` (days since a concept was last exercised → `hot`/`fresh`/`fading`/`cold`, bands derived from `WINDOWS` so they cannot drift; read-side only, nothing is scheduled off it), seeded sampling, `logAttempt()` (the ONE write path — email taps, webhook, gate scoring all converge here), and overload leveling: at most `REVIEW_CAP` (5 since the break, to drain its backlog within the sprint) reviews surface per day — `levelReviews()` (run by `sendDigest` on real sends only, never dry) gives everything past the cap a concrete future WORKING day, ≤ 5 per day, oldest first, instead of letting the due pile grow.
- `apps/api/src/srs.ts` — SRS domain: the ladder (`new → +3 → +7 → retired`; fail resets to `+3`; first-ever log enters at `+3`), `WINDOWS`/`STAGES`/`TEMPERATURES`/`DIFFICULTIES`/`SETS` (the one vocabulary shared by the charts, the `work/<n>` buckets, the review issues and the stats dashboard), ET date math, `workingDay()` (no scheduled date is ever a Sunday), `temperatureOf()` (days since a concept was last exercised → `hot`/`fresh`/`fading`/`cold`, bands derived from `WINDOWS` so they cannot drift; read-side only, nothing is scheduled off it), seeded sampling, `logAttempt()` (the ONE write path — email taps, webhook, gate scoring all converge here), and overload leveling: at most `REVIEW_CAP` (3) reviews surface per day — `levelReviews()` (run by `sendDigest` on real sends only, never dry) gives everything past the cap a concrete future WORKING day, ≤ 3 per day, oldest first, instead of letting the due pile grow.
- `apps/api/src/index.ts` — router + cron dispatch; `apps/api/wrangler.jsonc` — bindings (`DB`, `EMAIL`), crons, vars; secrets `GH_PAT`/`WEBHOOK_SECRET`/`LINK_KEY` via `wrangler secret put`, and `OPENROUTER_API_KEY`, whose one copy is the repo's Actions secret (deploy.yml puts it before every deploy — rotate it there).
- `apps/api/src/ask.ts` — `POST /ask`, the docs assistant's backend. Blume's own assistant route is server-rendered and cannot ship in the static Pages build (`BLUME_SERVER_FEATURE_REQUIRED`), so `blume.config.ts` sets `ai.assistant.endpoint` to this route instead of a `provider`. It speaks the Blume island's wire contract (`{ messages, page: { path } }` in, a plain-text stream out, a 503 "The assistant is not configured…" when the key is missing), validates like Blume's generated route (1–40 user/assistant messages, 24 000 chars, 64 KB — no caller system prompt), grounds on the published `llms-full.txt` (current page first, then keyword-ranked excerpts in Blume's 6 × 2 000 / 10 000-char budget, cached 5 min), and streams OpenRouter's chat completions over plain `fetch` — no AI SDK — for the `ASK_MODEL` var. `:free` models share an upstream pool and often answer 429, which the island shows as its rate-limit notice.
- `apps/api/src/digest.ts` / `apps/api/src/email.tsx` — the daily digest, split data/presentation. `digest.ts` reads D1 into a `DigestData`; `email.tsx` owns every colour and every sentence, and renders both the HTML and (via its own `plainDigest`, not React Email's `plainText` mode, which flattens the tables) the text alternative. The retrieval rules hold by construction: `DigestRow` has no title and no issue field, so a review or drill line *cannot* leak the topic or a solution link. Subject is `(Day N/<SPRINT_DAYS>) LeetCode Daily Digest` and the progress line `Day N of <SPRINT_DAYS> · Week k` (Mon–Sun weeks, so the Friday start makes week 1 three days long) — 30 with the sprint running Nov 6 → Dec 5; before day 1 they count down (`(Sprint in N days)`). Booking a later day in `schedule.json` lengthens the sprint, and the whole-campaign heatmaps and `/api/stats` `heat` (still from `CAMPAIGN_START`) with it.
- `apps/api/src/digest.ts` / `apps/api/src/email.tsx` — the daily digest, split data/presentation. `digest.ts` reads D1 into a `DigestData`; `email.tsx` owns every colour and every sentence, and renders both the HTML and (via its own `plainDigest`, not React Email's `plainText` mode, which flattens the tables) the text alternative. The retrieval rules hold by construction: `DigestRow` has no title and no issue field, so a review or drill line *cannot* leak the topic or a solution link. Subject is `(Day N/<CAMPAIGN_DAYS>) LeetCode Daily Digest` — 79 with the plan ending Nov 3; booking a later day in `schedule.json` lengthens it, and the heatmaps and `/api/stats` `heat` with it.
- `apps/api/src/png.ts` — hand-rolled PNG encoder (RGB8, one IDAT, zlib via `CompressionStream("deflate")`, CRC32, 5×7 bitmap font). It exists because every major email client refuses remote SVG, so `/chart/heatmap.png` rasters the heatmap for the digest while `/chart/heatmap.svg` keeps serving the README byte-for-byte. Both come from one `heatmapCells()` so the two pictures cannot drift.
- `apps/cli/sync.ts` — work→docs contract: on existing pages only the frontmatter `icon:` and the `## Solution` section are regenerated, and `topics:` (LeetCode's `topicTags` as `{ name, slug }`, fetched by GraphQL) and `issue:` (the URL of the problem's `problem`-labelled GitHub issue, `LC <num> · …`, listed via `github()` — rendered as the neutral `Issue #N` badge after `Explanation`) are filled once — on create, or on any run that finds them missing — never refreshed (delete a key to refetch; offline or without `gh` auth, the page is written without it and the next sync retries; a sync with nothing missing makes no request). Only a problem the README plan lists gets `issue:` — an unplanned warm-up has no catalog issue, so it is never looked up. `companies:` (`{ name, period, frequency }`: every company that asked the problem in the last six months, at the most recent of the `Thirty Days`/`Three Months`/`Six Months` windows it appears in and its frequency there, most recent first — the vocabulary is ONE module, `apps/docs/companies.ts`, shared by sync, the schema and both readers) is the exception that IS rewritten every run, since recency ages: it comes from the local DuckDB company-frequency database (`~/.local/share/duckdb/data/leetcode-company-frequency.db`, built by that project's `mise run lc:build`) via one `duckdb -readonly -json` query, and where the database or the `duckdb` CLI is missing (CI) the step is skipped and pages keep the key as it is. `ProblemBadges.astro` renders it as a TypeTable-style table under the badge row (company pill · asked within · frequency bar), each pill linking to `companyHref()` — the company's Appendix card, filed A–Z among the glossary terms (category `Company`, no definition, no hand-written table: derived from every page's `companies:`), whose Solved backlinks are the problems it asked, each with how recently. `## Solution` is the latter bounded by `solutionEnd()` at the next unfenced top-level heading; human prose before it and any sections after it are never touched. Never hand-write solution pages or edit inside `## Solution`. Two classifications, two homes: the sidebar group is the README topic (`readmeTopics()`, first listing wins for a problem under two topics; a warm-up outside the plan groups under its category at `order: 0`, ahead of topic 1) and the `work/1/<Difficulty>/<Category>/` category, which only places an unplanned problem — the page's topic badges are LeetCode's tags, not either classification. Topics nest under their README phase (`## Phase N — Name` headings, shortened via `PHASE_ALIASES`): `content/(0N-<phase>)/<topic>/`, with every problem page in the topic's `(problems)/` group as `<num>_<slug>.mdx` (a page is found by its frontmatter title, wherever it sits, so a layout change is one sync of `rename`s). The topic `meta.ts` is script-owned and rewritten every run (label, curriculum order, `TOPIC_ICONS` icon, `display: "page"`); the phase `meta.ts` is written once when missing (`Phase N: Name`, `order: N`) and hand-owned after — rename its title freely. Never rewriting it loses nothing: its order and name are also the folder name, so a README change to either lands in a new folder with a fresh meta. The phase folder is a Blume `(group)` — no route segment, so `/tree-dfs` is unchanged and a landing page there is one level deep, never at `/` — and the topic folder is the real segment; never put a topic `index.mdx` directly inside a `(group)`. A page is `rename`d when its topic or phase changes; a folder every page has left is deleted (climbing through an emptied `(problems)` and topic into an emptied phase folder) unless it still holds a hand-written `index.mdx` (sync never writes, moves, or deletes one — so page-relative asset paths in a moved landing page are yours to fix). Slugs come from `slugify()`, which glues a leading number to the next token (`1d-dynamic-programming`) because Blume strips `<digits>-` prefixes from routes. The hand-owned parts have their own owners: `## Approach` (a Mermaid flowchart tracing the page's code, placed *before* `## Solution` so a re-sync keeps it) is written per the `problem-approach` skill (`/problem-approach` fills every page missing one); the frontmatter `video:` (the NeetCode walkthrough, rendered as the red `Explanation` badge) is added by `bun run videos --write` (`apps/cli/videos.ts`).
- `apps/cli/work.ts` — the `work/` layout: bucket paths, `isSolution()`/`solvedInBucket()` (the stub test, shared with `close-solved.ts`), `firstSolved()` (one `git log`, ET dates, keyed by LC number so past layout moves don't matter), and `reviewQueues()` (what each window still owes). Everything that needs to know where a solution file lives, or when it was solved, goes through here.
+1 -1
View File
@@ -9,7 +9,7 @@
</p>
<p align="center">
<sub>Interview-prep fight camp: eight weeks of topics, a review phase, then a 30-day interview sprint to Dec 5. Each phase is a milestone; each topic an issue; each problem a sub-issue.<br>
<sub>Interview-prep fight camp: eight weeks of topics, then a review phase to Nov 3. Each phase is a milestone; each topic an issue; each problem a sub-issue.<br>
New topic Mon–Fri, gate on Saturday, Sunday off — every solve comes back at <strong>+3</strong> and <strong>+7</strong> days,
the two windows that keep the rest day clear (a Thursday solve's +3 slides to Monday).<br>
Charts are live from the SRS Worker, never committed: pushing a solution or tapping the daily digest moves them within ~5 min.</sub>
+15 -18
View File
@@ -26,24 +26,21 @@
"2026-10-02": { "review": { "issue": 226, "title": "Review 2 — Linear Structures", "problems": [290, 525, 189, 1004, 84] }, "sweep": true },
"2026-10-03": { "gate": [] },
"2026-10-05": { "review": { "issue": 227, "title": "Review 3 — Linear Structures", "problems": [643, 128, 162, 71, 4] } },
"2026-10-06": { "review": { "issue": 228, "title": "Review 4 — Linear Structures", "problems": [496, 80, 901, 76, 224] } },
"2026-10-08": { "review": { "issue": 229, "title": "Review 5 — Nodal & Grid", "problems": [876, 19, 36] } },
"2026-10-09": { "review": { "issue": 230, "title": "Review 6 — Nodal & Grid", "problems": [92, 82, 380] } },
"2026-11-02": { "review": { "issue": 228, "title": "Review 4 — Linear Structures", "problems": [496, 80, 901, 76, 224] } },
"2026-11-06": { "review": { "issue": 231, "title": "Review 7 — Nodal & Grid", "problems": [143, 86, 289] } },
"2026-11-09": { "review": { "issue": 232, "title": "Review 8 — Nodal & Grid", "problems": [61, 138] } },
"2026-11-10": { "review": { "issue": 233, "title": "Review 9 — Hierarchical", "problems": [100, 112, 530, 1046, 103, 124] } },
"2026-11-12": { "review": { "issue": 234, "title": "Review 10 — Hierarchical", "problems": [572, 543, 703, 105, 295] } },
"2026-11-13": { "review": { "issue": 235, "title": "Review 11 — Hierarchical", "problems": [101, 637, 173, 621, 502] } },
"2026-11-16": { "review": { "issue": 236, "title": "Review 12 — Hierarchical", "problems": [108, 129, 373, 648, 212] } },
"2026-11-17": { "review": { "issue": 237, "title": "Review 13 — Relational", "problems": [1971, 695, 547, 433] } },
"2026-11-19": { "review": { "issue": 238, "title": "Review 14 — Relational", "problems": [130, 261, 909, 1462] } },
"2026-11-20": { "review": { "issue": 239, "title": "Review 15 — Relational", "problems": [286, 1254, 721, 399] } },
"2026-11-23": { "review": { "issue": 240, "title": "Review 16 — Relational", "problems": [1091, 802, 269] } },
"2026-11-24": { "review": { "issue": 241, "title": "Review 17 — Decision Space", "problems": [746, 228, 17, 77, 213, 63, 122] } },
"2026-11-27": { "review": { "issue": 242, "title": "Review 18 — Decision Space", "problems": [22, 90, 139, 120, 45, 253, 72] } },
"2026-11-30": { "review": { "issue": 243, "title": "Review 19 — Decision Space", "problems": [79, 131, 91, 494, 763, 986, 920] } },
"2026-12-01": { "review": { "issue": 244, "title": "Review 20 — Decision Space", "problems": [300, 647, 97, 221, 918, 452, 52] } },
"2026-12-03": { "sweep": true },
"2026-12-04": { "sweep": true },
"2026-12-05": { "gate": [] }
"2026-10-12": { "review": { "issue": 231, "title": "Review 7 — Nodal & Grid", "problems": [143, 86, 289] } },
"2026-10-13": { "review": { "issue": 232, "title": "Review 8 — Nodal & Grid", "problems": [61, 138] } },
"2026-10-15": { "review": { "issue": 233, "title": "Review 9 — Hierarchical", "problems": [100, 112, 530, 1046, 103, 124] } },
"2026-10-16": { "review": { "issue": 234, "title": "Review 10 — Hierarchical", "problems": [572, 543, 703, 105, 295] } },
"2026-10-19": { "review": { "issue": 235, "title": "Review 11 — Hierarchical", "problems": [101, 637, 173, 621, 502] } },
"2026-10-20": { "review": { "issue": 236, "title": "Review 12 — Hierarchical", "problems": [108, 129, 373, 648, 212] } },
"2026-10-22": { "review": { "issue": 237, "title": "Review 13 — Relational", "problems": [1971, 695, 547, 433] } },
"2026-10-23": { "review": { "issue": 238, "title": "Review 14 — Relational", "problems": [130, 261, 909, 1462] } },
"2026-10-26": { "review": { "issue": 239, "title": "Review 15 — Relational", "problems": [286, 1254, 721, 399] } },
"2026-10-27": { "review": { "issue": 240, "title": "Review 16 — Relational", "problems": [1091, 802, 269] } },
"2026-10-29": { "review": { "issue": 241, "title": "Review 17 — Decision Space", "problems": [746, 228, 17, 77, 213, 63, 122] } },
"2026-10-30": { "review": { "issue": 242, "title": "Review 18 — Decision Space", "problems": [22, 90, 139, 120, 45, 253, 72] } },
"2026-11-02": { "review": { "issue": 243, "title": "Review 19 — Decision Space", "problems": [79, 131, 91, 494, 763, 986, 920] } },
"2026-11-03": { "review": { "issue": 244, "title": "Review 20 — Decision Space", "problems": [300, 647, 97, 221, 918, 452, 52] } }
}
+6 -18
View File
@@ -10,13 +10,12 @@
* either, so the rule holds by construction. The plan's own sections — a
* learning day's topics (core, then the optional "if time" list), a set
* day's problems, and a review day's `Review <N>` issue — are the only
* labeled, linked ones. At most REVIEW_CAP (5)
* labeled, linked ones. At most REVIEW_CAP (3)
* reviews per day; drills fill toward DAILY_CAP (6) only when the due queue
* is light, and never on a sweep day, which assigns every open miss and
* nothing new. Overflow is levelled, not stacked: sendDigest gives every
* review past the cap a concrete future date, ≤ REVIEW_CAP per day, oldest
* first. Sunday is a two-line rest note. Day N/M counts the interview sprint
* (SPRINT_START → the plan's last day); before it, the email counts down.
* first. Sunday is a two-line rest note.
*
* Idempotency: email_log keys sends by ET date — a same-day re-send is a
* no-op unless forced; the body itself is deterministic (drill picks are
@@ -24,15 +23,15 @@
* and a re-run finds nothing left over the cap.
*/
import {
CAMPAIGN_DAYS,
MISS_WHERE,
type ProblemRow,
RESERVED,
REVIEW_CAP,
SCHEDULE,
SPRINT_DAYS,
SPRINT_START,
type ScheduledDay,
addDays,
campaignDay,
campaignWeek,
dueReviews,
isoWeek,
@@ -40,8 +39,6 @@ import {
lookupProblems,
pickDrills,
prettyDate,
sprintDay,
sprintWeek,
streak,
weekdayOf,
} from "./srs.ts";
@@ -110,13 +107,9 @@ export async function collectDigest(env: Env, date: string): Promise<DigestData>
const drills =
rest || plan.sweep ? [] : await pickDrills(db, date, DAILY_CAP - Math.min(due.length, DAILY_CAP));
const sprint = sprintDay(date);
const data: DigestData = {
day: prettyDate(date),
progress:
sprint >= 1
? `Day ${sprint} of ${SPRINT_DAYS} · Week ${sprintWeek(date)}`
: `The ${SPRINT_DAYS}-day interview sprint starts ${prettyDate(SPRINT_START)}`,
progress: `Day ${campaignDay(date)} of ${CAMPAIGN_DAYS} · Week ${campaignWeek(date)}`,
streak: await streak(db, date),
rest,
topics: [],
@@ -213,13 +206,8 @@ export async function collectDigest(env: Env, date: string): Promise<DigestData>
export async function buildDigest(env: Env, date: string): Promise<Digest> {
const data = await collectDigest(env, date);
const { html, text } = await renderDigest(data);
const sprint = sprintDay(date);
const until = 1 - sprint;
return {
subject:
sprint >= 1
? `(Day ${sprint}/${SPRINT_DAYS}) LeetCode Daily Digest`
: `(Sprint in ${until} ${until === 1 ? "day" : "days"}) LeetCode Daily Digest`,
subject: `(Day ${campaignDay(date)}/${CAMPAIGN_DAYS}) LeetCode Daily Digest`,
html,
text,
};
+1 -1
View File
@@ -108,7 +108,7 @@ export interface TopicSection {
export interface DigestData {
/** "Tuesday, August 25" */
day: string;
/** "Day 9 of 30 · Week 2" of the sprint (SPRINT_DAYS, derived from the plan), or its countdown before day 1. */
/** "Day 9 of 79 · Week 2" — the length is CAMPAIGN_DAYS, derived from the plan. */
progress: string;
streak: number;
rest: boolean;
+2 -17
View File
@@ -15,12 +15,6 @@ import {
addDays,
campaignDay,
campaignWeek,
CAMPAIGN_DAYS,
CAMPAIGN_END,
SPRINT_DAYS,
SPRINT_START,
sprintDay,
sprintWeek,
etDate,
etHour,
isRestDay,
@@ -34,8 +28,8 @@ import {
workingDay,
} from "./srs.ts";
/** Every ET date of the campaign (sprint included), plus two weeks of ladder tail. */
const CALENDAR = Array.from({ length: CAMPAIGN_DAYS + 14 }, (_, i) => addDays("2026-08-17", i));
/** Every ET date of the campaign, plus the deferred-Hard tail. */
const CALENDAR = Array.from({ length: 70 }, (_, i) => addDays("2026-08-17", i));
describe("DST-proof cron guard", () => {
test("digest fires only at ET hour 8 — EDT (UTC-4)", () => {
@@ -73,15 +67,6 @@ describe("date math (noon-UTC anchored)", () => {
expect(campaignWeek("2026-08-30")).toBe(2);
expect(campaignWeek("2026-08-31")).toBe(3);
});
test("sprint math: day 1 is SPRINT_START, the last is the plan's end, weeks run Mon–Sun", () => {
expect(sprintDay(SPRINT_START)).toBe(1);
expect(sprintDay(addDays(SPRINT_START, -1))).toBe(0); // countdown, never "Day 0 of N"
expect(sprintDay(CAMPAIGN_END)).toBe(SPRINT_DAYS);
expect(sprintWeek("2026-11-06")).toBe(1); // Friday start
expect(sprintWeek("2026-11-08")).toBe(1); // its Sunday closes week 1
expect(sprintWeek("2026-11-09")).toBe(2);
expect(sprintWeek(CAMPAIGN_END)).toBe(5);
});
test("iso week is stable across a week", () => {
expect(isoWeek("2026-08-24")).toBe(isoWeek("2026-08-29"));
expect(isoWeek("2026-08-30")).toBe(isoWeek("2026-08-24")); // Sun ends the ISO week
+2 -30
View File
@@ -166,30 +166,6 @@ export const SCHEDULE: Record<string, ScheduledDay> = scheduleJson;
export const CAMPAIGN_END = Object.keys(SCHEDULE).sort().at(-1)!;
export const CAMPAIGN_DAYS = daysBetween(CAMPAIGN_START, CAMPAIGN_END) + 1;
/**
* Day 1 of the interview sprint: the plan restarted here after two weeks off,
* with every open Review issue rebooked between this Friday and CAMPAIGN_END.
* Only the start is declared; the length is derived like CAMPAIGN_DAYS, so
* booking a later day lengthens both. The digest counts days and weeks from
* here; the heatmaps and /api/stats keep the whole campaign.
*/
export const SPRINT_START = "2026-11-06";
export const SPRINT_DAYS = daysBetween(SPRINT_START, CAMPAIGN_END) + 1;
/** 1-based sprint day; zero or negative before SPRINT_START. */
export function sprintDay(date: string): number {
return daysBetween(SPRINT_START, date) + 1;
}
/**
* 1-based sprint week, Mon–Sun like the campaign's, so the Friday start makes
* week 1 three days long and every later week a whole one.
*/
export function sprintWeek(date: string): number {
const monday = addDays(SPRINT_START, -((weekdayOf(SPRINT_START) + 6) % 7));
return Math.floor(daysBetween(monday, date) / 7) + 1;
}
/** Date the schedule teaches a topic on, if it teaches it at all. */
export function topicDate(topic: number): string | undefined {
for (const [date, day] of Object.entries(SCHEDULE)) {
@@ -437,12 +413,8 @@ export async function dueToday(db: D1Database, date: string): Promise<{ date: st
};
}
/**
* Reviews surfaced per day; the digest levels everything past this forward.
* 5, not 3, since the two-week break: at 3 a day the ~110-review backlog would
* outlast the sprint by a fortnight.
*/
export const REVIEW_CAP = 5;
/** Reviews surfaced per day; the digest levels everything past this forward. */
export const REVIEW_CAP = 3;
/**
* Load-level an overloaded review queue: everything due beyond today's
+42 -220
View File
@@ -9,20 +9,12 @@
* slug and kept forever. Only the campaign problems linked from README.md
* are ever fetched, and paid-only problems are skipped because GraphQL
* returns null content for them.
* Everything goes through /graphql: Cloudflare challenges the REST endpoints
* (`/api/problems/all/`, `/api/submissions/`) with a 403 for any non-browser
* client, signed in or not.
*
* A `solutions` table tracks the lifecycle of each work/ file by LC number:
* `created_at` (backfilled from the file's birthtime on every picker run),
* `last_tested` and `last_submitted` (stamped on exit 0 of leetcode-cli).
* The test picker sorts by last_tested, the submit picker by created_at.
* Local stamps die with the db and birthtimes with a clone, so at most once
* per 24h (`meta.history_fetched_at`) the signed-in profile's submission
* history is folded in: a problem's first submission caps `created_at`, its
* latest raises `last_submitted` and `last_tested` (a submission is a judge
* run too; LeetCode keeps no record of `leetcode test` runs). Folding only
* ever moves a timestamp outward, so a re-run is a no-op.
* The test picker sorts by last_tested, the submit picker by created_at, and
* submit hides anything with a last_submitted.
*
* Library: side-effect-free on import — nothing is opened or fetched until
* problemDb() is called. Index upserts never touch the description column, so
@@ -48,8 +40,8 @@ export interface SolutionActivity {
lastSubmitted: string | null;
}
const DIFFICULTY = { EASY: "Easy", MEDIUM: "Medium", HARD: "Hard" } as const;
const TTL_MS = 24 * 60 * 60 * 1000;
const DIFFICULTY = ["", "Easy", "Medium", "Hard"] as const;
const INDEX_TTL_MS = 24 * 60 * 60 * 1000;
export const DB_PATH = join(
process.env.XDG_DATA_HOME ?? join(homedir(), ".local", "share"),
@@ -70,185 +62,50 @@ export function readmeSlugs(markdown: string): string[] {
// ── remote fetches ───────────────────────────────────────────────
/** leetcode-cli's login: the two cookies every signed-in request carries. */
export interface Credentials {
session: string;
csrfToken: string;
}
/**
* POST one query to leetcode.com/graphql. Throws on an HTTP error or a
* response with no `data`; partial `errors` beside data are the caller's.
*/
async function graphql<T>(
query: string,
variables: Record<string, unknown>,
{ referer = "https://leetcode.com/", auth }: { referer?: string; auth?: Credentials } = {},
): Promise<T> {
const res = await fetch("https://leetcode.com/graphql", {
method: "POST",
headers: {
"content-type": "application/json",
"user-agent": "Mozilla/5.0",
referer,
...(auth && {
cookie: `LEETCODE_SESSION=${auth.session}; csrftoken=${auth.csrfToken}`,
"x-csrftoken": auth.csrfToken,
}),
},
body: JSON.stringify({ query, variables }),
});
if (!res.ok) throw new Error(`POST /graphql -> ${res.status} ${res.statusText}`);
const body = (await res.json()) as { data?: T | null; errors?: { message: string }[] };
if (!body.data) {
throw new Error(`POST /graphql: ${body.errors?.map((e) => e.message).join("; ") || "no data"}`);
}
return body.data;
}
/** Server-side cap on `limit` for the problem list. */
const INDEX_PAGE = 100;
async function fetchIndex(): Promise<Problem[]> {
type Page = {
problemsetQuestionListV2: {
totalLength: number;
questions: Array<{
questionFrontendId: string;
title: string;
titleSlug: string;
difficulty: keyof typeof DIFFICULTY;
paidOnly: boolean;
}>;
};
const res = await fetch("https://leetcode.com/api/problems/all/", {
headers: { "user-agent": "Mozilla/5.0" },
});
if (!res.ok) throw new Error(`GET /api/problems/all -> ${res.status} ${res.statusText}`);
const data = (await res.json()) as {
stat_status_pairs: Array<{
stat: {
frontend_question_id: number;
question__title: string;
question__title_slug: string;
};
difficulty: { level: 1 | 2 | 3 };
paid_only: boolean;
}>;
};
const page = async (skip: number) =>
(
await graphql<Page>(
`query problemIndex($skip: Int!, $limit: Int!) {
problemsetQuestionListV2(skip: $skip, limit: $limit) {
totalLength
questions { questionFrontendId title titleSlug difficulty paidOnly }
}
}`,
{ skip, limit: INDEX_PAGE },
{ referer: "https://leetcode.com/problemset/" },
)
).problemsetQuestionListV2;
// The first page names the total; the rest go six at a time.
const first = await page(0);
const questions = [...first.questions];
const skips = Array.from({ length: Math.ceil(first.totalLength / INDEX_PAGE) - 1 }, (_, i) => (i + 1) * INDEX_PAGE);
await Promise.all(
Array.from({ length: 6 }, async () => {
for (let skip = skips.shift(); skip !== undefined; skip = skips.shift()) {
questions.push(...(await page(skip)).questions);
}
}),
);
if (questions.length < first.totalLength) {
throw new Error(`problem index came back short: ${questions.length} of ${first.totalLength}`);
}
return questions
.map((q) => ({
id: Number(q.questionFrontendId),
title: q.title,
slug: q.titleSlug,
difficulty: DIFFICULTY[q.difficulty],
paidOnly: q.paidOnly,
return data.stat_status_pairs
.map((p) => ({
id: p.stat.frontend_question_id,
title: p.stat.question__title,
slug: p.stat.question__title_slug,
difficulty: DIFFICULTY[p.difficulty.level] as Problem["difficulty"],
paidOnly: p.paid_only,
}))
.filter((p) => Number.isInteger(p.id))
.sort((a, b) => a.id - b.id);
}
/** HTML problem statement, or null when LeetCode withholds it (paid-only). */
export async function fetchDescription(slug: string): Promise<string | null> {
const data = await graphql<{ question?: { content?: string | null } | null }>(
"query questionContent($titleSlug: String!) { question(titleSlug: $titleSlug) { content } }",
{ titleSlug: slug },
{ referer: `https://leetcode.com/problems/${slug}/` },
);
return data.question?.content ?? null;
}
/**
* The login leetcode-cli holds, read from where it keeps it: its own
* LEETCODE_SESSION + LEETCODE_CSRF_TOKEN override, else the OS keychain entry
* `leetcode login` writes through keytar (service `leetcode-cli`, account
* `auth`). Null when signed out, or when the login lives in leetcode-cli's
* encrypted-file fallback, whose key is the CLI's own.
*/
export function leetcodeCredentials(): Credentials | null {
const { LEETCODE_SESSION: session, LEETCODE_CSRF_TOKEN: csrfToken } = process.env;
if (session && csrfToken) return { session, csrfToken };
const lookup =
process.platform === "darwin"
? ["security", "find-generic-password", "-s", "leetcode-cli", "-a", "auth", "-w"]
: ["secret-tool", "lookup", "service", "leetcode-cli", "account", "auth"];
let stored: string;
try {
const out = Bun.spawnSync(lookup, { stderr: "ignore" });
if (!out.success) return null;
stored = out.stdout.toString();
} catch {
return null; // no keychain CLI on this machine
}
let parsed: unknown;
try {
parsed = JSON.parse(stored);
} catch {
return null;
}
if (
!parsed ||
typeof parsed !== "object" ||
!("session" in parsed) ||
typeof parsed.session !== "string" ||
!("csrfToken" in parsed) ||
typeof parsed.csrfToken !== "string"
) {
return null;
}
return { session: parsed.session, csrfToken: parsed.csrfToken };
}
/** One submission of the signed-in profile, any verdict. */
export interface Submission {
slug: string;
/** ISO timestamp. */
at: string;
}
/** Server-side cap on `limit` for the submission list. */
const SUBMISSION_PAGE = 20;
/** The signed-in profile's whole submission history, newest first. */
export async function fetchSubmissions(auth: Credentials): Promise<Submission[]> {
type Page = {
submissionList: { hasNext: boolean; submissions: Array<{ titleSlug: string; timestamp: string }> } | null;
};
const all: Submission[] = [];
for (let offset = 0; ; offset += SUBMISSION_PAGE) {
const { submissionList } = await graphql<Page>(
`query submissionList($offset: Int!, $limit: Int!) {
submissionList(offset: $offset, limit: $limit) {
hasNext
submissions { titleSlug timestamp }
}
}`,
{ offset, limit: SUBMISSION_PAGE },
{ referer: "https://leetcode.com/submissions/", auth },
);
// An expired session is answered with a null list, not an HTTP error.
if (!submissionList) throw new Error("submission history unavailable — session expired? run `leetcode login`");
for (const s of submissionList.submissions) {
all.push({ slug: s.titleSlug, at: new Date(Number(s.timestamp) * 1000).toISOString() });
}
if (!submissionList.hasNext) return all;
}
const res = await fetch("https://leetcode.com/graphql", {
method: "POST",
headers: {
"content-type": "application/json",
"user-agent": "Mozilla/5.0",
referer: `https://leetcode.com/problems/${slug}/`,
},
body: JSON.stringify({
query: "query questionContent($titleSlug: String!) { question(titleSlug: $titleSlug) { content } }",
variables: { titleSlug: slug },
}),
});
if (!res.ok) throw new Error(`POST /graphql (${slug}) -> ${res.status} ${res.statusText}`);
const data = (await res.json()) as { data?: { question?: { content?: string | null } | null } };
return data.data?.question?.content ?? null;
}
// ── the cache ────────────────────────────────────────────────────
@@ -292,28 +149,14 @@ export function problemDb(path = DB_PATH) {
const descState = db.query<{ paid_only: number; described: number }, [string]>(
"SELECT paid_only, description IS NOT NULL AS described FROM problems WHERE slug = ?1",
);
// MIN/MAX on TEXT compare ISO timestamps chronologically; COALESCE because
// SQLite's scalar MAX returns NULL when any argument is NULL.
const fold = db.query<never, [number, string, string]>(
`UPDATE solutions SET
created_at = MIN(created_at, ?2),
last_tested = MAX(COALESCE(last_tested, ''), ?3),
last_submitted = MAX(COALESCE(last_submitted, ''), ?3)
WHERE id = ?1`,
);
/** True when `key` has never been stamped or was stamped over 24h ago. */
const stale = (key: string) => {
const row = getMeta.get(key);
return !row || Date.now() - Number(row.value) > TTL_MS;
};
return {
path,
/** True when the index has never been fetched or is older than 24h. */
indexStale(): boolean {
return stale("index_fetched_at");
const row = getMeta.get("index_fetched_at");
return !row || Date.now() - Number(row.value) > INDEX_TTL_MS;
},
/** Pull the full index from leetcode.com and upsert it. Returns the count. */
@@ -385,27 +228,6 @@ export function problemDb(path = DB_PATH) {
).run(id, new Date().toISOString());
},
/** True when submission history has never been folded in or over 24h ago. */
historyStale(): boolean {
return stale("history_fetched_at");
},
/**
* Fold the profile's submissions (LC number → first and latest submission,
* ISO) into existing solution rows and stamp the refresh. Rows are never
* created here — the table holds work/ files only — and a timestamp only
* ever moves outward, so folding twice changes nothing. Returns the number
* of solutions that have submissions.
*/
foldHistory(history: Map<number, { first: string; last: string }>): number {
let matched = 0;
db.transaction(() => {
for (const [id, { first, last }] of history) matched += fold.run(id, first, last).changes;
setMeta.run("history_fetched_at", String(Date.now()));
})();
return matched;
},
close(): void {
db.close();
},
+3 -35
View File
@@ -9,9 +9,7 @@
*
* Every run backfills the db's solutions table with created_at (from the
* file's birthtime, mtime when the filesystem has none) so ordering works for
* files that predate the db, and at most once a day folds in the signed-in
* LeetCode profile's submission history (see db.ts), which survives a lost db
* or a fresh clone. Files git reports as modified, added or
* files that predate the db. Files git reports as modified, added or
* untracked come first, newest edit first — the problem being worked on is
* the focused option, so Enter runs it without a search. The rest are ordered
* per command — test lists the most recently tested first (never-tested
@@ -21,10 +19,10 @@
* Library: side-effect-free on import — nothing scans the filesystem until
* runOnSolution() is called.
*/
import { autocomplete, cancel, isCancel, log, spinner } from "@clack/prompts";
import { autocomplete, cancel, isCancel, log } from "@clack/prompts";
import { statSync } from "node:fs";
import { basename, join, relative } from "node:path";
import { fetchSubmissions, leetcodeCredentials, problemDb } from "./db";
import { problemDb } from "./db";
import { ROOT, WORK as WORK_DIR, parseWorkPath } from "./work";
/** Leading LC number of a work/ file, or undefined for unnumbered names. */
@@ -79,36 +77,6 @@ export async function runOnSolution(
new Date(stat.birthtimeMs || stat.mtimeMs).toISOString(),
);
}
// the profile's history: first and latest submission per work/ problem
if (db.historyStale()) {
const auth = leetcodeCredentials();
if (!auth) {
log.warn("Not signed in to leetcode-cli — run `leetcode login` to restore history from your profile.");
} else {
const s = spinner();
s.start("Syncing submission history from your LeetCode profile");
try {
const bySlug = new Map<string, number>();
for (const f of scanned) {
const parsed = parseWorkPath(f);
if (parsed) bySlug.set(parsed.slug, parsed.lc);
}
const history = new Map<number, { first: string; last: string }>();
for (const { slug, at } of await fetchSubmissions(auth)) {
const id = bySlug.get(slug);
if (id === undefined) continue;
const seen = history.get(id);
if (!seen) history.set(id, { first: at, last: at });
else if (at < seen.first) seen.first = at;
else if (at > seen.last) seen.last = at;
}
s.stop(`Synced submission history for ${db.foldHistory(history)} solutions`);
} catch (err) {
s.error(`Submission history sync failed (${err}) — using cached timestamps`);
}
}
}
const activity = db.solutionActivity();
// test → most recently tested first; submit → most recently created first
+137
View File
@@ -0,0 +1,137 @@
// Written by `blume eject`. This is your Astro config now: edit it freely.
import { availableParallelism } from "node:os";
import { fileURLToPath } from "node:url";
import { defineConfig, fontProviders } from "astro/config";
import mdx from "@astrojs/mdx";
import tailwindcss from "@tailwindcss/vite";
import { blumeMarkdownProcessor, blumeMdxProcessor, blumeShikiTransformers, blumeTwoslashTransformer } from "blume/markdown";
import react from "@astrojs/react";
import { blumeIntegration, includeHmrPlugin, mdxSourceErrorsPlugin, prerenderDepsPlugin } from "blume/astro";
export default defineConfig({
root: ".",
srcDir: "./src",
outDir: "./dist",
publicDir: "./public",
output: "static",
site: "https://lc.prd.lk",
redirects: {"/1":{"destination":"/hash-table/two-sum","status":301},"/1.md":{"destination":"/hash-table/two-sum.md","status":301},"/1.mdx":{"destination":"/hash-table/two-sum.mdx","status":301},"/2":{"destination":"/hybrid-structures/add-two-numbers","status":301},"/2.md":{"destination":"/hybrid-structures/add-two-numbers.md","status":301},"/2.mdx":{"destination":"/hybrid-structures/add-two-numbers.mdx","status":301},"/3":{"destination":"/sliding-window/longest-substring-without-repeating-characters","status":301},"/3.md":{"destination":"/sliding-window/longest-substring-without-repeating-characters.md","status":301},"/3.mdx":{"destination":"/sliding-window/longest-substring-without-repeating-characters.mdx","status":301},"/4":{"destination":"/binary-search/median-of-two-sorted-arrays","status":301},"/4.md":{"destination":"/binary-search/median-of-two-sorted-arrays.md","status":301},"/4.mdx":{"destination":"/binary-search/median-of-two-sorted-arrays.mdx","status":301},"/11":{"destination":"/two-pointers/container-with-most-water","status":301},"/11.md":{"destination":"/two-pointers/container-with-most-water.md","status":301},"/11.mdx":{"destination":"/two-pointers/container-with-most-water.mdx","status":301},"/15":{"destination":"/two-pointers/3sum","status":301},"/15.md":{"destination":"/two-pointers/3sum.md","status":301},"/15.mdx":{"destination":"/two-pointers/3sum.mdx","status":301},"/19":{"destination":"/linked-list/remove-nth-node-from-end-of-list","status":301},"/19.md":{"destination":"/linked-list/remove-nth-node-from-end-of-list.md","status":301},"/19.mdx":{"destination":"/linked-list/remove-nth-node-from-end-of-list.mdx","status":301},"/20":{"destination":"/stack/valid-parentheses","status":301},"/20.md":{"destination":"/stack/valid-parentheses.md","status":301},"/20.mdx":{"destination":"/stack/valid-parentheses.mdx","status":301},"/21":{"destination":"/linked-list/merge-two-sorted-lists","status":301},"/21.md":{"destination":"/linked-list/merge-two-sorted-lists.md","status":301},"/21.mdx":{"destination":"/linked-list/merge-two-sorted-lists.mdx","status":301},"/23":{"destination":"/heap-priority-queue/merge-k-sorted-lists","status":301},"/23.md":{"destination":"/heap-priority-queue/merge-k-sorted-lists.md","status":301},"/23.mdx":{"destination":"/heap-priority-queue/merge-k-sorted-lists.mdx","status":301},"/33":{"destination":"/binary-search/search-in-rotated-sorted-array","status":301},"/33.md":{"destination":"/binary-search/search-in-rotated-sorted-array.md","status":301},"/33.mdx":{"destination":"/binary-search/search-in-rotated-sorted-array.mdx","status":301},"/34":{"destination":"/binary-search/find-first-and-last-position-of-element-in-sorted-array","status":301},"/34.md":{"destination":"/binary-search/find-first-and-last-position-of-element-in-sorted-array.md","status":301},"/34.mdx":{"destination":"/binary-search/find-first-and-last-position-of-element-in-sorted-array.mdx","status":301},"/35":{"destination":"/binary-search/search-insert-position","status":301},"/35.md":{"destination":"/binary-search/search-insert-position.md","status":301},"/35.mdx":{"destination":"/binary-search/search-insert-position.mdx","status":301},"/36":{"destination":"/matrix-index-math/valid-sudoku","status":301},"/36.md":{"destination":"/matrix-index-math/valid-sudoku.md","status":301},"/36.mdx":{"destination":"/matrix-index-math/valid-sudoku.mdx","status":301},"/39":{"destination":"/backtracking/combination-sum","status":301},"/39.md":{"destination":"/backtracking/combination-sum.md","status":301},"/39.mdx":{"destination":"/backtracking/combination-sum.mdx","status":301},"/42":{"destination":"/two-pointers/trapping-rain-water","status":301},"/42.md":{"destination":"/two-pointers/trapping-rain-water.md","status":301},"/42.mdx":{"destination":"/two-pointers/trapping-rain-water.mdx","status":301},"/46":{"destination":"/backtracking/permutations","status":301},"/46.md":{"destination":"/backtracking/permutations.md","status":301},"/46.mdx":{"destination":"/backtracking/permutations.mdx","status":301},"/48":{"destination":"/matrix-index-math/rotate-image","status":301},"/48.md":{"destination":"/matrix-index-math/rotate-image.md","status":301},"/48.mdx":{"destination":"/matrix-index-math/rotate-image.mdx","status":301},"/49":{"destination":"/hash-table/group-anagrams","status":301},"/49.md":{"destination":"/hash-table/group-anagrams.md","status":301},"/49.mdx":{"destination":"/hash-table/group-anagrams.mdx","status":301},"/53":{"destination":"/greedy/maximum-subarray","status":301},"/53.md":{"destination":"/greedy/maximum-subarray.md","status":301},"/53.mdx":{"destination":"/greedy/maximum-subarray.mdx","status":301},"/54":{"destination":"/matrix-index-math/spiral-matrix","status":301},"/54.md":{"destination":"/matrix-index-math/spiral-matrix.md","status":301},"/54.mdx":{"destination":"/matrix-index-math/spiral-matrix.mdx","status":301},"/55":{"destination":"/greedy/jump-game","status":301},"/55.md":{"destination":"Line truncated
fonts: [{ provider: fontProviders.local(), name: "Inter", cssVariable: "--blume-ff-inter", fallbacks: ["ui-sans-serif","system-ui","sans-serif"], options: { variants: [{"weight":"100..900","src":["node_modules/@fontsource-variable/inter/files/inter-latin-wght-normal.woff2"]}] } }, { provider: fontProviders.local(), name: "IBM Plex Mono", cssVariable: "--blume-ff-ibm-plex-mono", fallbacks: ["ui-monospace","SF Mono","Menlo","monospace"], options: { variants: [{"weight":400,"src":["node_modules/@fontsource/ibm-plex-mono/files/ibm-plex-mono-latin-400-normal.woff2"]},{"weight":500,"src":["node_modules/@fontsource/ibm-plex-mono/files/ibm-plex-mono-latin-500-normal.woff2"]}] } }],
integrations: [mdx({ processor: blumeMdxProcessor({ ...{"basePath":"","codeThemes":{"dark":"github-dark","light":"github-light"},"contentRoot":"content","deployBase":"","externalLinks":false,"headingAnchors":true,"variables":{}}, dataFile: fileURLToPath(new URL("./src/generated/data.json", import.meta.url)) }) }), react({ exclude: [/\/\.cache\/vite\//] }), blumeIntegration({"pages":[{"entrypoint":"pages/appendix.astro","pattern":"/appendix"},{"entrypoint":"pages/changelog.astro","pattern":"/changelog"},{"entrypoint":"pages/progress.astro","pattern":"/progress"}],"poweredBy":true,"redirects":[["^/1/?$","/hash-table/two-sum",301],["^/1\\.md/?$","/hash-table/two-sum.md",301],["^/1\\.mdx/?$","/hash-table/two-sum.mdx",301],["^/2/?$","/hybrid-structures/add-two-numbers",301],["^/2\\.md/?$","/hybrid-structures/add-two-numbers.md",301],["^/2\\.mdx/?$","/hybrid-structures/add-two-numbers.mdx",301],["^/3/?$","/sliding-window/longest-substring-without-repeating-characters",301],["^/3\\.md/?$","/sliding-window/longest-substring-without-repeating-characters.md",301],["^/3\\.mdx/?$","/sliding-window/longest-substring-without-repeating-characters.mdx",301],["^/4/?$","/binary-search/median-of-two-sorted-arrays",301],["^/4\\.md/?$","/binary-search/median-of-two-sorted-arrays.md",301],["^/4\\.mdx/?$","/binary-search/median-of-two-sorted-arrays.mdx",301],["^/11/?$","/two-pointers/container-with-most-water",301],["^/11\\.md/?$","/two-pointers/container-with-most-water.md",301],["^/11\\.mdx/?$","/two-pointers/container-with-most-water.mdx",301],["^/15/?$","/two-pointers/3sum",301],["^/15\\.md/?$","/two-pointers/3sum.md",301],["^/15\\.mdx/?$","/two-pointers/3sum.mdx",301],["^/19/?$","/linked-list/remove-nth-node-from-end-of-list",301],["^/19\\.md/?$","/linked-list/remove-nth-node-from-end-of-list.md",301],["^/19\\.mdx/?$","/linked-list/remove-nth-node-from-end-of-list.mdx",301],["^/20/?$","/stack/valid-parentheses",301],["^/20\\.md/?$","/stack/valid-parentheses.md",301],["^/20\\.mdx/?$","/stack/valid-parentheses.mdx",301],["^/21/?$","/linked-list/merge-two-sorted-lists",301],["^/21\\.md/?$","/linked-list/merge-two-sorted-lists.md",301],["^/21\\.mdx/?$","/linked-list/merge-two-sorted-lists.mdx",301],["^/23/?$","/heap-priority-queue/merge-k-sorted-lists",301],["^/23\\.md/?$","/heap-priority-queue/merge-k-sorted-lists.md",301],["^/23\\.mdx/?$","/heap-priority-queue/merge-k-sorted-lists.mdx",301],["^/33/?$","/binary-search/search-in-rotated-sorted-array",301],["^/33\\.md/?$","/binary-search/search-in-rotated-sorted-array.md",301],["^/33\\.mdx/?$","/binary-search/search-in-rotated-sorted-array.mdx",301],["^/34/?$","/binary-search/find-first-and-last-position-of-element-in-sorted-array",301],["^/34\\.md/?$","/binary-search/find-first-and-last-position-of-element-in-sorted-array.md",301],["^/34\\.mdx/?$","/binary-search/find-first-and-last-position-of-element-in-sorted-array.mdx",301],["^/35/?$","/binary-search/search-insert-position",301],["^/35\\.md/?$","/binary-search/search-insert-position.md",301],["^/35\\.mdx/?$","/binary-search/search-insert-position.mdx",301],["^/36/?$","/matrix-index-math/valid-sudoku",301],["^/36\\.md/?$","/matrix-index-math/valid-sudoku.md",301],["^/36\\.mdx/?$","/matrix-index-math/valid-sudoku.mdx",301],["^/39/?$","/backtracking/combination-sum",301],["^/39\\.md/?$","/backtracking/combination-sum.md",301],["^/39\\.mdx/?$","/backtracking/combination-sum.mdx",301],["^/42/?$","/two-pointers/trapping-rain-water",301],["^/42\\.md/?$","/two-pointers/trapping-rain-water.md",301],["^/42\\.mdx/?$","/two-pointers/trapping-rain-water.mdx",301],["^/46/?$","/backtracking/permutations",301],["^/46\\.md/?$","/backtracking/permutations.md",301],["^/46\\.mdx/?$","/backtracking/permutations.mdx",301],["^/48/?$","/matrix-index-math/rotate-image",301],["^/48\\.md/?$","/matrix-index-math/rotate-image.md",301],["^/48\\.mdx/?$","/matrix-index-math/rotate-image.mdx",301],["^/49/?$","/hash-table/group-anagrams",301],["^/49\\.md/?$","/hash-table/group-anagrams.md",301],["^/49\\.mdx/?$","/hash-table/group-anagrams.mdx",301],["^/53/?$","/greedy/maximum-subarray",301],["^/53\\.md/?$","/greedy/maximum-subarray.md",301],["^/53\\.mdx/?$","/greedy/maximum-subarray.mdx",301],["^/54/?$","/matrix-index-math/spiral-matrix",301],["^/54\\.md/?$","/matrix-index-math/spiral-matrix.md",301],["^/54\\.mdx/?$","/matrix-index-math/spiral-matrix.mdx",301],["^/55/?$","/greedy/jump-game",301],["^/55\\.md/?$","/greedy/jump-game.md",301],["^/55\\.mdx/?$","/greedy/jump-game.mdx",301],["^/56/?$","/intervals/merge-intervals",301],["^/56\\.md/?$","/intervals/merge-intervals.md",301],["^/56\\.mdx/?$","/intervals/merge-intervals.mdx",301],["^/57/?$","/intervals/insert-interval",301],["^/57\\.md/?$","/intervals/insert-interval.md",301],["^/57\\.mdx/?$","/intervals/insert-interval.mdx",301],["^/62/?$","/multi-d-grid-dp/unique-paths",301],["^/62\\.md/?$","/multi-d-grid-dp/unique-paths.md",301],["^/62\\.mdx/?$","/multi-d-grid-dp/unique-paths.mdx",301],["^/64/?$","Line truncated
markdown: {
processor: blumeMarkdownProcessor({ ...{"basePath":"","codeThemes":{"dark":"github-dark","light":"github-light"},"contentRoot":"content","deployBase":"","externalLinks":false,"headingAnchors":true,"variables":{}}, dataFile: fileURLToPath(new URL("./src/generated/data.json", import.meta.url)) }),
shikiConfig: {
themes: {
light: "github-light",
dark: "github-dark",
},
defaultColor: false,
transformers: [blumeTwoslashTransformer(), ...blumeShikiTransformers({"icons":true})],
},
},
devToolbar: { enabled: false },
// One canonical URL per page: canonicals, the sitemap, and hreflang all use
// the slashless form, so the slashed spelling is not a second address. Astro
// applies this itself — an on-demand route redirects, and the integration
// has the dev server redirect too, ahead of Astro's own 404 — and the Vercel
// adapter turns it into the platform's 308 route, so the Build Output config
// needs no hand-spliced redirect (see deploy/vercel-negotiation.ts). Static hosts
// serve the `index.html` directory layout as they always did.
trailingSlash: "never",
// The layouts render Astro's <ClientRouter />, and its in-place swaps read
// from the prefetch cache — fetching every link on hover/viewport hides the
// request latency behind the user's intent, so most navigations swap
// instantly.
prefetch: { prefetchAll: true },
// Prerender several pages at once. Rendering is single-threaded, but a
// page's OG card renders on a native thread and its HTML is written
// asynchronously, so the main thread would otherwise idle behind each
// page's off-thread work. Capped at 8: the gain flattens there and beyond it
// the overlap only adds memory.
build: { concurrency: Math.min(8, availableParallelism()) },
vite: {
plugins: [tailwindcss(), includeHmrPlugin(fileURLToPath(new URL("./src/generated/includes.json", import.meta.url))), mdxSourceErrorsPlugin(), prerenderDepsPlugin()],
build: {
chunkSizeWarningLimit: 2048,
rolldownOptions: {
onLog(level, log, handler) {
if (log.code === "MODULE_LEVEL_DIRECTIVE" && String(log.message).includes("use astro:head-inject")) {
return;
}
handler(level, log);
},
},
},
// Everything hydration can reach must be part of the dev dep optimizer's
// FIRST run. The Vite root is the generated runtime, so user pages,
// islands, and aliased components live outside it and are only crawled
// when first requested — and `react/compiler-runtime` is injected by the
// React Compiler, so no source scan can ever see it. A dependency discovered after
// hydration begins triggers a mid-session re-optimization whose new
// generation imports React through new `?v=` URLs; the browser then
// evaluates a second React copy and every island tears down with
// "Invalid hook call" (#157). `entries` points the startup scanner at
// the user's files (the scanner follows their imports, so their deps land
// in the initial optimization); the compiler runtime rides the include
// list because only the transform pipeline knows it exists.
//
// The mermaid/epub includes fix CJS interop instead: both lazy client-side
// imports land on CJS/UMD files (mermaid statically imports dayjs as CJS,
// epub-gen-memory's browser bundle is a browserified UMD) that break when
// served as raw ESM — mermaid throws on load and the EPUB export throws
// `epub is not a function`. They resolve through the `blume` package
// (they aren't direct deps of the generated project), so the nested
// `blume > x` form is required, and epub-gen-memory must name the
// `/bundle` subpath that is actually imported: optimizing the package
// root leaves that entry out. Production (Rollup) already handles the
// interop, so all of this only affects dev.
//
// The search adapter's client library rides the list for the same reason
// as the compiler runtime: the search dialog lazy-imports it on first
// open, and discovering it then re-optimizes and reloads the page.
optimizeDeps: {
entries: ["./pages/**/*.astro","./islands/**/*.{jsx,svelte,tsx,vue}"],
include: ["blume > mermaid","blume > @orama/orama"],
},
// Blume's render-time deps are forced external on both build environments so
// native bindings resolve at runtime and isolated linkers don't bundle
// symlinked store copies (which would surface their children as unresolvable
// imports). See RENDER_EXTERNAL_DEPS / prerenderDepsPlugin.
//
// The SSR externals go through the legacy `ssr.external` key rather than
// `environments.ssr`: defining a user-owned `environments.ssr` block
// collides with the internal environment Astro 7 builds the server under and
// detaches the adapter's server entrypoint from the rolldown input, so the
// SSR entry is emitted as `index.mjs` instead of the `entry.mjs` the
// Vercel adapter's `astro:build:done` hook then fails to find. `prerender`
// is Astro-only and has no legacy equivalent, so it stays under `environments`.
ssr: { external: ["@astrojs/markdown-satteri","@pierre/diffs","@shikijs/transformers","@takumi-rs/core","@takumi-rs/helpers","@takumi-rs/wasm","github-slugger","katex","shiki","simple-icons","takumi-js","takumi-js/helpers","zod"] },
environments: {
prerender: { resolve: { external: ["@astrojs/markdown-satteri","@pierre/diffs","@shikijs/transformers","@takumi-rs/core","@takumi-rs/helpers","@takumi-rs/wasm","github-slugger","katex","shiki","simple-icons","takumi-js","takumi-js/helpers","zod"] } },
},
resolve: {
alias: {
"blume:ask": fileURLToPath(new URL("./src/generated/Ask.astro", import.meta.url)),
"blume:consent-client": fileURLToPath(new URL("./src/generated/consent-client.ts", import.meta.url)),
"blume:examples": fileURLToPath(new URL("./src/generated/examples.ts", import.meta.url)),
"blume:examples-theme": fileURLToPath(new URL("./src/generated/examples.css", import.meta.url)),
"blume:features": fileURLToPath(new URL("./src/generated/features.ts", import.meta.url)),
"blume:search-client": fileURLToPath(new URL("./src/generated/search-client.ts", import.meta.url)),
"blume:theme": fileURLToPath(new URL("./src/generated/app.css", import.meta.url)),
"blume:ask-data": fileURLToPath(new URL("./src/generated/ask-data.json", import.meta.url)),
"blume:content-assets": fileURLToPath(new URL("./src/generated/content-assets.json", import.meta.url)),
"blume:data": fileURLToPath(new URL("./src/generated/data.json", import.meta.url)),
"blume:mcp-data": fileURLToPath(new URL("./src/generated/mcp-data.json", import.meta.url)),
"blume:openapi": fileURLToPath(new URL("./src/generated/openapi.json", import.meta.url)),
"blume:raw-markdown": fileURLToPath(new URL("./src/generated/raw-markdown.json", import.meta.url)),
"blume:rss": fileURLToPath(new URL("./src/generated/rss.json", import.meta.url)),
"blume:search-index": fileURLToPath(new URL("./src/generated/search.json", import.meta.url)),
},
},
server: {
fs: {
allow: ["."],
},
},
},
});
+12 -4
View File
@@ -3,11 +3,18 @@
"private": true,
"type": "module",
"scripts": {
"dev": "blume dev",
"build": "blume build",
"doctor": "blume doctor"
"dev": "astro dev",
"build": "astro build",
"doctor": "blume doctor",
"preview": "astro preview"
},
"dependencies": {
"@astrojs/mdx": "^8.0.2",
"@astrojs/react": "^7.0.0",
"@orama/orama": "^3.1.18",
"@tailwindcss/typography": "^0.5.20",
"@tailwindcss/vite": "^4.3.3",
"astro": "^7.3.2",
"blume": "^2.1.2",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
@@ -16,7 +23,8 @@
"react": "^19.2.8",
"react-dom": "^19.2.8",
"recharts": "3.8.0",
"tailwind-merge": "^3.7.0"
"tailwind-merge": "^3.7.0",
"tailwindcss": "^4.3.3"
},
"devDependencies": {
"@fontsource-variable/inter": "^5.3.0",
+27
View File
@@ -0,0 +1,27 @@
// Generated by Blume. Do not edit.
import { defineCollection } from "astro:content";
import { glob } from "astro/loaders";
import { withIncludeRefresh } from "blume/astro";
import { pageCollectionSchema } from "blume/core/schema.ts";
// withIncludeRefresh keeps <include>-bearing pages fresh: plain .md entries
// are rendered at sync time and digest-cached on the page file alone, so a
// partial edit (or a warm-cache rebuild after one) would serve stale HTML.
//
// pageCollectionSchema is the scan's page front-matter schema, so entry.data
// is typed and normalized the same way; it passes custom keys through and
// falls back to defaults for a page the scan already dropped as invalid.
//
// Astro's content store re-parses an entry only when its file or this config
// changes, so naming the Blume version (2.2.4) here re-parses
// every entry after an upgrade: none keeps what an older schema produced.
const docs = defineCollection({
loader: withIncludeRefresh(glob({
pattern: ["**/*.{md,mdx}","!**/_*","!**/.*","!**/.blume-verify/**","!**/.cache/**","!**/.git/**","!**/.next/**","!**/.turbo/**","!**/.vercel/**","!**/dist/**","!**/node_modules/**"],
base: "content",
generateId: ({ entry }) => entry,
}), "./src/generated/includes.json"),
schema: pageCollectionSchema,
});
export const collections = { docs };
+15
View File
@@ -0,0 +1,15 @@
---
// Generated by Blume. Do not edit.
import Assistant from "blume/components/islands/Assistant.astro";
import data from "blume:data";
const { strings } = Astro.props;
---
<Assistant
captcha={data.config.assistant?.captcha ?? undefined}
endpoint={data.config.assistant?.endpoint ?? undefined}
strings={strings ?? data.ui.assistant}
suggestions={data.config.assistant?.suggestions ?? []}
support={data.config.assistant?.support ?? undefined}
/>
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,11 @@
---
// Generated by Blume. Do not edit.
import Component from "../../../islands/DependencySpine.tsx";
type Props = typeof Component extends (
props: infer P extends object,
...rest: never[]
) => unknown
? P
: Record<string, unknown>;
---
<Component client:visible {...Astro.props}><slot /></Component>
+9
View File
@@ -0,0 +1,9 @@
// Generated by Blume. Do not edit.
import type { ComponentOverride } from "blume/core/define-components.ts";
import __blumeSlot0 from "./component-slots/mdx-DependencySpine.astro";
import __blumeSlot1 from "../../components/Carousel.astro";
import __blumeSlot2 from "../../components/CarouselImage.astro";
import __blumeSlot3 from "../../components/ProblemBadges.astro";
import __blumeSlot4 from "../../components/BacklinksList.astro";
export const mdxComponents: Record<string, ComponentOverride> = { "DependencySpine": __blumeSlot0, "Carousel": __blumeSlot1, "CarouselImage": __blumeSlot2, "wrapper": __blumeSlot3 };
export const layoutOverrides: Record<string, ComponentOverride> = { "PageFooter": __blumeSlot4 };
@@ -0,0 +1,3 @@
// Generated by Blume. Do not edit.
// The consent adapter has no browser module, or consent is off.
export const startConsentClient = (): void => {};
@@ -0,0 +1 @@
{"public/hash-table-operations.svg":"public/hash-table-operations.svg","public/intervals_overlap-condition.svg":"public/intervals_overlap-condition.svg","public/intervals_merge-formula.svg":"public/intervals_merge-formula.svg","public/intervals_overlap-formula.svg":"public/intervals_overlap-formula.svg","public/prefix-sum_1.png":"public/prefix-sum_1.png","public/sliding-window-fixed-size.svg":"public/sliding-window-fixed-size.svg","public/sliding-window-flexible.svg":"public/sliding-window-flexible.svg","public/sliding-window-frequency.svg":"public/sliding-window-frequency.svg","public/stack-plates-metaphor.svg":"public/stack-plates-metaphor.svg","public/stack-operations-sequence.svg":"public/stack-operations-sequence.svg","public/topo_intro_1.png":"public/topo_intro_1.png","public/topo_intro_2.png":"public/topo_intro_2.png","public/two-pointers-decision-tree.svg":"public/two-pointers-decision-tree.svg","public/same-direction-compaction.svg":"public/same-direction-compaction.svg","public/same-direction-relative-speed.svg":"public/same-direction-relative-speed.svg","public/same-direction-fixed-gap.svg":"public/same-direction-fixed-gap.svg","public/opposite-direction-monotonic-sum.svg":"public/opposite-direction-monotonic-sum.svg","public/opposite-direction-symmetry.svg":"public/opposite-direction-symmetry.svg","public/opposite-direction-bottleneck.svg":"public/opposite-direction-bottleneck.svg"}
File diff suppressed because it is too large. Load diff
+154
View File
@@ -0,0 +1,154 @@
/* Generated by Blume. Do not edit. */
@import "tailwindcss";
/* Scan the project (and an out-of-root examples directory) for utility classes. */
@source "../../**/*.{astro,jsx,svelte,ts,tsx,vue}";
/* Dark mode is driven by data-theme on the <html> element. */
@custom-variant dark (&:where([data-theme="dark"], [data-theme="dark"] *));
:root {
color-scheme: light;
}
:root[data-theme="dark"] {
color-scheme: dark;
}
:root {
--blume-background: oklch(1 0 0);
--blume-background-image: none;
--blume-background-image-repeat: no-repeat;
--blume-background-image-size: cover;
--blume-foreground: oklch(0.145 0 0);
--blume-muted: oklch(0.965 0 0);
/* 5.28:1 on the page background. The headroom over 4.5:1 is the point: muted
text is 14px body copy routinely set on a tinted surface (callouts, badges,
panels), and each tint costs a few tenths. */
--blume-muted-foreground: oklch(0.53 0 0);
--blume-border: oklch(0.88 0.006 260 / 0.72);
--blume-accent: oklch(0.145 0 0);
--blume-accent-foreground: oklch(1 0 0);
--blume-action: var(--blume-accent);
--blume-action-foreground: var(--blume-accent-foreground);
--blume-code-background: oklch(0.99 0 0);
/* Shiki notation transformers: line/word highlight, diff add/remove, and
error/warning lines. */
--blume-code-highlight: oklch(0.55 0.16 255 / 0.1);
--blume-code-highlight-border: oklch(0.55 0.16 255 / 0.55);
--blume-code-add: oklch(0.72 0.16 150 / 0.16);
--blume-code-add-border: oklch(0.52 0.15 150 / 0.7);
--blume-code-remove: oklch(0.66 0.21 22 / 0.16);
--blume-code-remove-border: oklch(0.55 0.2 22 / 0.7);
--blume-code-error: oklch(0.66 0.21 22 / 0.16);
--blume-code-error-border: oklch(0.55 0.2 22 / 0.7);
--blume-code-warning: oklch(0.8 0.16 80 / 0.2);
--blume-code-warning-border: oklch(0.66 0.15 70 / 0.75);
--blume-code-word: oklch(0.55 0.16 255 / 0.16);
--blume-code-word-border: oklch(0.55 0.16 255 / 0.5);
--blume-radius: 0.75rem;
/* Max width of the prose content column (article, breadcrumb, TOC, feedback,
pagination, last-updated). Override to re-measure the whole column at once. */
--blume-content-width: 42rem;
/*
* Font tokens resolve through optional src variables that Astro's Fonts API
* populates (via theme.fonts). When unset they fall back to the system
* stacks, so the default look is unchanged. The display font defaults to the
* body font.
*/
--blume-font-body: var(
--blume-font-body-src,
ui-sans-serif,
system-ui,
-apple-system,
"Segoe UI",
Roboto,
Helvetica,
Arial,
sans-serif
);
--blume-font-mono: var(
--blume-font-mono-src,
ui-monospace,
"SF Mono",
"Cascadia Code",
"Source Code Pro",
Menlo,
Consolas,
monospace
);
--blume-font-display: var(--blume-font-display-src, var(--blume-font-body));
}
:root[data-theme="dark"] {
--blume-background: oklch(0.085 0 0);
--blume-background-image: none;
--blume-background-image-repeat: no-repeat;
--blume-background-image-size: cover;
--blume-foreground: oklch(0.96 0 0);
--blume-muted: oklch(0.16 0 0);
--blume-muted-foreground: oklch(0.68 0 0);
--blume-border: oklch(0.24 0 0 / 0.8);
--blume-accent: oklch(0.96 0 0);
--blume-accent-foreground: oklch(0.085 0 0);
--blume-action: var(--blume-accent);
--blume-action-foreground: var(--blume-accent-foreground);
--blume-code-background: oklch(0.12 0 0);
/* Brighter tints read better over the dark code surface. */
--blume-code-highlight: oklch(0.7 0.14 255 / 0.16);
--blume-code-highlight-border: oklch(0.7 0.14 255 / 0.6);
--blume-code-add: oklch(0.78 0.17 150 / 0.2);
--blume-code-add-border: oklch(0.72 0.16 150 / 0.7);
--blume-code-remove: oklch(0.72 0.21 22 / 0.22);
--blume-code-remove-border: oklch(0.7 0.2 22 / 0.7);
--blume-code-error: oklch(0.72 0.21 22 / 0.22);
--blume-code-error-border: oklch(0.7 0.2 22 / 0.7);
--blume-code-warning: oklch(0.82 0.16 80 / 0.18);
--blume-code-warning-border: oklch(0.82 0.16 80 / 0.7);
--blume-code-word: oklch(0.7 0.14 255 / 0.22);
--blume-code-word-border: oklch(0.7 0.14 255 / 0.55);
}
@theme inline {
--color-background: var(--blume-background);
--color-foreground: var(--blume-foreground);
--color-muted: var(--blume-muted);
--color-muted-foreground: var(--blume-muted-foreground);
--color-border: var(--blume-border);
--color-accent: var(--blume-accent);
--color-accent-foreground: var(--blume-accent-foreground);
--color-action: var(--blume-action);
--color-action-foreground: var(--blume-action-foreground);
--color-code: var(--blume-code-background);
--radius-blume: var(--blume-radius);
--container-content: var(--blume-content-width);
--font-sans: var(--blume-font-body);
--font-mono: var(--blume-font-mono);
--font-display: var(--blume-font-display);
}
/* Frame defaults: readable text in both modes, transparent so the docs pane's
surface shows through. Base layer, so any user/example CSS wins. */
@layer base {
body {
background: transparent;
color: var(--blume-foreground);
}
}
/* Token overrides: config first, then the configured examples css (highest
priority) — the place for shadcn variables and other component tokens. */
/* Generated by Blume from theme config. */
:root {
--blume-accent: oklch(0.55 0.16 250);
--blume-accent-foreground: oklch(1 0 0);
--blume-radius: 0.5rem;
}
:root[data-theme="dark"] {
--blume-accent: oklch(0.62 0.16 250);
--blume-accent-foreground: oklch(0.085 0 0);
}
+3
View File
@@ -0,0 +1,3 @@
// Generated by Blume. Do not edit.
export const examplesBase = "/blume-examples";
export const examples = {};
+11
View File
@@ -0,0 +1,11 @@
// Generated by Blume. Do not edit.
//
// The client features this site uses. A feature no page needs stays out of
// the module graph entirely: its loader is null and its library never enters
// the client bundle.
/** Registers the <blume-mermaid> element; null when no page has a mermaid fence. */
export const loadMermaid: (() => Promise<unknown>) | null = () => import("blume/components/content/mermaid-element.ts");
/** The EPUB generator's browser bundle; null when export.epub is off. */
export const loadEpub: (() => Promise<unknown>) | null = null;
+1
View File
@@ -0,0 +1 @@
{}
+1
View File
@@ -0,0 +1 @@
{"base":"","documents":[{"content":"1. Linear Structures Data is read in one direction, from the first item to the last. The current position is recorded at each step. All later phases use this skill. Hash Table A key maps to a value in constant time, which removes the need for a full scan. Prefix Sum A running total is stored at each index, so the sum of any range is one subtraction. Two Pointers Two indices move through the data from different positions and meet at the answer. Sliding Window A range with a start and an end moves across the data, and the contents are updated at each step. Binary Search Half of a sorted range is removed at each step until the target is found. Stack Items are added and removed from one end, so the last item added is the first item removed. Monotonic Stack A stack keeps its items in sorted order, which finds the next larger or smaller item in one pass. 2. Nodal & Grid The data is in separate parts. Each part points to the next part, or sits next to other parts on a flat surface. An index does not find the next item. The pointers or the neighbors give the path instead. Linked List Each node points to the next node, and the pointers give the path through the data. Hybrid Structures Two structures are combined, such as a hash map and a linked list, to get the best properties of each. Matrix Index Math A two-dimensional grid is read as a flat array through a formula, and the neighbors are found by arithmetic. 3. Hierarchical Each part points to one or more parts below it. Those parts do the same. The method that solves a small part also solves the large part. This is recursion. Tree DFS Each branch is followed to its end before the next branch is examined. Tree BFS All nodes at one level are visited before the nodes at the next level. Binary Search Tree Each node holds a value that is larger than its left subtree and smaller than its right subtree. Heap The largest or smallest item is always at the top, and it is removed in logarithmic time. Trie Each node holds one character, and a path from the root spells a word. 4. Relational Each part can point to any other part. Loops can occur. The method must visit each part one time only. The order of the parts, or the distance between them, is not given in the data. The method finds it. Graph DFS Each edge is followed to its end, and a visited set prevents a repeat visit. Graph BFS Nodes are visited in order of distance from the start, which gives the shortest path in an unweighted graph. Topological Sort Nodes are put in an order where each node comes after all nodes that point to it. Union-Find Two sets are merged and the set of any item is found in near-constant time. 5. Decision Space The data has no structure. The method makes one. Each choice makes a new branch. The problem sets the correct method: examine all branches, keep a record of the results already found, or make one choice and do not go back. Backtracking Each choice makes a branch, and a branch is undone when it does not give a valid result. 1-D DP The result for each state is stored in a list, and each new result is computed from the results before it. Multi-D / Grid DP The result for each state is stored in a table with two or more dimensions, and each cell is computed from its neighbors. Greedy The best local choice is made at each step and is not changed later. Intervals Ranges with a start and an end are sorted, and then merged, counted, or scheduled.","contentType":"doc","description":"Structured dependency spine based roadmap for solving DSA Problems on leetcode.","locale":"","route":"/","title":"Introduction","version":""},{"content":"","contentType":"doc","description":"The result for each state is stored in a list, and each new result is computed from the results before it.","locale":"","route":"/1d-dynamic-programming","title":"1-D Dynamic Programming","version":""},{"content":"Example 1: Input: n = 2 Output: 2 Explanation: There are two ways to climb to the top. 1. 1 step + 1 step 2. 2 steps Example 2: Input: n = 3 Output: 3 Explanation: There are three ways to climb to the top. 1. 1 step + 1 step + 1 step 2. 1 step + 2 steps 3. 2 steps + 1 step Constraints: 1 <= n <= 45 Solution","contentType":"doc","description":"You are climbing a staircase. It takes n steps to reach the top","locale":"","route":"/1d-dynamic-programming/climbing-stairs","title":"70. Climbing Stairs","version":""},{"content":"Example 1: Input: coins = [1,2,5], amount = 11 Output: 3 Explanation: 11 = 5 + 5 + 1 Example 2: Input: coins = [2], amount = 3 Output: -1 Example 3: Input: coins = [1], amount = 0 Output: 0 Constraints: 1 <= coins.length <= 12 1 <= coins[i] <= 2^31 - 1 0 <= amount <= 10^4 Solution","contentType":"doc","description":"You are given an integer array coins representing coins of different denominations and an integer amount representing a total amount of money","locale":"","route":"/1d-dynamic-programming/coin-change","title":"322. Coin Change","version":""},{"content":"ExLine truncated
+1
View File
@@ -0,0 +1 @@
{}
@@ -0,0 +1 @@
{"/":{"md":"---\ntitle: Introduction\ndescription: Structured dependency spine based roadmap for solving DSA Problems on leetcode.\nicon: play\n---\n\n## 1. Linear Structures\n\n:::tip\nData is read in one direction, from the first item to the last. The current position is recorded at each step. All later phases use this skill.\n:::\n\n**[Hash Table](/hash-table)**\n\nA key maps to a value in constant time, which removes the need for a full scan.\n\n**[Prefix Sum](/prefix-sum)**\n\nA running total is stored at each index, so the sum of any range is one subtraction.\n\n**[Two Pointers](/two-pointers)**\n\nTwo indices move through the data from different positions and meet at the answer.\n\n**[Sliding Window](/sliding-window)**\n\nA range with a start and an end moves across the data, and the contents are updated at each step.\n\n**[Binary Search](/binary-search)**\n\nHalf of a sorted range is removed at each step until the target is found.\n\n**[Stack](/stack)**\n\nItems are added and removed from one end, so the last item added is the first item removed.\n\n**[Monotonic Stack](/monotonic-stack)**\n\nA stack keeps its items in sorted order, which finds the next larger or smaller item in one pass.\n\n## 2. Nodal & Grid\n\n:::tip\nThe data is in separate parts. Each part points to the next part, or sits next to other parts on a flat surface. An index does not find the next item. The pointers or the neighbors give the path instead.\n:::\n\n**[Linked List](/linked-list)**\n\nEach node points to the next node, and the pointers give the path through the data.\n\n**[Hybrid Structures](/hybrid-structures)**\n\nTwo structures are combined, such as a hash map and a linked list, to get the best properties of each.\n\n**[Matrix Index Math](/matrix-index-math)**\n\nA two-dimensional grid is read as a flat array through a formula, and the neighbors are found by arithmetic.\n\n## 3. Hierarchical\n\n:::tip\nEach part points to one or more parts below it. Those parts do the same. The method that solves a small part also solves the large part. This is recursion.\n:::\n\n**[Tree DFS](/tree-dfs)**\n\nEach branch is followed to its end before the next branch is examined.\n\n**[Tree BFS](/tree-bfs)**\n\nAll nodes at one level are visited before the nodes at the next level.\n\n**[Binary Search Tree](/binary-search-tree)**\n\nEach node holds a value that is larger than its left subtree and smaller than its right subtree.\n\n**[Heap](/heap-priority-queue)**\n\nThe largest or smallest item is always at the top, and it is removed in logarithmic time.\n\n**[Trie](/trie)**\n\nEach node holds one character, and a path from the root spells a word.\n\n## 4. Relational\n\n:::tip\nEach part can point to any other part. Loops can occur. The method must visit each part one time only. The order of the parts, or the distance between them, is not given in the data. The method finds it.\n:::\n\n**[Graph DFS](/graph-dfs)**\n\nEach edge is followed to its end, and a visited set prevents a repeat visit.\n\n**[Graph BFS](/graph-bfs)**\n\nNodes are visited in order of distance from the start, which gives the shortest path in an unweighted graph.\n\n**[Topological Sort](/topological-sort)**\n\nNodes are put in an order where each node comes after all nodes that point to it.\n\n**[Union-Find](/union-find)**\n\nTwo sets are merged and the set of any item is found in near-constant time.\n\n## 5. Decision Space\n\n:::tip\nThe data has no structure. The method makes one. Each choice makes a new branch. The problem sets the correct method: examine all branches, keep a record of the results already found, or make one choice and do not go back.\n:::\n\n**[Backtracking](/backtracking)**\n\nEach choice makes a branch, and a branch is undone when it does not give a valid result.\n\n**[1-D DP](/1d-dynamic-programming)**\n\nThe result for each state is stored in a list, and each new result is computed from the results before it.\n\n**[Multi-D / Grid DP](/multi-d-grid-dp)**\n\nThe result for each state is stored in a table with two or more dimensions, and each cell is computed from its neighbors.\n\n**[Greedy](/greedy)**\n\nThe best local choice is made at each step and is not changed later.\n\n**[Intervals](/intervals)**\n\nRanges with a start and an end are sorted, and then merged, counted, or scheduled.\n","mdx":"---\ntitle: Introduction\ndescription: Structured dependency spine based roadmap for solving DSA Problems on leetcode.\nicon: play\n---\n\n## 1. Linear Structures\n\n:::tip\nData is read in one direction, from the first item to the last. The current position is recorded at each step. All later phases use this skill.\n:::\n\n<Columns cols={2}>\n <Column>\n <Tile\n title=\"Hash Table\"\n description=\"A key maps to a value in constant time, which removes the need for a full scan.\"\n href=\"/hash-table\"\n >\n <Icon icon=\"hash\" size={28} />\n </Tile>\n </Column>\n <Column>\n <Tile\n title=\"Prefix Sum\"\n descLine truncated
+7
View File
@@ -0,0 +1,7 @@
// Generated by Blume. Do not edit.
import { createSearch as create } from "blume/components/layout/search/orama.ts";
import { joinBase } from "blume/components/islands/base-path.ts";
const indexUrl = joinBase(import.meta.env.BASE_URL, "blume-search.json");
export const createSearch = () => create({ indexUrl });
+1
View File
@@ -0,0 +1 @@
[{"breadcrumb":[],"content":"1. Linear Structures Data is read in one direction, from the first item to the last. The current position is recorded at each step. All later phases use this skill. Hash Table A key maps to a value in constant time, which removes the need for a full scan. Prefix Sum A running total is stored at each index, so the sum of any range is one subtraction. Two Pointers Two indices move through the data from different positions and meet at the answer. Sliding Window A range with a start and an end moves across the data, and the contents are updated at each step. Binary Search Half of a sorted range is removed at each step until the target is found. Stack Items are added and removed from one end, so the last item added is the first item removed. Monotonic Stack A stack keeps its items in sorted order, which finds the next larger or smaller item in one pass. 2. Nodal & Grid The data is in separate parts. Each part points to the next part, or sits next to other parts on a flat surface. An index does not find the next item. The pointers or the neighbors give the path instead. Linked List Each node points to the next node, and the pointers give the path through the data. Hybrid Structures Two structures are combined, such as a hash map and a linked list, to get the best properties of each. Matrix Index Math A two-dimensional grid is read as a flat array through a formula, and the neighbors are found by arithmetic. 3. Hierarchical Each part points to one or more parts below it. Those parts do the same. The method that solves a small part also solves the large part. This is recursion. Tree DFS Each branch is followed to its end before the next branch is examined. Tree BFS All nodes at one level are visited before the nodes at the next level. Binary Search Tree Each node holds a value that is larger than its left subtree and smaller than its right subtree. Heap The largest or smallest item is always at the top, and it is removed in logarithmic time. Trie Each node holds one character, and a path from the root spells a word. 4. Relational Each part can point to any other part. Loops can occur. The method must visit each part one time only. The order of the parts, or the distance between them, is not given in the data. The method finds it. Graph DFS Each edge is followed to its end, and a visited set prevents a repeat visit. Graph BFS Nodes are visited in order of distance from the start, which gives the shortest path in an unweighted graph. Topological Sort Nodes are put in an order where each node comes after all nodes that point to it. Union-Find Two sets are merged and the set of any item is found in near-constant time. 5. Decision Space The data has no structure. The method makes one. Each choice makes a new branch. The problem sets the correct method: examine all branches, keep a record of the results already found, or make one choice and do not go back. Backtracking Each choice makes a branch, and a branch is undone when it does not give a valid result. 1-D DP The result for each state is stored in a list, and each new result is computed from the results before it. Multi-D / Grid DP The result for each state is stored in a table with two or more dimensions, and each cell is computed from its neighbors. Greedy The best local choice is made at each step and is not changed later. Intervals Ranges with a start and an end are sorted, and then merged, counted, or scheduled.","contentType":"doc","description":"Structured dependency spine based roadmap for solving DSA Problems on leetcode.","locale":"","route":"/","section":"Docs","title":"Introduction","version":""},{"breadcrumb":["Decision Space","1-D Dynamic Programming"],"content":"","contentType":"doc","description":"The result for each state is stored in a list, and each new result is computed from the results before it.","locale":"","route":"/1d-dynamic-programming","section":"1-D Dynamic Programming","title":"1-D Dynamic Programming","version":""},{"breadcrumb":["Decision Space","1-D Dynamic Programming"],"content":"Example 1: Input: n = 2 Output: 2 Explanation: There are two ways to climb to the top. 1. 1 step + 1 step 2. 2 steps Example 2: Input: n = 3 Output: 3 Explanation: There are three ways to climb to the top. 1. 1 step + 1 step + 1 step 2. 1 step + 2 steps 3. 2 steps + 1 step Constraints: 1 <= n <= 45 Solution","contentType":"doc","description":"You are climbing a staircase. It takes n steps to reach the top","locale":"","route":"/1d-dynamic-programming/climbing-stairs","section":"1-D Dynamic Programming","title":"70. Climbing Stairs","version":""},{"breadcrumb":["Decision Space","1-D Dynamic Programming"],"content":"Example 1: Input: coins = [1,2,5], amount = 11 Output: 3 Explanation: 11 = 5 + 5 + 1 Example 2: Input: coins = [2], amount = 3 Output: -1 Example 3: Input: coins = [1], amount = 0 Output: 0 Constraints: 1 <= coins.length <= 12 1 <= coins[i] <= 2^31 - 1 0 <= amount <= 10^4 Solution","contentType":"doc","description":"Line truncated
+176
View File
@@ -0,0 +1,176 @@
---
// Generated by Blume. Do not edit. Override by adding `pages/404.astro`.
import Icon from "blume/components/Icon.astro";
import PageLayout from "blume/components/layout/PageLayout.astro";
import { withMountedBase } from "blume/components/islands/base-path.ts";
import { mountBasePath } from "blume/core/base-path.ts";
import data from "blume:data";
export const prerender = true;
const nf = data.ui.notFound;
// The 404 page is an unlocalized route, so its chrome renders in the default
// locale's dictionary and direction (`data.ui` is the default locale's resolved
// dictionary), mirroring the catch-all's locale wiring.
const i18n = data.config.i18n;
const localeMeta = i18n
? i18n.locales.find((l) => l.code === i18n.defaultLocale)
: null;
const dir = localeMeta?.dir ?? "ltr";
const htmlLang = i18n ? i18n.defaultLocale : "en";
// Recovery links, so a reader — or an agent that followed a stale URL — can
// get back on track without guessing: every top-level section, then the
// machine-readable indexes the build emits (the sitemap only exists with a
// `deployment.site`; llms.txt only when `agents.llmsTxt` is on). Tabs link to
// their resolved target when the section has no index page of its own.
const suggestions = [
...data.navigation.tabs.map((tab) => ({
href: withMountedBase(tab.href ?? tab.path),
label: tab.label,
})),
...(data.config.discovery.sitemap
? [{ href: withMountedBase("/sitemap.xml"), label: nf.sitemap }]
: []),
...(data.config.discovery.llmsTxt
? [{ href: withMountedBase("/llms.txt"), label: nf.llms }]
: []),
];
// Every host serves this one page for any missing URL, so a reader who
// mistyped `/ar/…` would get the default locale's message. The other locales'
// copy rides along, and the script below swaps it in when the URL's first
// segment past the site's base names one of them — the message, its language
// and direction, the tab title, and the home link (to that locale's root).
// Localized routes sit under both bases (`{deployment.base}/{basePath}/ar/…`),
// so the script strips the two together, trailing slash included.
const { basePath } = data.config;
const siteRoot = withMountedBase(mountBasePath(basePath, "/"));
const localeBase = siteRoot.endsWith("/") ? siteRoot : siteRoot + "/";
const localized = Object.fromEntries(
(i18n?.locales ?? [])
.filter((l) => l.code !== i18n?.defaultLocale && data.uiByLocale[l.code])
.map((l) => {
const strings = data.uiByLocale[l.code].notFound;
return [
l.code,
{
description: strings.description,
dir: l.dir ?? "ltr",
home: strings.home,
homeHref: withMountedBase(mountBasePath(basePath, "/" + l.code)),
suggestions: strings.suggestions,
title: strings.title,
},
];
})
);
const localizedJson = JSON.stringify(localized).replaceAll("<", "\\u003c");
---
<PageLayout
site={{ title: data.config.title, description: data.config.description }}
logo={data.config.logo}
favicon={data.config.favicon}
appleIcon={data.config.appleIcon}
banner={data.config.banner}
analytics={data.config.analytics}
navigation={data.navigation}
page={{ title: nf.title, route: "/404" }}
themeMode={data.config.theme.mode}
fontCssVars={data.fontCssVars}
searchEnabled={data.config.search.enabled}
locale={htmlLang}
dir={dir}
ui={data.ui}
noindex={true}
>
<div
class="mx-auto grid w-full max-w-5xl gap-12 px-6 py-20 sm:py-28 md:grid-cols-[3fr_2fr] md:gap-16 lg:gap-24 lg:py-36"
data-base={localeBase}
data-blume-not-found
>
<div class="flex flex-col items-start">
<p
class="font-mono text-xs font-medium tracking-widest text-muted-foreground"
>
404
</p>
<h1
class="mt-4 text-balance text-4xl font-semibold tracking-tight text-foreground sm:text-5xl"
data-nf="title"
>
{nf.title}
</h1>
<p class="mt-4 max-w-md text-pretty text-lg text-muted-foreground" data-nf="description">
{nf.description}
</p>
<a
class="mt-8 inline-flex items-center gap-1.5 rounded-full bg-accent py-2 pe-4 ps-3.5 text-sm font-medium text-accent-foreground transition-opacity hover:opacity-90"
data-nf-home
href={withMountedBase("/")}
>
<Icon class="rtl:-scale-x-100" name="arrow-left" size={14} />
<span data-nf="home">{nf.home}</span>
</a>
</div>
{
suggestions.length > 0 && (
<nav aria-label={nf.suggestions} class="md:border-s md:border-border md:ps-12 lg:ps-16" data-nf-suggestions>
<h2 class="text-xs font-medium uppercase tracking-widest text-muted-foreground" data-nf="suggestions">
{nf.suggestions}
</h2>
<ul class="mt-4 divide-y divide-border border-y border-border">
{suggestions.map((link) => (
<li>
<a
class="group flex items-center justify-between gap-4 py-3 text-sm font-medium text-foreground transition-colors hover:text-accent"
href={link.href}
>
<span>{link.label}</span>
<Icon
class="shrink-0 text-muted-foreground transition-transform group-hover:translate-x-0.5 rtl:-scale-x-100 rtl:group-hover:-translate-x-0.5"
name="arrow-right"
size={14}
/>
</a>
</li>
))}
</ul>
</nav>
)
}
</div>
<script id="blume-not-found-locales" is:inline type="application/json" set:html={localizedJson} />
<script is:inline>
(() => {
const apply = () => {
const root = document.querySelector("[data-blume-not-found]");
const source = document.getElementById("blume-not-found-locales");
if (!root || !source) {
return;
}
const base = root.getAttribute("data-base") || "/";
const path = location.pathname.startsWith(base)
? location.pathname.slice(base.length)
: location.pathname.replace(/^\/+/, "");
const code = path.split("/")[0];
const entry = JSON.parse(source.textContent || "{}")[code];
if (!entry) {
return;
}
root.setAttribute("lang", code);
root.setAttribute("dir", entry.dir);
for (const el of root.querySelectorAll("[data-nf]")) {
el.textContent = entry[el.getAttribute("data-nf")];
}
root.querySelector("[data-nf-home]")?.setAttribute("href", entry.homeHref);
root.querySelector("[data-nf-suggestions]")?.setAttribute("aria-label", entry.suggestions);
document.title = entry.title;
};
apply();
document.addEventListener("astro:page-load", apply);
})();
</script>
</PageLayout>
+52
View File
@@ -0,0 +1,52 @@
// Generated by Blume. Do not edit. Override by adding `pages/404.astro`.
import { problem } from "blume/ai/api/problem.ts";
import { withMountedBase } from "blume/components/islands/base-path.ts";
import { absoluteUrl } from "blume/core/site-url.ts";
import data from "blume:data";
export const prerender = true;
const nf = data.ui.notFound;
// Absolute for internal routes when the site is known; an external tab href
// passes through untouched.
const href = (path: string): string => {
const based = withMountedBase(path);
return data.config.site && based.startsWith("/") && !based.startsWith("//")
? absoluteUrl(data.config.site, based)
: based;
};
// The recovery set of 404.astro: home, every top-level section (a tab links to
// its resolved target), then the machine-readable indexes that exist.
const links = [
{ href: href("/"), label: nf.home },
...data.navigation.tabs.map((tab) => ({
href: href(tab.href ?? tab.path),
label: tab.label,
})),
...(data.config.discovery.sitemap
? [{ href: href("/sitemap.xml"), label: nf.sitemap }]
: []),
...(data.config.discovery.llmsTxt
? [{ href: href("/llms.txt"), label: nf.llms }]
: []),
...(data.config.discovery.api
? [{ href: href("/openapi.json"), label: nf.api }]
: []),
];
const body = problem({
code: "PAGE_NOT_FOUND",
detail: nf.description,
links,
resolution: nf.suggestions + ": " + links.map((link) => link.href).join(", "),
status: 404,
title: nf.title,
});
export function GET() {
return new Response(JSON.stringify(body, null, 2) + "\n", {
headers: { "Content-Type": "application/problem+json; charset=utf-8" },
});
}
+57
View File
@@ -0,0 +1,57 @@
// Generated by Blume. Do not edit. Override by adding `pages/404.astro`.
import { withMountedBase } from "blume/components/islands/base-path.ts";
import { absoluteUrl } from "blume/core/site-url.ts";
import data from "blume:data";
export const prerender = true;
const nf = data.ui.notFound;
// Absolute for internal routes when the site is known; an external tab href
// passes through untouched.
const href = (path: string): string => {
const based = withMountedBase(path);
return data.config.site && based.startsWith("/") && !based.startsWith("//")
? absoluteUrl(data.config.site, based)
: based;
};
// The recovery set of 404.astro: home, every top-level section (a tab links to
// its resolved target), then the machine-readable indexes that exist.
const links = [
{ href: href("/"), label: nf.home },
...data.navigation.tabs.map((tab) => ({
href: href(tab.href ?? tab.path),
label: tab.label,
})),
...(data.config.discovery.sitemap
? [{ href: href("/sitemap.xml"), label: nf.sitemap }]
: []),
...(data.config.discovery.llmsTxt
? [{ href: href("/llms.txt"), label: nf.llms }]
: []),
...(data.config.discovery.api
? [{ href: href("/openapi.json"), label: nf.api }]
: []),
];
const body = [
"# " + nf.title,
"",
nf.description,
"",
"## " + nf.suggestions,
"",
...links.map((link) => "- [" + link.label + "](" + link.href + ")"),
"",
].join("\n");
export function GET() {
return new Response(body, {
headers: {
"Content-Type": "text/markdown; charset=utf-8",
// ~4 characters per token; keep in sync with markdownTokenCount.
"x-markdown-tokens": String(Math.ceil(body.length / 4)),
},
});
}
+492
View File
@@ -0,0 +1,492 @@
---
// Generated by Blume. Do not edit.
import { getEntry, render } from "astro:content";
import { pageModeLayout } from "blume/core/page-modes.ts";
import type { CollectionKey } from "astro:content";
import RootLayout from "blume/components/layout/RootLayout.astro";
import { withBase, withMountedBase } from "blume/components/islands/base-path.ts";
import { mountBasePath, stripBasePath } from "blume/core/base-path.ts";
import { routeSetFor, servesRoute } from "blume/core/locale-links.ts";
import { resolveLocalizable } from "blume/core/localizable.ts";
import { resolveSlot } from "blume/components/layout/overrides.ts";
import Accordion from "blume/components/content/Accordion.astro";
import AccordionItem from "blume/components/content/AccordionItem.astro";
import ApiEndpoint from "blume/components/content/ApiEndpoint.astro";
import ApiPlayground from "blume/components/content/ApiPlayground.astro";
import ApiRail from "blume/components/content/ApiRail.astro";
import AutoTypeTable from "blume/components/content/AutoTypeTable.astro";
import Badge from "blume/components/content/Badge.astro";
import Callout from "blume/components/content/Callout.astro";
import Card from "blume/components/content/Card.astro";
import CardGroup from "blume/components/content/CardGroup.astro";
import Changelog from "blume/components/content/Changelog.astro";
import CodeBlock from "blume/components/content/CodeBlock.astro";
import CodeGroup from "blume/components/content/CodeGroup.astro";
import ColorRoot from "blume/components/content/Color.astro";
import ColorItem from "blume/components/content/ColorItem.astro";
import ColorRow from "blume/components/content/ColorRow.astro";
import Column from "blume/components/content/Column.astro";
import Columns from "blume/components/content/Columns.astro";
import Component from "blume/components/content/Component.astro";
import Diff from "blume/components/content/Diff.astro";
import Expandable from "blume/components/content/Expandable.astro";
import FileTree from "blume/components/content/FileTree.astro";
import Frame from "blume/components/content/Frame.astro";
import GithubInfo from "blume/components/content/GithubInfo.astro";
import Panel from "blume/components/content/Panel.astro";
import ParamField from "blume/components/content/ParamField.astro";
import Prompt from "blume/components/content/Prompt.astro";
import RequestExample from "blume/components/content/RequestExample.astro";
import ResponseExample from "blume/components/content/ResponseExample.astro";
import ResponseField from "blume/components/content/ResponseField.astro";
import Step from "blume/components/content/Step.astro";
import Steps from "blume/components/content/Steps.astro";
import Tab from "blume/components/content/Tab.astro";
import Tabs from "blume/components/content/Tabs.astro";
import Tile from "blume/components/content/Tile.astro";
import Tooltip from "blume/components/content/Tooltip.astro";
import TreeRoot from "blume/components/content/Tree.astro";
import TreeFile from "blume/components/content/TreeFile.astro";
import TreeFolder from "blume/components/content/TreeFolder.astro";
import TypeTable from "blume/components/content/TypeTable.astro";
import View from "blume/components/content/View.astro";
import Visibility from "blume/components/content/Visibility.astro";
import YouTube from "blume/components/content/YouTube.astro";
import Icon from "blume/components/Icon.astro";
import LocaleLinks from "blume/components/layout/LocaleLinks.astro";
import NarrationPlayer from "blume/components/layout/NarrationPlayer.astro";
import ViewSwitcher from "blume/components/content/ViewSwitcher.astro";
import ApiOverview from "blume/components/openapi/ApiOverview.astro";
import ApiTagOperations from "blume/components/openapi/ApiTagOperations.astro";
import Operation from "blume/components/openapi/Operation.astro";
import { mdxComponents as userMdx, layoutOverrides } from "../generated/components.ts";
import data from "blume:data";
const Color = Object.assign(ColorRoot, { Item: ColorItem, Row: ColorRow });
const Tree = Object.assign(TreeRoot, { File: TreeFile, Folder: TreeFolder });
// Docs content is file-based and always prerendered, even in server output
// (where only endpoints like /api/ask render on demand). Without this, server
// builds would render this route on demand and ignore getStaticPaths, leaving
// the entry id undefined.
export const prerender = true;
const components = {
Accordion,
AccordionItem,
ApiEndpoint,
ApiOverview,
ApiPlayground,
ApiRail,
ApiTagOperations,
AutoTypeTable,
Badge,
Callout,
Card,
CardGroup,
Changelog,
CodeBlock,
CodeGroup,
Color,
Column,
Columns,
Component,
Diff,
Expandable,
FileTree,
Frame,
GithubInfo,
Icon,
Operation,
Panel,
ParamField,
Prompt,
RequestExample,
ResponseExample,
ResponseField,
Step,
Steps,
Tab,
Tabs,
Tile,
Tooltip,
Tree,
TypeTable,
View,
Visibility,
YouTube,
...userMdx,
};
export function getStaticPaths() {
return data.routes.map((route) => ({
params: { slug: route.path === "/" ? undefined : route.path.slice(1) },
props: {
alternates: route.alternates,
collection: route.collection,
editUrl: route.editUrl,
entryId: route.entryId,
fallback: route.fallback,
hreflang: route.hreflang,
indexable: route.indexable,
lastModified: route.lastModified,
published: route.published,
locale: route.locale,
monolingual: route.monolingual,
route: route.path,
title: route.title,
version: route.version,
versionAlternates: route.versionAlternates,
},
}));
}
const { entryId, collection, route, title, indexable, editUrl, lastModified, published, locale, alternates, hreflang, fallback, monolingual, version, versionAlternates } = Astro.props;
const entry = await getEntry(collection as CollectionKey, entryId);
if (!entry) {
return new Response(null, { status: 404 });
}
const { Content, headings: allHeadings, remarkPluginFrontmatter } = await render(entry);
// `[!toc]`-marked headings render on the page but stay out of the table of
// contents; the heading plugin reports their slugs through the render's
// frontmatter (see markdown/heading-anchors.ts). Only the plugin's array
// counts: `frontmatter.extend` can declare the same key, and on a page with
// no headings that user-supplied value would pass straight through.
// A page written in `<View>` blocks gets their picker above the content;
// the views plugin lists them through the render's frontmatter (see
// markdown/views.ts), and only a list of titled entries counts.
const viewsRaw = remarkPluginFrontmatter?.blumeViews;
const views = Array.isArray(viewsRaw)
? viewsRaw.filter((view) => typeof view?.title === "string")
: [];
const tocHiddenRaw = remarkPluginFrontmatter?.__blumeTocHidden;
const tocHidden = new Set(Array.isArray(tocHiddenRaw) ? tocHiddenRaw : []);
// A heading holding a `<Badge>` lists without the badge's text, which the
// heading plugin reports by slug the same way.
const tocTextRaw = remarkPluginFrontmatter?.__blumeTocText;
const tocText = tocTextRaw && typeof tocTextRaw === "object" ? tocTextRaw : {};
const headings = (
tocHidden.size > 0
? allHeadings.filter((heading) => !tocHidden.has(heading.slug))
: allHeadings
).map((heading) =>
typeof tocText[heading.slug] === "string"
? { ...heading, text: tocText[heading.slug] }
: heading
);
// An untitled page's opening `# Heading` is its title, so the title-heading
// plugin drops it from the body and reports its id here: the title `<h1>`
// below takes it, so links to the heading's anchor still land (see
// markdown/title-heading.ts).
const titleIdRaw = remarkPluginFrontmatter?.blumeTitleId;
const titleId = typeof titleIdRaw === "string" ? titleIdRaw : undefined;
const frontmatter = entry.data;
const seo = frontmatter.seo;
const base = data.config.site ? data.config.site.replace(/\/$/, "") : null;
// Percent-encode the route-derived path (the sitemap convention): a Unicode
// slug (`/api/größe`) is not legal in a raw URI, and crawlers compare
// canonical against the sitemap's encoded <loc> byte-for-byte.
const ogPath = data.config.og.enabled
? encodeURI(`/og/${route === "/" ? "index" : route.slice(1)}.png`)
: null;
// Root-relative paths carry the deployment base (the page is served under it),
// and the site too once it's known: `site + base + path`. `seo.image` may be
// an external URL, which passes through verbatim (mirrors PageLayout). An
// authored image keeps a base written by hand; the generated card is always
// mounted under it.
const absoluteOg = (path: string, based: (path: string) => string) =>
path.startsWith("/") ? `${base ?? ""}${based(path)}` : path;
// The page's own image, else the site's default for its locale.
const selectedOgImage =
seo.image ??
(data.config.og.image && resolveLocalizable(data.config.og.image, locale, data.config.i18n?.defaultLocale));
const ogImage = selectedOgImage
? absoluteOg(selectedOgImage, withBase)
: ogPath && absoluteOg(ogPath, withMountedBase);
// Blume's generated card has known dimensions the layout can declare; an
// authored image could be any size or format, so it gets none.
const ogGenerated = !selectedOgImage && Boolean(ogPath);
// X attribution: the site's account, plus a creator the page can claim for
// itself (a guest post crediting its own author) over the configured default.
const x = { ...data.config.x, ...(seo.x?.creator ? { creator: seo.x.creator } : {}) };
const basedRoute = withMountedBase(route);
// Locale resolution. With i18n on, pick the active locale's nav + dictionary,
// build hreflang alternates, and derive the language-switcher targets.
const i18n = data.config.i18n;
const localePrefix = (codeArg: string) =>
i18n && codeArg === i18n.defaultLocale && i18n.hideDefaultLocalePrefix
? ""
: `/${codeArg}`;
const localizeRoute = (logical: string, codeArg: string) => {
const prefix = localePrefix(codeArg);
if (!prefix) {
return logical;
}
return logical === "/" ? prefix : `${prefix}${logical}`;
};
const stripLocale = (path: string, codeArg: string) => {
const prefix = localePrefix(codeArg);
return prefix && path.startsWith(prefix) ? path.slice(prefix.length) || "/" : path;
};
// Version resolution. An archived page renders its snapshot's navigation tree,
// points its canonical at the latest equivalent (unless configured otherwise),
// and shows the old-version notice.
const versionsConfig = data.config.versions;
const archived = versionsConfig && version
? (versionsConfig.archived.find((v) => v.id === version) ?? null)
: null;
const latestVersionAlt = (versionAlternates ?? []).find((alt) => alt.version === "");
const navigation = version
? (data.navigationByVersion[version]?.[i18n ? locale : ""] ?? data.navigation)
: i18n
? (data.navigationByLocale[locale] ?? data.navigation)
: data.navigation;
const ui = i18n ? (data.uiByLocale[locale] ?? data.ui) : data.ui;
const localeMeta = i18n ? i18n.locales.find((l) => l.code === locale) : null;
const dir = localeMeta?.dir ?? "ltr";
const htmlLang = i18n ? locale : "en";
// A fallback page renders the fallback locale's content, so its text direction
// follows that language — not the (mirrored) page locale.
const contentLocale =
fallback && i18n?.fallbackLocale ? i18n.fallbackLocale : locale;
// "Listen to this page", unless the page opts out. The spoken cues are read
// between the content's own sentences, so they come from the content's
// language (a fallback page's), while the player's labels follow the page.
// A `custom` or `frame` page brings its own heading: no title, description,
// or narration player above its content.
const pageChrome = pageModeLayout(frontmatter.mode).chrome;
const narration = pageChrome && frontmatter.narration ? data.config.narration : null;
const narrationCues = (i18n ? (data.uiByLocale[contentLocale] ?? data.ui) : data.ui).narration;
const contentDir = i18n
? (i18n.locales.find((l) => l.code === contentLocale)?.dir ?? "ltr")
: "ltr";
// The root route keeps its trailing slash (`https://site/`) so canonical and
// hreflang URLs byte-match the sitemap's <loc> for the home page.
const absolute = (path: string) => base + withMountedBase(path);
// A fallback page renders the fallback locale's page at this locale's URL, so
// its canonical names the page it copies, and search engines never rank the
// copy against the original.
const fallbackSource =
fallback && i18n?.fallbackLocale
? (alternates ?? []).find((alt) => alt.locale === i18n.fallbackLocale)
: undefined;
// The page it copies, when that page is archived too, points on to the latest
// docs; the copy names that final page directly, the fallback locale's
// version of the latest one, so the canonical is never a chain.
const fallbackLatest =
fallbackSource && archived && archived.canonical === "latest" && latestVersionAlt
? (data.routes.find((entry) => entry.path === latestVersionAlt.path)?.alternates ?? []).find(
(alt) => alt.locale === i18n?.fallbackLocale
)
: undefined;
const fallbackTarget = fallbackLatest ?? fallbackSource;
// An archived page defaults its canonical to the same page in the latest docs
// when that page still exists — search engines treat the live page as
// authoritative without deindexing version-only content. A page's own
// `seo.canonical` always wins, and `canonical: "self"` keeps the default.
const canonical =
seo.canonical ??
(fallbackTarget && base
? absolute(fallbackTarget.path)
: archived && archived.canonical === "latest" && latestVersionAlt && base
? absolute(latestVersionAlt.path)
: base
? `${base}${basedRoute === "/" ? "/" : encodeURI(basedRoute)}`
: null);
const effectiveNoindex = Boolean(seo.noindex) || (archived?.noindex ?? false);
// hreflang names only indexable translations (see `hreflangAlternates`).
const localeAlternates =
i18n && base
? (hreflang ?? []).map((alt) => ({ hreflang: alt.locale, href: absolute(alt.path) }))
: [];
const defaultAlt = i18n ? (hreflang ?? []).find((alt) => alt.locale === i18n.defaultLocale) : null;
const xDefault = defaultAlt && base ? absolute(defaultAlt.path) : null;
// `route` arrives with `basePath` already applied, so the locale segment it
// carries sits *after* the base (`/docs/ja/guide`), while `localePrefix` is
// base-less (`/ja`). Stripping and re-adding a locale therefore happen in
// base-less space, with the base re-applied at the end — the same
// `mountBasePath(basePath, localizeRoute(...))` composition the manifest uses to
// build every real route. Done in based space, `stripLocale` matches nothing
// and `localizeRoute` prepends a second prefix (`/ja/docs/ja/guide`). Only a
// switcher entry for a locale with no real translation reaches this fallback:
// a partially translated hand-written page for its missing locales, or a
// generated reference (which has no `alternates` at all) for every locale
// but its own.
const mountLocalized = (logical: string, codeArg: string) =>
mountBasePath(data.config.basePath, localizeRoute(logical, codeArg));
const logicalRoute = i18n
? stripLocale(stripBasePath(data.config.basePath, route), locale)
: route;
// A page from a one-language source (GitHub Releases) gets no switcher: every
// other locale would only repeat the same text. A locale with no real
// translation links the page's fallback copy, which exists only while
// fallbacks are on; where nothing is served at that URL (`fallbackLocale:
// null`), the locale is left out rather than linked to a 404.
const localeSwitch = i18n && !monolingual
? i18n.locales.flatMap((l) => {
const alt = (alternates ?? []).find((x) => x.locale === l.code);
const href = alt ? alt.path : mountLocalized(logicalRoute, l.code);
return alt || servesRoute(routeSetFor(data.routes), href)
? [{
code: l.code,
current: l.code === locale,
dir: l.dir,
href,
label: l.label,
untranslated: !alt,
}]
: [];
})
: [];
// Version switcher + old-version notice. The switcher auto-populates from the
// versions config as a `kind: "version"` selector; a user-declared version
// selector in `navigation.selectors` suppresses it (theirs renders instead).
// Fallback version roots compose like real routes — `{basePath}/{locale?}/{id}`
// (manifest `versionAlternates` paths arrive with the base already applied).
const versionRootFor = (id: string) => {
const logical = id ? `/${id}` : "/";
return i18n
? mountLocalized(logical, locale)
: mountBasePath(data.config.basePath, logical);
};
const samePageSwitch = versionsConfig
? versionsConfig.switcher.redirect === "same-page"
: true;
const userHasVersionSelector = navigation.selectors.some(
(selector) => selector.kind === "version"
);
const versionSelector =
versionsConfig && !userHasVersionSelector
? {
items: [
{
id: "",
label: versionsConfig.current.label,
tag: versionsConfig.current.badge,
},
...versionsConfig.archived.map((v) => ({
id: v.id,
label: v.label ?? v.id,
tag: undefined,
})),
].map((entry) => {
const alt = (versionAlternates ?? []).find(
(a) => a.version === entry.id
);
return {
label: entry.label,
path: samePageSwitch && alt ? alt.path : versionRootFor(entry.id),
...(entry.tag ? { tag: entry.tag } : {}),
};
}),
kind: "version" as const,
label: ui.versions.switcher,
}
: null;
const versionNotice =
archived && archived.banner !== false
? {
latestHref: latestVersionAlt
? latestVersionAlt.path
: versionRootFor(""),
latestLabel: ui.versions.latest,
message:
typeof archived.banner === "string"
? archived.banner
: ui.versions.notice.replace(
"{version}",
archived.label ?? archived.id
),
}
: null;
// The whole page shell is overridable via `layout.Layout`; it receives the same
// props as the built-in RootLayout, plus the `layout` map for its inner slots.
const LayoutComponent = resolveSlot(layoutOverrides.Layout, RootLayout);
---
<LayoutComponent
site={{ title: data.config.title, description: data.config.description }}
layout={layoutOverrides}
clientData={{ config: data.config, navigation, page: { route, title: seo.title ?? title } }}
logo={data.config.logo}
mcp={data.config.mcp}
favicon={data.config.favicon}
appleIcon={data.config.appleIcon}
banner={data.config.banner}
analytics={data.config.analytics}
imageZoom={data.config.imageZoom}
codeWrap={data.config.codeWrap}
navigation={navigation}
locale={htmlLang}
dir={dir}
contentDir={contentDir}
contentLocale={contentLocale}
ui={ui}
localeAlternates={localeAlternates}
xDefault={xDefault}
localeSwitch={localeSwitch}
versionSelector={versionSelector}
versionNotice={versionNotice}
searchVersion={versionsConfig ? version : null}
page={{ title: seo.title ?? title, description: seo.description ?? frontmatter.description, route }}
headings={headings}
toc={data.config.toc}
dateFormat={data.config.dateFormat}
themeMode={data.config.theme.mode}
fontCssVars={data.fontCssVars}
searchEnabled={data.config.search.enabled}
indexable={indexable}
ogImage={ogImage}
ogGenerated={ogGenerated}
x={x}
canonical={canonical}
editUrl={editUrl}
feedback={data.config.feedback}
feedbackComments={data.config.feedbackComments}
pagination={frontmatter.pagination}
related={frontmatter.related}
search={frontmatter.search}
exportPdf={false}
exportEpub={false}
openInChat={data.config.openInChat}
feeds={data.feeds}
discovery={data.config.discovery}
siteUrl={data.config.site}
pageType={frontmatter.type}
apiRail={remarkPluginFrontmatter?.blumeApiRail === true}
pageMode={frontmatter.mode}
published={frontmatter.date ?? frontmatter.changelog?.date ?? published}
authors={frontmatter.authors}
lastModified={lastModified}
noindex={effectiveNoindex}
structuredDataEnabled={data.config.structuredData}
>
{pageChrome && <h1 id={titleId}>{title}</h1>}
{pageChrome && frontmatter.description && <p class="text-lg text-muted-foreground">{frontmatter.description}</p>}
{narration && (
<NarrationPlayer
audioBase={narration.generated ? withMountedBase("/blume-narration/audio/") : undefined}
cues={narrationCues}
lang={i18n ? contentLocale : htmlLang}
manifest={narration.generated ? withMountedBase(encodeURI(`/blume-narration/${route === "/" ? "index" : route.slice(1)}.json`)) : undefined}
strings={ui.narration}
/>
)}
{views.length > 1 && <ViewSwitcher label={ui.content.selectView} views={views} />}
<LocaleLinks locale={locale}>
<Content components={components} />
</LocaleLinks>
</LayoutComponent>
+24
View File
@@ -0,0 +1,24 @@
// Generated by Blume. Do not edit.
import raw from "blume:raw-markdown";
export const prerender = true;
export function getStaticPaths() {
return Object.keys(raw).map((route) => ({
params: { slug: route === "/" ? "index" : route.slice(1) },
props: { route },
}));
}
export function GET({ props }: { props: { route: string } }) {
const entries = raw as Record<string, { md?: string; mdx?: string }>;
const entry = entries[props.route];
const body = entry ? (entry.md ?? entry.mdx ?? "") : "";
return new Response(body, {
headers: {
"Content-Type": "text/markdown; charset=utf-8",
// ~4 characters per token; keep in sync with markdownTokenCount.
"x-markdown-tokens": String(Math.ceil(body.length / 4)),
},
});
}
+24
View File
@@ -0,0 +1,24 @@
// Generated by Blume. Do not edit.
import raw from "blume:raw-markdown";
export const prerender = true;
export function getStaticPaths() {
return Object.keys(raw).map((route) => ({
params: { slug: route === "/" ? "index" : route.slice(1) },
props: { route },
}));
}
export function GET({ props }: { props: { route: string } }) {
const entries = raw as Record<string, { md?: string; mdx?: string }>;
const entry = entries[props.route];
const body = entry ? (entry.mdx ?? "") : "";
return new Response(body, {
headers: {
"Content-Type": "text/markdown; charset=utf-8",
// ~4 characters per token; keep in sync with markdownTokenCount.
"x-markdown-tokens": String(Math.ceil(body.length / 4)),
},
});
}
@@ -0,0 +1,9 @@
// Generated by Blume. Do not edit.
import { navigationResponse } from "blume/ai/api/handlers.ts";
import data from "blume:mcp-data";
export const prerender = true;
export function GET() {
return navigationResponse(data);
}
@@ -0,0 +1,9 @@
// Generated by Blume. Do not edit.
import { pagesIndexResponse } from "blume/ai/api/handlers.ts";
import data from "blume:mcp-data";
export const prerender = true;
export function GET() {
return pagesIndexResponse(data);
}
@@ -0,0 +1,13 @@
// Generated by Blume. Do not edit.
import { pageParams, pageResponse } from "blume/ai/api/handlers.ts";
import data from "blume:mcp-data";
export const prerender = true;
export function getStaticPaths() {
return pageParams(data);
}
export function GET({ props }: { props: { route: string } }) {
return pageResponse(data, props.route);
}
@@ -0,0 +1,120 @@
// Generated by Blume. Do not edit.
import { existsSync } from "node:fs";
import { readdir, readFile } from "node:fs/promises";
import { isAbsolute, join, relative, resolve } from "node:path";
import type { APIRoute } from "astro";
import assets from "blume:content-assets";
export const prerender = true;
const files = assets as Record<string, string>;
const STAGED_DIR = null;
const TYPES: Record<string, string> = {
".apng": "image/apng",
".avif": "image/avif",
".bmp": "image/bmp",
".gif": "image/gif",
".ico": "image/x-icon",
".jpeg": "image/jpeg",
".jpg": "image/jpeg",
".mov": "video/quicktime",
".mp4": "video/mp4",
".ogv": "video/ogg",
".png": "image/png",
".svg": "image/svg+xml",
".tiff": "image/tiff",
".webm": "video/webm",
".webp": "image/webp",
};
// The other files a page's links name beside it, typed so the dev server
// opens them the way a static host would. Anything else downloads.
const FILE_TYPES: Record<string, string> = {
".csv": "text/csv; charset=utf-8",
".json": "application/json",
".mp3": "audio/mpeg",
".pdf": "application/pdf",
".txt": "text/plain; charset=utf-8",
".wav": "audio/wav",
".zip": "application/zip",
};
const extensionOf = (path: string): string =>
path.slice(path.lastIndexOf(".")).toLowerCase();
const contentType = (path: string): string =>
TYPES[extensionOf(path)] ??
FILE_TYPES[extensionOf(path)] ??
"application/octet-stream";
// A source downloads only images and videos; anything else in the staged
// directory (a page an older build saved under its URL's extension) is never
// published from the docs origin.
const isMedia = (path: string): boolean => Object.hasOwn(TYPES, extensionOf(path));
const stagedParams = async (): Promise<string[]> => {
if (STAGED_DIR === null || !existsSync(STAGED_DIR)) {
return [];
}
const entries = await readdir(STAGED_DIR, {
recursive: true,
withFileTypes: true,
});
return entries
.filter((entry) => entry.isFile() && isMedia(entry.name))
.map((entry) =>
relative(STAGED_DIR, join(entry.parentPath, entry.name)).replaceAll(
"\\",
"/"
)
);
};
export async function getStaticPaths() {
const params = [
...Object.keys(files).map((key) => "content/" + key),
...(await stagedParams()),
];
return params.map((asset) => ({ params: { asset } }));
}
const resolveAsset = (asset: string): string | null => {
if (asset.startsWith("content/")) {
return files[asset.slice("content/".length)] ?? null;
}
if (STAGED_DIR === null) {
return null;
}
const abs = resolve(STAGED_DIR, asset);
// Traversal guard: the dev server renders on demand, so the param is
// attacker-controlled there — never step outside the staged directory.
// path.relative rather than a string-prefix test: STAGED_DIR is baked in
// with forward slashes while resolve() answers in the platform's
// separators, so on Windows the prefix test 404'd every legitimate staged
// asset — and a bare prefix also admits a sibling directory that merely
// shares the name.
const rel = relative(STAGED_DIR, abs);
if (rel === "" || rel.startsWith("..") || isAbsolute(rel) || !isMedia(rel)) {
return null;
}
return existsSync(abs) ? abs : null;
};
export const GET: APIRoute = async ({ params }) => {
const path = resolveAsset(String(params.asset ?? ""));
if (!path) {
return new Response(null, { status: 404 });
}
const body = await readFile(path);
const type = contentType(path);
// An SVG opened directly is a document that can run script; the sandbox
// keeps an uploaded one inert on the docs origin. This endpoint
// prerenders, so these headers reach dev only: a build serves the files
// with the same headers from its host's rules (see deploy/headers.ts).
const headers: Record<string, string> =
type === "image/svg+xml"
? { ...{"Content-Security-Policy":"sandbox","X-Content-Type-Options":"nosniff"}, "Content-Type": type }
: { "Content-Type": type };
return new Response(new Uint8Array(body), { headers });
};
+10
View File
@@ -0,0 +1,10 @@
// Generated by Blume. Do not edit.
import documents from "blume:search-index";
export const prerender = true;
export function GET() {
return new Response(JSON.stringify(documents), {
headers: { "Content-Type": "application/json" },
});
}
+96
View File
@@ -0,0 +1,96 @@
// Generated by Blume. Do not edit.
import { cachedOgImage } from "blume/og";
import type { OgCache, OgFont, OgFontFamilies, OgGoogleFont } from "blume/og";
import data from "blume:data";
export const prerender = true;
// Custom (non-content) pages opted into a generated card, baked in at build.
// The annotation keeps the empty-array case from being an implicit any[]
// (ts(7034)) under a strict tsconfig.
const customRoutes: { slug: string; title: string }[] = [{"slug":"appendix","title":"Appendix"},{"slug":"changelog","title":"Changelog"},{"slug":"progress","title":"Progress"}];
// Card fonts, resolved at generation time (explicit seo.og.fonts, or derived
// from theme.fonts). Local entries carry absolute build-machine paths, which
// is why they are baked into this build-only endpoint instead of the runtime
// data that pages serialize into HTML.
const fonts: OgFont[] = [];
const families: OgFontFamilies | undefined = {"body":"Geist","title":"Geist"};
// Script fallbacks (Noto families, the configured locales' first). A card
// fetches them only when its text has a glyph Takumi's built-in font lacks.
const fallbacks: OgGoogleFont[] = [{"name":"Noto Sans","weight":[400,600]},{"name":"Noto Sans Ethiopic","weight":[400,600]},{"name":"Noto Sans Arabic","weight":[400,600]},{"name":"Noto Sans Bengali","weight":[400,600]},{"name":"Noto Sans Gujarati","weight":[400,600]},{"name":"Noto Sans Hebrew","weight":[400,600]},{"name":"Noto Sans Devanagari","weight":[400,600]},{"name":"Noto Sans Armenian","weight":[400,600]},{"name":"Noto Sans JP","weight":[400,600]},{"name":"Noto Sans Georgian","weight":[400,600]},{"name":"Noto Sans Khmer","weight":[400,600]},{"name":"Noto Sans Kannada","weight":[400,600]},{"name":"Noto Sans KR","weight":[400,600]},{"name":"Noto Sans Lao","weight":[400,600]},{"name":"Noto Sans Malayalam","weight":[400,600]},{"name":"Noto Sans Myanmar","weight":[400,600]},{"name":"Noto Sans Oriya","weight":[400,600]},{"name":"Noto Sans Gurmukhi","weight":[400,600]},{"name":"Noto Sans Sinhala","weight":[400,600]},{"name":"Noto Sans Tamil","weight":[400,600]},{"name":"Noto Sans Telugu","weight":[400,600]},{"name":"Noto Sans Thai","weight":[400,600]},{"name":"Noto Sans SC","weight":[400,600]},{"name":"Noto Sans TC","weight":[400,600]}];
// Rendered cards are kept on disk between builds, keyed by everything that
// decides their pixels, so a rebuild renders only the cards whose title,
// description, branding, or fonts changed. The directory is a build-machine
// path, baked in for the same reason as the local font paths above.
const cache: OgCache | undefined = undefined;
// A page's own description (its `seo.description`, else `description`) is
// the card subtitle, so the image says what the page's og:description says.
// Pages without one fall back to the site-wide subtitle at render time.
// `seo.og.description: false` hides the subtitle on every card, page text
// included, which is what switches this off.
const pageDescriptions = true;
interface CardProps {
title: string;
description: string | null;
}
export function getStaticPaths() {
const seen = new Set<string>();
const paths: { params: { slug: string }; props: CardProps }[] = [];
const add = (slug: string, title: string, description: string | null) => {
if (seen.has(slug)) {
return;
}
seen.add(slug);
paths.push({
params: { slug },
props: { title, description: pageDescriptions ? description : null },
});
};
// A custom page wins over a content route sharing its path, so add it first.
// Its description is unknown at generate time, so it takes the site subtitle.
for (const route of customRoutes) {
add(route.slug, route.title, null);
}
for (const route of data.routes) {
add(
route.path === "/" ? "index" : route.path.slice(1),
route.title,
route.description
);
}
return paths;
}
// Footer branding shared by every card. The slug comes from the configured
// repo rather than the URL, so an Enterprise host reads the same as github.com;
// the site text (host plus deployment base) is resolved at generate time.
const repoSlug = data.config.github
? `${data.config.github.owner}/${data.config.github.repo}`
: undefined;
export async function GET({ props }: { props: CardProps }) {
const png = await cachedOgImage(cache, {
accent: data.config.og.palette?.accent ?? data.config.theme.accent.light,
brand: data.config.title,
description: props.description ?? data.config.og.description,
fallbacks,
families,
fonts,
logo: data.config.og.logo,
palette: data.config.og.palette,
repo: repoSlug,
site: data.config.og.site,
title: props.title,
});
return new Response(new Uint8Array(png), {
headers: {
"Cache-Control": "public, max-age=31536000, immutable",
"Content-Type": "image/png",
},
});
}
+803
View File
@@ -0,0 +1,803 @@
// Generated by Blume. Do not edit.
export const prerender = true;
const payload = {
"components": {
"schemas": {
"JsonRpcRequest": {
"description": "A JSON-RPC 2.0 request, as the Model Context Protocol sends.",
"properties": {
"id": {
"description": "Request id (absent on notifications)."
},
"jsonrpc": {
"const": "2.0",
"type": "string"
},
"method": {
"description": "The MCP method, e.g. `initialize` or `tools/call`.",
"type": "string"
},
"params": {
"description": "Method parameters.",
"type": "object"
}
},
"required": [
"jsonrpc",
"method"
],
"type": "object"
},
"JsonRpcResponse": {
"description": "A JSON-RPC 2.0 response: a `result` or an `error`.",
"properties": {
"error": {
"properties": {
"code": {
"type": "integer"
},
"data": {},
"message": {
"type": "string"
}
},
"required": [
"code",
"message"
],
"type": "object"
},
"id": {},
"jsonrpc": {
"const": "2.0",
"type": "string"
},
"result": {
"type": "object"
}
},
"required": [
"jsonrpc"
],
"type": "object"
},
"NavLink": {
"properties": {
"href": {
"description": "Link target — an internal route or an external URL.",
"type": "string"
},
"icon": {
"description": "Optional icon name.",
"type": "string"
},
"label": {
"type": "string"
}
},
"required": [
"href",
"label"
],
"type": "object"
},
"NavNode": {
"description": "A sidebar entry: a page (linking to its route) or a group holding further nodes.",
"oneOf": [
{
"properties": {
"badge": {
"type": "string"
},
"deprecated": {
"type": "boolean"
},
"description": {
"type": "string"
},
"icon": {
"type": "string"
},
"kind": {
"const": "page",
"type": "string"
},
"label": {
"type": "string"
},
"pageId": {
"description": "The page's stable content id.",
"type": "string"
},
"route": {
"description": "The page's route.",
"type": "string"
}
},
"required": [
"kind",
"label",
"pageId",
"route"
],
"type": "object"
},
{
"properties": {
"badge": {
"type": "string"
},
"children": {
"items": {
"$ref": "#/components/schemas/NavNode"
},
"type": "array"
},
"collapsed": {
"type": "boolean"
},
"icon": {
"type": "string"
},
"kind": {
"const": "group",
"type": "string"
},
"label": {
"type": "string"
},
"path": {
"description": "The group's route prefix (not necessarily a page).",
"type": "string"
},
"route": {
"description": "The group's index page route, when it has one.",
"type": "string"
}
},
"required": [
"children",
"kind",
"label"
],
"type": "object"
}
]
},
"NavSelector": {
"description": "A top-level partition selector (products, versions, languages).",
"properties": {
"items": {
"items": {
"properties": {
"description": {
"type": "string"
},
"label": {
"type": "string"
},
"path": {
"type": "string"
},
"tag": {
"type": "string"
}
},
"required": [
"label",
"path"
],
"type": "object"
},
"type": "array"
},
"kind": {
"type": "string"
},
"label": {
"type": "string"
}
},
"required": [
"items",
"kind",
"label"
],
"type": "object"
},
"NavTab": {
"properties": {
"href": {
"description": "The clickable target when it differs from `path` (the section's first page).",
"type": "string"
},
"icon": {
"type": "string"
},
"label": {
"type": "string"
},
"path": {
"description": "The tab's section prefix.",
"type": "string"
}
},
"required": [
"label",
"path"
],
"type": "object"
},
"Navigation": {
"description": "The docs navigation model: header tabs, the sidebar tree, partition selectors, and pinned links.",
"properties": {
"actions": {
"items": {
"$ref": "#/components/schemas/NavLink"
},
"type": "array"
},
"cta": {
"oneOf": [
{
"$ref": "#/components/schemas/NavLink"
},
{
"type": "null"
}
]
},
"featured": {
"items": {
"$ref": "#/components/schemas/NavLink"
},
"type": "array"
},
"repoUrl": {
"type": [
"string",
"null"
]
},
"root": {
"description": "The tree root's route (`/`, or a locale/version prefix).",
"type": "string"
},
"selectors": {
"items": {
"$ref": "#/components/schemas/NavSelector"
},
"type": "array"
},
"sidebar": {
"items": {
"$ref": "#/components/schemas/NavNode"
},
"type": "array"
},
"tabs": {
"items": {
"$ref": "#/components/schemas/NavTab"
},
"type": "array"
}
},
"required": [
"featured",
"selectors",
"sidebar",
"tabs"
],
"type": "object"
},
"Page": {
"allOf": [
{
"$ref": "#/components/schemas/PageSummary"
},
{
"properties": {
"markdown": {
"description": "The page as agent Markdown: frontmatter included, components downleveled to plain Markdown.",
"type": "string"
}
},
"required": [
"markdown"
],
"type": "object"
}
],
"description": "A page's index entry plus its full Markdown body."
},
"PageSummary": {
"properties": {
"contentType": {
"description": "The page's content type (frontmatter `type`).",
"type": "string"
},
"description": {
"type": "string"
},
"facets": {
"additionalProperties": {
"type": "string"
},
"description": "Facet values the site declares for the page's content type (`content.types.<type>.facets`), key → value.",
"type": "object"
},
"json": {
"description": "This page's JSON representation (the `getPage` operation).",
"format": "uri-reference",
"type": "string"
},
"lastModified": {
"description": "ISO 8601 last-modified date, when known.",
"type": [
"string",
"null"
]
},
"locale": {
"description": "The page's locale code.",
"type": "string"
},
"markdownUrl": {
"description": "The page's raw-Markdown mirror (the `getPageMarkdown` operation).",
"format": "uri-reference",
"type": "string"
},
"route": {
"description": "The page's route (`/guides/install`); the key every other operation takes.",
"type": "string"
},
"title": {
"type": "string"
},
"url": {
"description": "Where the rendered page is served.",
"format": "uri-reference",
"type": "string"
},
"version": {
"description": "Docs version the page belongs to on a versioned site: `\"\"` for the current docs, else an archived version id. Absent on unversioned sites.",
"type": "string"
}
},
"required": [
"contentType",
"json",
"lastModified",
"locale",
"markdownUrl",
"route",
"title",
"url"
],
"type": "object"
},
"PagesIndex": {
"properties": {
"count": {
"type": "integer"
},
"generator": {
"description": "The Blume version that built the site.",
"type": "string"
},
"pages": {
"items": {
"$ref": "#/components/schemas/PageSummary"
},
"type": "array"
},
"site": {
"type": [
"string",
"null"
]
}
},
"required": [
"count",
"generator",
"pages",
"site"
],
"type": "object"
},
"Problem": {
"description": "RFC 9457 problem details, with a stable code and a resolution hint.",
"properties": {
"code": {
"description": "Stable error code (e.g. `PAGE_NOT_FOUND`).",
"type": "string"
},
"detail": {
"description": "Human-readable explanation of this occurrence.",
"type": "string"
},
"instance": {
"description": "The request path the problem occurred on.",
"type": "string"
},
"links": {
"description": "Recovery links, when there is somewhere useful to go.",
"items": {
"properties": {
"href": {
"type": "string"
},
"label": {
"type": "string"
}
},
"required": [
"href",
"label"
],
"type": "object"
},
"type": "array"
},
"resolution": {
"description": "What to do next.",
"type": "string"
},
"status": {
"type": "integer"
},
"title": {
"type": "string"
},
"type": {
"description": "Problem type URI; `about:blank` by default.",
"type": "string"
}
},
"required": [
"code",
"detail",
"resolution",
"status",
"title",
"type"
],
"type": "object"
},
"SearchHit": {
"properties": {
"contentType": {
"type": "string"
},
"excerpt": {
"description": "The page description, else the start of its content.",
"type": "string"
},
"facets": {
"additionalProperties": {
"type": "string"
},
"description": "Facet values the site declares for the page's content type (`content.types.<type>.facets`), key → value.",
"type": "object"
},
"route": {
"description": "The page's route; pass it to `getPage`.",
"type": "string"
},
"title": {
"type": "string"
},
"url": {
"format": "uri-reference",
"type": "string"
},
"version": {
"description": "Docs version the page belongs to on a versioned site: `\"\"` for the current docs, else an archived version id. Absent on unversioned sites.",
"type": "string"
}
},
"required": [
"excerpt",
"route",
"title",
"url"
],
"type": "object"
},
"SearchResponse": {
"properties": {
"count": {
"type": "integer"
},
"query": {
"type": "string"
},
"results": {
"items": {
"$ref": "#/components/schemas/SearchHit"
},
"type": "array"
}
},
"required": [
"count",
"query",
"results"
],
"type": "object"
}
}
},
"info": {
"description": "Read-only JSON API over the prdlk documentation: Documentation for my LeetCode solutions\n\nEvery operation is public and needs no authentication. Errors are RFC 9457 problem details (`application/problem+json`) with a stable `code`, a `detail`, and a `resolution` hint.",
"title": "prdlk API",
"version": "2.2.4",
"x-generator": "blume@2.2.4"
},
"openapi": "3.1.0",
"paths": {
"/api/docs/pages.json": {
"get": {
"description": "Every documentation page with its route, title, description, content type, locale, facets, and the URLs of its rendered, Markdown, and JSON forms. Unfiltered; on a versioned site every version is listed with its `version`. Use it to enumerate the docs or to find a page when search is too narrow.",
"operationId": "listPages",
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/PagesIndex"
}
}
},
"description": "The page index."
},
"default": {
"content": {
"application/problem+json": {
"schema": {
"$ref": "#/components/schemas/Problem"
}
}
},
"description": "An error, as problem details."
}
},
"summary": "List every page",
"tags": [
"Pages"
]
}
},
"/api/docs/pages/{route}.json": {
"get": {
"description": "A single page as JSON: its index entry plus the page's agent Markdown (frontmatter included, components downleveled to plain Markdown). Take `route` from `listPages` or `searchDocs`.",
"operationId": "getPage",
"parameters": [
{
"description": "The page route without its leading slash (`guides/install`), or `index` for the home page. May contain slashes.",
"in": "path",
"name": "route",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Page"
}
}
},
"description": "The page."
},
"404": {
"content": {
"application/problem+json": {
"schema": {
"$ref": "#/components/schemas/Problem"
}
}
},
"description": "No page has that route."
},
"default": {
"content": {
"application/problem+json": {
"schema": {
"$ref": "#/components/schemas/Problem"
}
}
},
"description": "An error, as problem details."
}
},
"summary": "Get a page as JSON",
"tags": [
"Pages"
]
}
},
"/api/docs/navigation.json": {
"get": {
"description": "The navigation tree (header tabs and the sidebar hierarchy) as readers see it, for the default locale and the current docs.",
"operationId": "getNavigation",
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Navigation"
}
}
},
"description": "The navigation tree."
},
"default": {
"content": {
"application/problem+json": {
"schema": {
"$ref": "#/components/schemas/Problem"
}
}
},
"description": "An error, as problem details."
}
},
"summary": "Get the navigation tree",
"tags": [
"Navigation"
]
}
},
"/{route}.md": {
"get": {
"description": "A page's raw-Markdown mirror: append `.md` to any page URL. Components are downleveled to plain Markdown; `.mdx` serves the source as written. The same body the `getPage` operation carries in its `markdown` field.",
"operationId": "getPageMarkdown",
"parameters": [
{
"description": "The page route without its leading slash (`guides/install`), or `index` for the home page. May contain slashes.",
"in": "path",
"name": "route",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"content": {
"text/markdown": {
"schema": {
"type": "string"
}
}
},
"description": "The page as Markdown."
},
"404": {
"content": {
"text/markdown": {
"schema": {
"type": "string"
}
}
},
"description": "No page has that route; the body lists where to look next."
}
},
"summary": "Get a page as Markdown",
"tags": [
"Markdown"
]
}
},
"/llms.txt": {
"get": {
"description": "The llms.txt index: the site's summary, when to use it, and every page with a one-line description, grouped by section.",
"operationId": "getLlmsTxt",
"responses": {
"200": {
"content": {
"text/plain": {
"schema": {
"type": "string"
}
}
},
"description": "The index."
}
},
"summary": "Get llms.txt",
"tags": [
"Markdown"
]
}
},
"/llms-full.txt": {
"get": {
"description": "The full Markdown of every current-docs page in one file.",
"operationId": "getLlmsFullTxt",
"responses": {
"200": {
"content": {
"text/plain": {
"schema": {
"type": "string"
}
}
},
"description": "Every page's Markdown."
}
},
"summary": "Get llms-full.txt",
"tags": [
"Markdown"
]
}
},
"/agent-readability.json": {
"get": {
"description": "A manifest indexing every agent-facing artifact the site publishes — this API, the Markdown mirrors, llms.txt, the MCP server, feeds, and the sitemap.",
"operationId": "getAgentReadability",
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"type": "object"
}
}
},
"description": "The manifest."
}
},
"summary": "Get the agent-readability manifest",
"tags": [
"Discovery"
]
}
}
},
"security": [],
"servers": [
{
"description": "prdlk",
"url": "https://lc.prd.lk"
}
],
"tags": [
{
"description": "Enumerate and read documentation pages.",
"name": "Pages"
},
{
"description": "How the docs are organized.",
"name": "Navigation"
},
{
"description": "Plain-text and Markdown surfaces.",
"name": "Markdown"
},
{
"description": "Agent discovery documents.",
"name": "Discovery"
}
],
"externalDocs": {
"description": "prdlk documentation",
"url": "https://lc.prd.lk/"
}
};
export function GET() {
return new Response(JSON.stringify(payload), {
headers: { "Content-Type": "application/json" },
});
}