docs(docs): add BacklinksList component and shared tagTarget logic for badge routing, update docs pages

This commit is contained in:
Prad Nukala committed 2026-09-29 15:12:54 -04:00
1 parent eea8d242e0
commit 40e965aa06
5 files changed
+161 -23

No files matched your search

+84
View File
@@ -0,0 +1,84 @@
---
/**
* The `PageFooter` slot: an Obsidian-style backlinks list under a topic landing
* or guide page — every problem page that points at it, as `<num> <name>`
* rows in LeetCode-number order. Pages nothing points at (problem pages, the
* overview, the changelog) render nothing.
*
* A problem points at a page when either
* - one of its topic badges links there — the same `tagTarget()` answer
* `ProblemBadges.astro` renders, so this list and the badges cannot drift
* (a DFS problem is a backlink of the one DFS landing its badge picks); or
* - it lives under the page's route (`/hybrid-structures/lru-cache` under
* `/hybrid-structures`), so a topic no LeetCode tag names still lists its
* own problems.
*
* Problem pages are recognised by their `<num>. <name>` title, the shape
* `apps/cli/sync.ts` writes.
*/
import { getEntry } from "astro:content";
import data from "blume:data";
import { withBase } from "blume/components/islands/base-path.ts";
import { tagTarget, type TopicTag } from "../topic-landings.ts";
interface Props {
route: string;
}
interface Route {
path: string;
title: string;
collection: string;
entryId: string;
}
const { route } = Astro.props;
const routes: Route[] = data.routes;
const served = new Set(routes.map((r) => r.path));
const problems = routes.flatMap((r) => {
const m = r.title.match(/^(\d+)\.\s+(.+)$/);
return m ? [{ route: r, num: Number(m[1]), name: m[2]! }] : [];
});
// A problem page is never a backlink target: skip the per-problem lookups there.
const backlinks = problems.some((p) => p.route.path === route)
? []
: (
await Promise.all(
problems.map(async (p) => {
if (p.route.path.startsWith(`${route}/`)) return p;
const entry = await getEntry(p.route.collection, p.route.entryId);
const tags: TopicTag[] = entry?.data.topics ?? [];
const page = { route: p.route.path, tags };
return tags.some((tag) => tagTarget(tag, page, served).href === route) ? p : undefined;
}),
)
)
.filter((p) => p !== undefined)
.sort((a, b) => a.num - b.num);
---
{
backlinks.length > 0 && (
<section aria-labelledby="backlinks" class="not-prose mx-auto mt-12 max-w-content border-border border-t pt-6">
<h2 class="mb-2 flex items-baseline gap-2 font-medium text-muted-foreground text-xs uppercase tracking-wide" id="backlinks">
Backlinks
<span class="tabular-nums">{backlinks.length}</span>
</h2>
<ul class="text-sm">
{backlinks.map((p) => (
<li>
<a
class="-mx-2 flex items-baseline gap-3 rounded-md px-2 py-1 text-foreground no-underline transition-colors hover:bg-muted"
href={withBase(p.route.path)}
>
<span class="w-10 shrink-0 text-right font-mono text-muted-foreground text-xs tabular-nums">{p.num}</span>
<span class="truncate">{p.name}</span>
</a>
</li>
))}
</ul>
</section>
)
}
+7 -22
View File
@@ -10,33 +10,23 @@
* topic tags. A tag the campaign teaches (`topic-landings.ts`) links to that
* topic's landing page and reads accent; any other tag links to its guide
* page, `/guides/<slug>` (sync creates one per such tag), and reads neutral.
* `tagTarget()` picks the route (and between two landings, e.g. DFS); the
* `PageFooter` backlinks list reads the same answer.
* - `video:` (problem pages, written by `apps/cli/videos.ts`) is the NeetCode
* walkthrough's YouTube id, the red "Explanation" badge at the end of the row.
* - `list:` (guide pages, written by sync on create) is the tag's LeetCode
* problem list, an orange "LeetCode list" badge.
*
* A tag naming two campaign topics (`Depth-First Search` is both Tree DFS and
* Graph DFS) links to the page's own topic — its route's first segment — when
* that is one of them; otherwise DFS/BFS prefer the tree topic for a problem
* LeetCode also tags `Tree` and the graph topic for the rest. A route the site
* does not serve (a landing or guide page removed) falls back to LeetCode's
* problem list for the tag, so a badge never links to a 404.
*/
import { getEntry } from "astro:content";
import data from "blume:data";
import Badge from "blume/components/content/Badge.astro";
import { withBase } from "blume/components/islands/base-path.ts";
import { TOPIC_LANDINGS } from "../topic-landings.ts";
import { tagTarget, type TopicTag } from "../topic-landings.ts";
interface Props {
route: string;
}
interface TopicTag {
name: string;
slug: string;
}
const { route } = Astro.props;
const at = data.routes.find((r: { path: string }) => r.path === route);
const entry = at ? await getEntry(at.collection, at.entryId) : undefined;
@@ -44,17 +34,12 @@ const tags: TopicTag[] = entry?.data.topics ?? [];
const video: string | undefined = entry?.data.video;
const list: string | undefined = entry?.data.list;
const served = new Set(data.routes.map((r: { path: string }) => r.path));
const own = `/${route.split("/")[1]}`;
const prefer = tags.some((t) => t.slug === "tree") ? "/tree-" : "/graph-";
const served = new Set<string>(data.routes.map((r: { path: string }) => r.path));
const badges = tags.map((tag) => {
const landings = (TOPIC_LANDINGS[tag.slug] ?? []).filter((path) => served.has(path));
const landing = landings.includes(own) ? own : (landings.find((p) => p.startsWith(prefer)) ?? landings[0]);
if (landing) return { name: tag.name, href: withBase(landing), accent: true, external: false };
const guide = `/guides/${tag.slug}`;
if (served.has(guide)) return { name: tag.name, href: withBase(guide), accent: false, external: false };
return { name: tag.name, href: `https://leetcode.com/problem-list/${tag.slug}/`, accent: false, external: true };
const target = tagTarget(tag, { route, tags }, served);
const external = target.kind === "external";
return { name: tag.name, href: external ? target.href : withBase(target.href), accent: target.kind === "landing", external };
});
const linkClass = "no-underline transition-opacity hover:opacity-80";