HASH c9aad19bfab6
DATE 2026-07-17
SUBJECT web: referenced-by backlinks from the wikilink graph + fix docs pagination
hrefs
FILES 11 CHANGED HASH c9aad19bfab6
DATE 2026-07-17
SUBJECT web: referenced-by backlinks from the
wikilink graph + fix docs pagination
hrefs
FILES 11 CHANGED ┌─ ooknet-design/src/components/ArticleBacklinks/ArticleBacklinks.astro ──── [ COPY ] ─┐
│ diff --git a/ooknet-design/src/components/ArticleBacklinks/ArticleBacklinks.astro │
│ b/ooknet-design/src/components/ArticleBacklinks/ArticleBacklinks.astro │
│ new file mode 100644 │
│ index 0000000..6303689 │
│ --- /dev/null │
│ +++ b/ooknet-design/src/components/ArticleBacklinks/ArticleBacklinks.astro │
│ @@ -0,0 +1,27 @@ │
│ +--- │
│ +import Pre from "../Pre/Pre.astro"; │
│ +import { FRAME_W, MOBILE_FRAME_W } from "../../lib/config"; │
│ +import type { Backlink } from "../../lib/wikilinks"; │
│ +import { BACKLINKS_LABEL, BACKLINKS_INDENT, layoutBacklinks, type Props } from ". │
│ /ArticleBacklinks"; │
│ +import "./ArticleBacklinks.scss"; │
│ + │
│ +const { refs } = Astro.props as Props; │
│ +const indent = " ".repeat(BACKLINKS_INDENT); │
│ + │
│ +const variants: [string, Backlink[]][] = [ │
│ + ["frame-wide", layoutBacklinks(refs, FRAME_W)], │
│ + ["frame-narrow", layoutBacklinks(refs, MOBILE_FRAME_W)], │
│ +]; │
│ +--- │
│ +{refs.length > 0 && ( │
│ + <nav class="backlinks" aria-label="Referenced by"> │
│ + {variants.map(([variant, rows]) => ( │
│ + <Fragment> │
│ + <Pre class={variant}>{BACKLINKS_LABEL}</Pre> │
│ + {rows.map((r) => ( │
│ + <Pre class={variant}>{indent}<a class="backlink-link" href={r.url}>{r.l │
│ abel}</a></Pre> │
│ + ))} │
│ + </Fragment> │
│ + ))} │
│ + </nav> │
│ +)} │
└────────────────────────────────────────────────────────────────────────────────────┘ ┌─ ...klinks/ArticleBacklinks.astro ── [ COPY ] ─┐
│ diff --git a/ooknet-design/src/components/Ar │
│ ticleBacklinks/ArticleBacklinks.astro b/ookn │
│ et-design/src/components/ArticleBacklinks/Ar │
│ ticleBacklinks.astro │
│ new file mode 100644 │
│ index 0000000..6303689 │
│ --- /dev/null │
│ +++ b/ooknet-design/src/components/ArticleBa │
│ cklinks/ArticleBacklinks.astro │
│ @@ -0,0 +1,27 @@ │
│ +--- │
│ +import Pre from "../Pre/Pre.astro"; │
│ +import { FRAME_W, MOBILE_FRAME_W } from ".. │
│ /../lib/config"; │
│ +import type { Backlink } from "../../lib/wi │
│ kilinks"; │
│ +import { BACKLINKS_LABEL, BACKLINKS_INDENT, │
│ layoutBacklinks, type Props } from "./Artic │
│ leBacklinks"; │
│ +import "./ArticleBacklinks.scss"; │
│ + │
│ +const { refs } = Astro.props as Props; │
│ +const indent = " ".repeat(BACKLINKS_INDENT) │
│ ; │
│ + │
│ +const variants: [string, Backlink[]][] = [ │
│ + ["frame-wide", layoutBacklinks(refs, FRAM │
│ E_W)], │
│ + ["frame-narrow", layoutBacklinks(refs, MO │
│ BILE_FRAME_W)], │
│ +]; │
│ +--- │
│ +{refs.length > 0 && ( │
│ + <nav class="backlinks" aria-label="Refere │
│ nced by"> │
│ + {variants.map(([variant, rows]) => ( │
│ + <Fragment> │
│ + <Pre class={variant}>{BACKLINKS_LAB │
│ EL}</Pre> │
│ + {rows.map((r) => ( │
│ + <Pre class={variant}>{indent}<a c │
│ lass="backlink-link" href={r.url}>{r.label}< │
│ /a></Pre> │
│ + ))} │
│ + </Fragment> │
│ + ))} │
│ + </nav> │
│ +)} │
└──────────────────────────────────────────────┘ ┌─ ooknet-design/src/components/ArticleBacklinks/ArticleBacklinks.scss ───── [ COPY ] ─┐
│ diff --git a/ooknet-design/src/components/ArticleBacklinks/ArticleBacklinks.scss b │
│ /ooknet-design/src/components/ArticleBacklinks/ArticleBacklinks.scss │
│ new file mode 100644 │
│ index 0000000..e0dcbf3 │
│ --- /dev/null │
│ +++ b/ooknet-design/src/components/ArticleBacklinks/ArticleBacklinks.scss │
│ @@ -0,0 +1,18 @@ │
│ +.backlinks { │
│ + display: block; │
│ +} │
│ + │
│ +.backlink-link { │
│ + color: var(--ink); │
│ + text-decoration: none; │
│ + │
│ + &:hover { │
│ + text-decoration: underline; │
│ + text-underline-offset: 3px; │
│ + } │
│ + │
│ + &:focus-visible { │
│ + outline: 1px dashed var(--ink); │
│ + outline-offset: 1px; │
│ + } │
│ +} │
└────────────────────────────────────────────────────────────────────────────────────┘ ┌─ ...cklinks/ArticleBacklinks.scss ── [ COPY ] ─┐
│ diff --git a/ooknet-design/src/components/Ar │
│ ticleBacklinks/ArticleBacklinks.scss b/ookne │
│ t-design/src/components/ArticleBacklinks/Art │
│ icleBacklinks.scss │
│ new file mode 100644 │
│ index 0000000..e0dcbf3 │
│ --- /dev/null │
│ +++ b/ooknet-design/src/components/ArticleBa │
│ cklinks/ArticleBacklinks.scss │
│ @@ -0,0 +1,18 @@ │
│ +.backlinks { │
│ + display: block; │
│ +} │
│ + │
│ +.backlink-link { │
│ + color: var(--ink); │
│ + text-decoration: none; │
│ + │
│ + &:hover { │
│ + text-decoration: underline; │
│ + text-underline-offset: 3px; │
│ + } │
│ + │
│ + &:focus-visible { │
│ + outline: 1px dashed var(--ink); │
│ + outline-offset: 1px; │
│ + } │
│ +} │
└──────────────────────────────────────────────┘ ┌─ ooknet-design/src/components/ArticleBacklinks/ArticleBacklinks.test.ts ── [ COPY ] ─┐
│ diff --git a/ooknet-design/src/components/ArticleBacklinks/ArticleBacklinks.test.t │
│ s b/ooknet-design/src/components/ArticleBacklinks/ArticleBacklinks.test.ts │
│ new file mode 100644 │
│ index 0000000..2d49ccd │
│ --- /dev/null │
│ +++ b/ooknet-design/src/components/ArticleBacklinks/ArticleBacklinks.test.ts │
│ @@ -0,0 +1,28 @@ │
│ +import { describe, expect, it } from "vitest"; │
│ +import { BACKLINKS_INDENT, layoutBacklinks } from "./ArticleBacklinks"; │
│ +import { len } from "../../lib/ascii"; │
│ + │
│ +const ref = (label: string) => ({ url: "/notes/x/", label }); │
│ + │
│ +describe("layoutBacklinks", () => { │
│ + it("passes short labels through", () => { │
│ + const rows = layoutBacklinks([ref("N0 - STYLE SHEET")], 86); │
│ + expect(rows[0].label).toBe("N0 - STYLE SHEET"); │
│ + }); │
│ + │
│ + it("truncates labels to the available width with an ellipsis", () => { │
│ + const long = "N218 - COLLECT DATA TO ANALYZE AND TROUBLESHOOT PERFORMANCE SCE │
│ NARIOS"; │
│ + const rows = layoutBacklinks([ref(long)], 48); │
│ + expect(len(rows[0].label)).toBe(48 - BACKLINKS_INDENT); │
│ + expect(rows[0].label.endsWith("...")).toBe(true); │
│ + }); │
│ + │
│ + it("keeps every row on the grid at both frame widths", () => { │
│ + const refs = [ref("N0 - STYLE SHEET"), ref("DOCS - FRAMES")]; │
│ + for (const w of [86, 48]) { │
│ + for (const r of layoutBacklinks(refs, w)) { │
│ + expect(len(r.label)).toBeLessThanOrEqual(w - BACKLINKS_INDENT); │
│ + } │
│ + } │
│ + }); │
│ +}); │
└────────────────────────────────────────────────────────────────────────────────────┘ ┌─ ...inks/ArticleBacklinks.test.ts ── [ COPY ] ─┐
│ diff --git a/ooknet-design/src/components/Ar │
│ ticleBacklinks/ArticleBacklinks.test.ts b/oo │
│ knet-design/src/components/ArticleBacklinks/ │
│ ArticleBacklinks.test.ts │
│ new file mode 100644 │
│ index 0000000..2d49ccd │
│ --- /dev/null │
│ +++ b/ooknet-design/src/components/ArticleBa │
│ cklinks/ArticleBacklinks.test.ts │
│ @@ -0,0 +1,28 @@ │
│ +import { describe, expect, it } from "vites │
│ t"; │
│ +import { BACKLINKS_INDENT, layoutBacklinks │
│ } from "./ArticleBacklinks"; │
│ +import { len } from "../../lib/ascii"; │
│ + │
│ +const ref = (label: string) => ({ url: "/no │
│ tes/x/", label }); │
│ + │
│ +describe("layoutBacklinks", () => { │
│ + it("passes short labels through", () => { │
│ + const rows = layoutBacklinks([ref("N0 - │
│ STYLE SHEET")], 86); │
│ + expect(rows[0].label).toBe("N0 - STYLE │
│ SHEET"); │
│ + }); │
│ + │
│ + it("truncates labels to the available wid │
│ th with an ellipsis", () => { │
│ + const long = "N218 - COLLECT DATA TO AN │
│ ALYZE AND TROUBLESHOOT PERFORMANCE SCENARIOS │
│ "; │
│ + const rows = layoutBacklinks([ref(long) │
│ ], 48); │
│ + expect(len(rows[0].label)).toBe(48 - BA │
│ CKLINKS_INDENT); │
│ + expect(rows[0].label.endsWith("...")).t │
│ oBe(true); │
│ + }); │
│ + │
│ + it("keeps every row on the grid at both f │
│ rame widths", () => { │
│ + const refs = [ref("N0 - STYLE SHEET"), │
│ ref("DOCS - FRAMES")]; │
│ + for (const w of [86, 48]) { │
│ + for (const r of layoutBacklinks(refs, │
│ w)) { │
│ + expect(len(r.label)).toBeLessThanOr │
│ Equal(w - BACKLINKS_INDENT); │
│ + } │
│ + } │
│ + }); │
│ +}); │
└──────────────────────────────────────────────┘ ┌─ ooknet-design/src/components/ArticleBacklinks/ArticleBacklinks.ts ─────── [ COPY ] ─┐
│ diff --git a/ooknet-design/src/components/ArticleBacklinks/ArticleBacklinks.ts b/o │
│ oknet-design/src/components/ArticleBacklinks/ArticleBacklinks.ts │
│ new file mode 100644 │
│ index 0000000..2d10737 │
│ --- /dev/null │
│ +++ b/ooknet-design/src/components/ArticleBacklinks/ArticleBacklinks.ts │
│ @@ -0,0 +1,25 @@ │
│ +// REFERENCED BY - pages linking here via wikilinks. Header row plus one │
│ +// linked entry per source, indented to the kv value column so the block │
│ +// sits flush with SEE ALSO / FILED / TAGS. │
│ +import { checkGrid, len } from "../../lib/ascii"; │
│ +import type { Backlink } from "../../lib/wikilinks"; │
│ + │
│ +export interface Props { │
│ + refs: Backlink[]; │
│ +} │
│ + │
│ +export const BACKLINKS_LABEL = "REFERENCED BY"; │
│ +export const BACKLINKS_INDENT = 12; │
│ + │
│ +/** Entries with labels truncated (...) to fit after the indent. */ │
│ +export function layoutBacklinks(refs: Backlink[], width: number): Backlink[] { │
│ + const avail = Math.max(1, width - BACKLINKS_INDENT); │
│ + return refs.map((r) => { │
│ + const label = │
│ + len(r.label) > avail │
│ + ? [...r.label].slice(0, avail - 1).join("") + "..." │
│ + : r.label; │
│ + checkGrid(label, avail, "backlink entry"); │
│ + return { url: r.url, label }; │
│ + }); │
│ +} │
└────────────────────────────────────────────────────────────────────────────────────┘ ┌─ ...Backlinks/ArticleBacklinks.ts ── [ COPY ] ─┐
│ diff --git a/ooknet-design/src/components/Ar │
│ ticleBacklinks/ArticleBacklinks.ts b/ooknet- │
│ design/src/components/ArticleBacklinks/Artic │
│ leBacklinks.ts │
│ new file mode 100644 │
│ index 0000000..2d10737 │
│ --- /dev/null │
│ +++ b/ooknet-design/src/components/ArticleBa │
│ cklinks/ArticleBacklinks.ts │
│ @@ -0,0 +1,25 @@ │
│ +// REFERENCED BY - pages linking here via w │
│ ikilinks. Header row plus one │
│ +// linked entry per source, indented to the │
│ kv value column so the block │
│ +// sits flush with SEE ALSO / FILED / TAGS. │
│ +import { checkGrid, len } from "../../lib/a │
│ scii"; │
│ +import type { Backlink } from "../../lib/wi │
│ kilinks"; │
│ + │
│ +export interface Props { │
│ + refs: Backlink[]; │
│ +} │
│ + │
│ +export const BACKLINKS_LABEL = "REFERENCED │
│ BY"; │
│ +export const BACKLINKS_INDENT = 12; │
│ + │
│ +/** Entries with labels truncated (...) to │
│ fit after the indent. */ │
│ +export function layoutBacklinks(refs: Backl │
│ ink[], width: number): Backlink[] { │
│ + const avail = Math.max(1, width - BACKLIN │
│ KS_INDENT); │
│ + return refs.map((r) => { │
│ + const label = │
│ + len(r.label) > avail │
│ + ? [...r.label].slice(0, avail - 1). │
│ join("") + "..." │
│ + : r.label; │
│ + checkGrid(label, avail, "backlink entry │
│ "); │
│ + return { url: r.url, label }; │
│ + }); │
│ +} │
└──────────────────────────────────────────────┘ ┌─ ooknet-design/src/components/ArticlePagination/ArticlePagination.astro ── [ COPY ] ─┐
│ diff --git a/ooknet-design/src/components/ArticlePagination/ArticlePagination.astr │
│ o b/ooknet-design/src/components/ArticlePagination/ArticlePagination.astro │
│ index b9e557d..7c45a82 100644 │
│ --- a/ooknet-design/src/components/ArticlePagination/ArticlePagination.astro │
│ +++ b/ooknet-design/src/components/ArticlePagination/ArticlePagination.astro │
│ @@ -16,7 +16,7 @@ const gap = (w: number) => │
│ --- │
│ {(prev || next) && ( │
│ <nav class="article-pagination" aria-label="Adjacent notes"> │
│ - <Pre class="frame-wide">{prevLabel && prev && <Button label={prevLabel} href= │
│ {`/notes/${prev.id}/`} />}{" ".repeat(gap(FRAME_W))}{nextLabel && next && <Button │
│ label={nextLabel} href={`/notes/${next.id}/`} />}</Pre> │
│ - <Pre class="frame-narrow">{prevLabel && prev && <Button label={prevLabel} hre │
│ f={`/notes/${prev.id}/`} />}{" ".repeat(gap(MOBILE_FRAME_W))}{nextLabel && next && │
│ <Button label={nextLabel} href={`/notes/${next.id}/`} />}</Pre> │
│ + <Pre class="frame-wide">{prevLabel && prev && <Button label={prevLabel} href= │
│ {prev.href} />}{" ".repeat(gap(FRAME_W))}{nextLabel && next && <Button label={next │
│ Label} href={next.href} />}</Pre> │
│ + <Pre class="frame-narrow">{prevLabel && prev && <Button label={prevLabel} hre │
│ f={prev.href} />}{" ".repeat(gap(MOBILE_FRAME_W))}{nextLabel && next && <Button la │
│ bel={nextLabel} href={next.href} />}</Pre> │
│ </nav> │
│ )} │
└────────────────────────────────────────────────────────────────────────────────────┘ ┌─ ...ation/ArticlePagination.astro ── [ COPY ] ─┐
│ diff --git a/ooknet-design/src/components/Ar │
│ ticlePagination/ArticlePagination.astro b/oo │
│ knet-design/src/components/ArticlePagination │
│ /ArticlePagination.astro │
│ index b9e557d..7c45a82 100644 │
│ --- a/ooknet-design/src/components/ArticlePa │
│ gination/ArticlePagination.astro │
│ +++ b/ooknet-design/src/components/ArticlePa │
│ gination/ArticlePagination.astro │
│ @@ -16,7 +16,7 @@ const gap = (w: number) => │
│ --- │
│ {(prev || next) && ( │
│ <nav class="article-pagination" aria-labe │
│ l="Adjacent notes"> │
│ - <Pre class="frame-wide">{prevLabel && p │
│ rev && <Button label={prevLabel} href={`/not │
│ es/${prev.id}/`} />}{" ".repeat(gap(FRAME_W) │
│ )}{nextLabel && next && <Button label={nextL │
│ abel} href={`/notes/${next.id}/`} />}</Pre> │
│ - <Pre class="frame-narrow">{prevLabel && │
│ prev && <Button label={prevLabel} href={`/n │
│ otes/${prev.id}/`} />}{" ".repeat(gap(MOBILE │
│ _FRAME_W))}{nextLabel && next && <Button lab │
│ el={nextLabel} href={`/notes/${next.id}/`} / │
│ >}</Pre> │
│ + <Pre class="frame-wide">{prevLabel && p │
│ rev && <Button label={prevLabel} href={prev. │
│ href} />}{" ".repeat(gap(FRAME_W))}{nextLabe │
│ l && next && <Button label={nextLabel} href= │
│ {next.href} />}</Pre> │
│ + <Pre class="frame-narrow">{prevLabel && │
│ prev && <Button label={prevLabel} href={pre │
│ v.href} />}{" ".repeat(gap(MOBILE_FRAME_W))} │
│ {nextLabel && next && <Button label={nextLab │
│ el} href={next.href} />}</Pre> │
│ </nav> │
│ )} │
└──────────────────────────────────────────────┘ ┌─ ooknet-design/src/components/ArticlePagination/ArticlePagination.ts ───── [ COPY ] ─┐
│ diff --git a/ooknet-design/src/components/ArticlePagination/ArticlePagination.ts b │
│ /ooknet-design/src/components/ArticlePagination/ArticlePagination.ts │
│ index 2136cfe..16a9d34 100644 │
│ --- a/ooknet-design/src/components/ArticlePagination/ArticlePagination.ts │
│ +++ b/ooknet-design/src/components/ArticlePagination/ArticlePagination.ts │
│ @@ -1,5 +1,5 @@ │
│ export interface PageLink { │
│ - id: string; │
│ + href: string; │
│ note: string; │
│ } │
│ │
└────────────────────────────────────────────────────────────────────────────────────┘ ┌─ ...gination/ArticlePagination.ts ── [ COPY ] ─┐
│ diff --git a/ooknet-design/src/components/Ar │
│ ticlePagination/ArticlePagination.ts b/ookne │
│ t-design/src/components/ArticlePagination/Ar │
│ ticlePagination.ts │
│ index 2136cfe..16a9d34 100644 │
│ --- a/ooknet-design/src/components/ArticlePa │
│ gination/ArticlePagination.ts │
│ +++ b/ooknet-design/src/components/ArticlePa │
│ gination/ArticlePagination.ts │
│ @@ -1,5 +1,5 @@ │
│ export interface PageLink { │
│ - id: string; │
│ + href: string; │
│ note: string; │
│ } │
│ │
└──────────────────────────────────────────────┘ ┌─ ooknet-design/src/content/docs/authoring/wikilinks.md ─────────────────── [ COPY ] ─┐
│ diff --git a/ooknet-design/src/content/docs/authoring/wikilinks.md b/ooknet-design │
│ /src/content/docs/authoring/wikilinks.md │
│ index 3e57328..e576a0d 100644 │
│ --- a/ooknet-design/src/content/docs/authoring/wikilinks.md │
│ +++ b/ooknet-design/src/content/docs/authoring/wikilinks.md │
│ @@ -34,3 +34,12 @@ wiki tradition is to show the gap. │
│ │
│ Anywhere markdown renders: notes, docs, and any prose the pipeline │
│ touches. They don't fire inside code fences or existing links. │
│ + │
│ +# Referenced by │
│ + │
│ +The graph runs both ways. Every resolved wikilink also feeds a │
│ +REFERENCED BY index on the target page - the pages that cite this one, │
│ +listed above the footer. Dead links, drafts, and self-references stay │
│ +out of the index, and literal syntax in code doesn't count as a │
│ +citation. Nothing to maintain: link a page and both directions appear │
│ +at the next build. │
└────────────────────────────────────────────────────────────────────────────────────┘ ┌─ ...t/docs/authoring/wikilinks.md ── [ COPY ] ─┐
│ diff --git a/ooknet-design/src/content/docs/ │
│ authoring/wikilinks.md b/ooknet-design/src/c │
│ ontent/docs/authoring/wikilinks.md │
│ index 3e57328..e576a0d 100644 │
│ --- a/ooknet-design/src/content/docs/authori │
│ ng/wikilinks.md │
│ +++ b/ooknet-design/src/content/docs/authori │
│ ng/wikilinks.md │
│ @@ -34,3 +34,12 @@ wiki tradition is to show │
│ the gap. │
│ │
│ Anywhere markdown renders: notes, docs, and │
│ any prose the pipeline │
│ touches. They don't fire inside code fences │
│ or existing links. │
│ + │
│ +# Referenced by │
│ + │
│ +The graph runs both ways. Every resolved wi │
│ kilink also feeds a │
│ +REFERENCED BY index on the target page - th │
│ e pages that cite this one, │
│ +listed above the footer. Dead links, drafts │
│ , and self-references stay │
│ +out of the index, and literal syntax in cod │
│ e doesn't count as a │
│ +citation. Nothing to maintain: link a page │
│ and both directions appear │
│ +at the next build. │
└──────────────────────────────────────────────┘ ┌─ ooknet-design/src/lib/wikilinks.test.ts ───────────────────────────────── [ COPY ] ─┐
│ diff --git a/ooknet-design/src/lib/wikilinks.test.ts b/ooknet-design/src/lib/wikil │
│ inks.test.ts │
│ index 7ee1d0f..de7c952 100644 │
│ --- a/ooknet-design/src/lib/wikilinks.test.ts │
│ +++ b/ooknet-design/src/lib/wikilinks.test.ts │
│ @@ -1,5 +1,5 @@ │
│ import { describe, expect, it } from "vitest"; │
│ -import { buildWikiMap, resolveWikilink } from "./wikilinks"; │
│ +import { buildBacklinks, buildWikiMap, extractWikiTargets, resolveWikilink } from │
│ "./wikilinks"; │
│ import { wikiNodes } from "./remark-wikilinks"; │
│ │
│ describe("buildWikiMap", () => { │
│ @@ -44,3 +44,49 @@ describe("wikiNodes", () => { │
│ expect(nodes[0].value).toContain("wikilink-dead"); │
│ }); │
│ }); │
│ + │
│ +describe("extractWikiTargets", () => { │
│ + it("collects targets, dropping aliases", () => { │
│ + expect(extractWikiTargets("see [[N217]] and [[N0|the sheet]]")).toEqual(["N21 │
│ 7", "N0"]); │
│ + }); │
│ + │
│ + it("ignores wikilinks inside inline code and fences", () => { │
│ + const body = "live [[N217]], literal `[[N999]]`\n```\n[[N1]]\n```\ndone"; │
│ + expect(extractWikiTargets(body)).toEqual(["N217"]); │
│ + }); │
│ +}); │
│ + │
│ +describe("buildBacklinks", () => { │
│ + const bl = buildBacklinks(); │
│ + const urls = (target: string) => (bl.get(target) ?? []).map((r) => r.url); │
│ + │
│ + it("inverts the graph: pages linking N217 appear on its entry", () => { │
│ + expect(urls("/notes/n217-entropy-bounds/")).toContain("/notes/n0-style-sheet/ │
│ "); │
│ + expect(urls("/notes/n217-entropy-bounds/")).toContain("/docs/authoring/filing │
│ -notes/"); │
│ + }); │
│ + │
│ + it("labels note sources with note number and title", () => { │
│ + const src = bl │
│ + .get("/notes/n217-entropy-bounds/")! │
│ + .find((r) => r.url === "/notes/n0-style-sheet/")!; │
│ + expect(src.label).toBe("N0 - STYLE SHEET"); │
│ + }); │
│ + │
│ + it("drops self-references", () => { │
│ + // n0 links [[Style Sheet]] (itself) - must not appear on its own list │
│ + expect(urls("/notes/n0-style-sheet/")).not.toContain("/notes/n0-style-sheet/" │
│ ); │
│ + }); │
│ + │
│ + it("never lists draft sources", () => { │
│ + for (const list of bl.values()) { │
│ + expect(list.map((r) => r.url)).not.toContain("/notes/n1-nixos-thoughts/"); │
│ + } │
│ + }); │
│ + │
│ + it("sorts note sources before doc sources", () => { │
│ + for (const list of bl.values()) { │
│ + const kinds = list.map((r) => (r.url.startsWith("/notes/") ? 0 : 1)); │
│ + expect(kinds).toEqual([...kinds].sort()); │
│ + } │
│ + }); │
│ +}); │
└────────────────────────────────────────────────────────────────────────────────────┘ ┌─ ...ign/src/lib/wikilinks.test.ts ── [ COPY ] ─┐
│ diff --git a/ooknet-design/src/lib/wikilinks │
│ .test.ts b/ooknet-design/src/lib/wikilinks.t │
│ est.ts │
│ index 7ee1d0f..de7c952 100644 │
│ --- a/ooknet-design/src/lib/wikilinks.test.t │
│ s │
│ +++ b/ooknet-design/src/lib/wikilinks.test.t │
│ s │
│ @@ -1,5 +1,5 @@ │
│ import { describe, expect, it } from "vites │
│ t"; │
│ -import { buildWikiMap, resolveWikilink } fr │
│ om "./wikilinks"; │
│ +import { buildBacklinks, buildWikiMap, extr │
│ actWikiTargets, resolveWikilink } from "./wi │
│ kilinks"; │
│ import { wikiNodes } from "./remark-wikilin │
│ ks"; │
│ │
│ describe("buildWikiMap", () => { │
│ @@ -44,3 +44,49 @@ describe("wikiNodes", () │
│ => { │
│ expect(nodes[0].value).toContain("wikil │
│ ink-dead"); │
│ }); │
│ }); │
│ + │
│ +describe("extractWikiTargets", () => { │
│ + it("collects targets, dropping aliases", │
│ () => { │
│ + expect(extractWikiTargets("see [[N217]] │
│ and [[N0|the sheet]]")).toEqual(["N217", "N │
│ 0"]); │
│ + }); │
│ + │
│ + it("ignores wikilinks inside inline code │
│ and fences", () => { │
│ + const body = "live [[N217]], literal `[ │
│ [N999]]`\n```\n[[N1]]\n```\ndone"; │
│ + expect(extractWikiTargets(body)).toEqua │
│ l(["N217"]); │
│ + }); │
│ +}); │
│ + │
│ +describe("buildBacklinks", () => { │
│ + const bl = buildBacklinks(); │
│ + const urls = (target: string) => (bl.get( │
│ target) ?? []).map((r) => r.url); │
│ + │
│ + it("inverts the graph: pages linking N217 │
│ appear on its entry", () => { │
│ + expect(urls("/notes/n217-entropy-bounds │
│ /")).toContain("/notes/n0-style-sheet/"); │
│ + expect(urls("/notes/n217-entropy-bounds │
│ /")).toContain("/docs/authoring/filing-notes │
│ /"); │
│ + }); │
│ + │
│ + it("labels note sources with note number │
│ and title", () => { │
│ + const src = bl │
│ + .get("/notes/n217-entropy-bounds/")! │
│ + .find((r) => r.url === "/notes/n0-sty │
│ le-sheet/")!; │
│ + expect(src.label).toBe("N0 - STYLE SHEE │
│ T"); │
│ + }); │
│ + │
│ + it("drops self-references", () => { │
│ + // n0 links [[Style Sheet]] (itself) - │
│ must not appear on its own list │
│ + expect(urls("/notes/n0-style-sheet/")). │
│ not.toContain("/notes/n0-style-sheet/"); │
│ + }); │
│ + │
│ + it("never lists draft sources", () => { │
│ + for (const list of bl.values()) { │
│ + expect(list.map((r) => r.url)).not.to │
│ Contain("/notes/n1-nixos-thoughts/"); │
│ + } │
│ + }); │
│ + │
│ + it("sorts note sources before doc sources │
│ ", () => { │
│ + for (const list of bl.values()) { │
│ + const kinds = list.map((r) => (r.url. │
│ startsWith("/notes/") ? 0 : 1)); │
│ + expect(kinds).toEqual([...kinds].sort │
│ ()); │
│ + } │
│ + }); │
│ +}); │
└──────────────────────────────────────────────┘ ┌─ ooknet-design/src/lib/wikilinks.ts ────────────────────────────────────── [ COPY ] ─┐
│ diff --git a/ooknet-design/src/lib/wikilinks.ts b/ooknet-design/src/lib/wikilinks. │
│ ts │
│ index eecda2d..d041d78 100644 │
│ --- a/ooknet-design/src/lib/wikilinks.ts │
│ +++ b/ooknet-design/src/lib/wikilinks.ts │
│ @@ -53,3 +53,79 @@ export function buildWikiMap(contentDir = "src/content"): WikiM │
│ ap { │
│ export function resolveWikilink(target: string, map: WikiMap): string | null { │
│ return map.get(target.trim().toLowerCase()) ?? null; │
│ } │
│ + │
│ +// ── Backlinks ──────────────────────────────────────────────────────── │
│ +// The same graph, inverted: for each page, which pages link to it. │
│ + │
│ +export interface Backlink { │
│ + url: string; │
│ + label: string; │
│ +} │
│ + │
│ +const WIKI_RE = /\[\[([^\]|]+)(?:\|[^\]]+)?\]\]/g; │
│ + │
│ +/** Wikilink targets in a markdown body. Code is stripped first so │
│ + * literal `[[syntax]]` in docs doesn't count as a reference - mirrors │
│ + * the remark plugin, which never sees code as text nodes. */ │
│ +export function extractWikiTargets(body: string): string[] { │
│ + const stripped = body │
│ + .replace(/^(?:```|~~~)[\s\S]*?^(?:```|~~~)/gm, "") │
│ + .replace(/`[^`\n]+`/g, ""); │
│ + return [...stripped.matchAll(WIKI_RE)].map((m) => m[1].trim()); │
│ +} │
│ + │
│ +function scanSources( │
│ + out: Map<string, Backlink[]>, │
│ + map: WikiMap, │
│ + dir: string, │
│ + urlFor: (id: string) => string, │
│ + labelFor: (id: string, fm: string) => string, │
│ +) { │
│ + let files: string[]; │
│ + try { │
│ + files = mdFiles(dir); │
│ + } catch { │
│ + return; │
│ + } │
│ + for (const file of files) { │
│ + const id = file.replace(/\.md$/, ""); │
│ + const raw = readFileSync(join(dir, file), "utf8"); │
│ + const fmMatch = raw.match(FM_RE); │
│ + const fm = fmMatch?.[1] ?? ""; │
│ + if (fmField(fm, "draft") === "true") continue; │
│ + const srcUrl = urlFor(id); │
│ + const targets = new Set<string>(); │
│ + for (const t of extractWikiTargets(raw.slice(fmMatch?.[0].length ?? 0))) { │
│ + const url = resolveWikilink(t, map); │
│ + if (url && url !== srcUrl) targets.add(url); │
│ + } │
│ + for (const target of targets) { │
│ + if (!out.has(target)) out.set(target, []); │
│ + out.get(target)!.push({ url: srcUrl, label: labelFor(id, fm) }); │
│ + } │
│ + } │
│ +} │
│ + │
│ +/** Target url ? pages whose body links to it. Dead links and │
│ + * self-references are dropped; notes sort before docs. */ │
│ +export function buildBacklinks(contentDir = "src/content"): Map<string, Backlink[ │
│ ]> { │
│ + const map = buildWikiMap(contentDir); │
│ + const out = new Map<string, Backlink[]>(); │
│ + scanSources(out, map, join(contentDir, "notes"), (id) => `/notes/${id}/`, (id, │
│ fm) => { │
│ + const note = fmField(fm, "note"); │
│ + const title = fmField(fm, "title") ?? id.toUpperCase(); │
│ + return note ? `${note} - ${title}` : title; │
│ + }); │
│ + scanSources(out, map, join(contentDir, "docs"), (id) => `/docs/${id}/`, (id, fm │
│ ) => { │
│ + return `DOCS - ${(fmField(fm, "title") ?? id).toUpperCase()}`; │
│ + }); │
│ + const group = (u: string) => (u.startsWith("/notes/") ? 0 : 1); │
│ + for (const list of out.values()) { │
│ + list.sort( │
│ + (a, b) => │
│ + group(a.url) - group(b.url) || │
│ + a.label.localeCompare(b.label, "en", { numeric: true }), │
│ + ); │
│ + } │
│ + return out; │
│ +} │
└────────────────────────────────────────────────────────────────────────────────────┘ ┌─ ...t-design/src/lib/wikilinks.ts ── [ COPY ] ─┐
│ diff --git a/ooknet-design/src/lib/wikilinks │
│ .ts b/ooknet-design/src/lib/wikilinks.ts │
│ index eecda2d..d041d78 100644 │
│ --- a/ooknet-design/src/lib/wikilinks.ts │
│ +++ b/ooknet-design/src/lib/wikilinks.ts │
│ @@ -53,3 +53,79 @@ export function buildWiki │
│ Map(contentDir = "src/content"): WikiMap { │
│ export function resolveWikilink(target: str │
│ ing, map: WikiMap): string | null { │
│ return map.get(target.trim().toLowerCase( │
│ )) ?? null; │
│ } │
│ + │
│ +// ── Backlinks ─────────────────────────── │
│ ───────────────────────────── │
│ +// The same graph, inverted: for each page, │
│ which pages link to it. │
│ + │
│ +export interface Backlink { │
│ + url: string; │
│ + label: string; │
│ +} │
│ + │
│ +const WIKI_RE = /\[\[([^\]|]+)(?:\|[^\]]+)? │
│ \]\]/g; │
│ + │
│ +/** Wikilink targets in a markdown body. Co │
│ de is stripped first so │
│ + * literal `[[syntax]]` in docs doesn't co │
│ unt as a reference - mirrors │
│ + * the remark plugin, which never sees cod │
│ e as text nodes. */ │
│ +export function extractWikiTargets(body: st │
│ ring): string[] { │
│ + const stripped = body │
│ + .replace(/^(?:```|~~~)[\s\S]*?^(?:```|~ │
│ ~~)/gm, "") │
│ + .replace(/`[^`\n]+`/g, ""); │
│ + return [...stripped.matchAll(WIKI_RE)].ma │
│ p((m) => m[1].trim()); │
│ +} │
│ + │
│ +function scanSources( │
│ + out: Map<string, Backlink[]>, │
│ + map: WikiMap, │
│ + dir: string, │
│ + urlFor: (id: string) => string, │
│ + labelFor: (id: string, fm: string) => str │
│ ing, │
│ +) { │
│ + let files: string[]; │
│ + try { │
│ + files = mdFiles(dir); │
│ + } catch { │
│ + return; │
│ + } │
│ + for (const file of files) { │
│ + const id = file.replace(/\.md$/, ""); │
│ + const raw = readFileSync(join(dir, file │
│ ), "utf8"); │
│ + const fmMatch = raw.match(FM_RE); │
│ + const fm = fmMatch?.[1] ?? ""; │
│ + if (fmField(fm, "draft") === "true") co │
│ ntinue; │
│ + const srcUrl = urlFor(id); │
│ + const targets = new Set<string>(); │
│ + for (const t of extractWikiTargets(raw. │
│ slice(fmMatch?.[0].length ?? 0))) { │
│ + const url = resolveWikilink(t, map); │
│ + if (url && url !== srcUrl) targets.ad │
│ d(url); │
│ + } │
│ + for (const target of targets) { │
│ + if (!out.has(target)) out.set(target, │
│ []); │
│ + out.get(target)!.push({ url: srcUrl, │
│ label: labelFor(id, fm) }); │
│ + } │
│ + } │
│ +} │
│ + │
│ +/** Target url ? pages whose body links to │
│ it. Dead links and │
│ + * self-references are dropped; notes sort │
│ before docs. */ │
│ +export function buildBacklinks(contentDir = │
│ "src/content"): Map<string, Backlink[]> { │
│ + const map = buildWikiMap(contentDir); │
│ + const out = new Map<string, Backlink[]>() │
│ ; │
│ + scanSources(out, map, join(contentDir, "n │
│ otes"), (id) => `/notes/${id}/`, (id, fm) => │
│ { │
│ + const note = fmField(fm, "note"); │
│ + const title = fmField(fm, "title") ?? i │
│ d.toUpperCase(); │
│ + return note ? `${note} - ${title}` : ti │
│ tle; │
│ + }); │
│ + scanSources(out, map, join(contentDir, "d │
│ ocs"), (id) => `/docs/${id}/`, (id, fm) => { │
│ + return `DOCS - ${(fmField(fm, "title") │
│ ?? id).toUpperCase()}`; │
│ + }); │
│ + const group = (u: string) => (u.startsWit │
│ h("/notes/") ? 0 : 1); │
│ + for (const list of out.values()) { │
│ + list.sort( │
│ + (a, b) => │
│ + group(a.url) - group(b.url) || │
│ + a.label.localeCompare(b.label, "en" │
│ , { numeric: true }), │
│ + ); │
│ + } │
│ + return out; │
│ +} │
└──────────────────────────────────────────────┘ ┌─ ooknet-design/src/pages/docs/[...slug].astro ──────────────────────────── [ COPY ] ─┐
│ diff --git a/ooknet-design/src/pages/docs/[...slug].astro b/ooknet-design/src/page │
│ s/docs/[...slug].astro │
│ index c4e1332..97c33ee 100644 │
│ --- a/ooknet-design/src/pages/docs/[...slug].astro │
│ +++ b/ooknet-design/src/pages/docs/[...slug].astro │
│ @@ -10,12 +10,15 @@ import Prose from "../../components/Prose/Prose.astro"; │
│ import ArticlePagination from "../../components/ArticlePagination/ArticlePaginati │
│ on.astro"; │
│ import type { PageLink } from "../../components/ArticlePagination/ArticlePaginati │
│ on"; │
│ import DocsNav from "../../components/DocsNav/DocsNav.astro"; │
│ +import ArticleBacklinks from "../../components/ArticleBacklinks/ArticleBacklinks. │
│ astro"; │
│ import { box, center, len, rule, ruleD, row2, wrap } from "../../lib/ascii"; │
│ import { published } from "../../lib/notes"; │
│ import { docsOrder, type DocEntry } from "../../lib/docs"; │
│ +import { buildBacklinks, type Backlink } from "../../lib/wikilinks"; │
│ │
│ export async function getStaticPaths() { │
│ const docs = await getCollection("docs", published); │
│ + const backlinks = buildBacklinks(); │
│ const ordered = docsOrder( │
│ docs.map((d) => ({ id: d.id, title: d.data.title, order: d.data.order })), │
│ ); │
│ @@ -27,7 +30,7 @@ export async function getStaticPaths() { │
│ if (!id) return null; │
│ const title = byId.get(id)!.data.title.toUpperCase(); │
│ const note = len(title) > 18 ? [...title].slice(0, 17).join("") + "..." : tit │
│ le; │
│ - return { id, note }; │
│ + return { href: `/docs/${id}/`, note }; │
│ }; │
│ │
│ return ordered.map((d, i) => ({ │
│ @@ -37,6 +40,7 @@ export async function getStaticPaths() { │
│ prev: link(ordered[i - 1]?.id), │
│ next: link(ordered[i + 1]?.id), │
│ all: ordered, │
│ + refs: backlinks.get(`/docs/${d.id}/`) ?? [], │
│ }, │
│ })); │
│ } │
│ @@ -46,8 +50,9 @@ interface Props { │
│ prev: PageLink | null; │
│ next: PageLink | null; │
│ all: DocEntry[]; │
│ + refs: Backlink[]; │
│ } │
│ -const { doc, prev, next, all } = Astro.props as Props; │
│ +const { doc, prev, next, all, refs } = Astro.props as Props; │
│ const { Content, headings } = await render(doc); │
│ │
│ const buildTitleCard = (w: number) => { │
│ @@ -71,6 +76,12 @@ const buildTitleCard = (w: number) => { │
│ <DocsNav docs={all} currentId={doc.id} /> │
│ {headings.length >= 3 && <TableOfContents headings={headings} />} │
│ <Prose><Content /></Prose> │
│ + {refs.length > 0 && ( │
│ + <Fragment> │
│ + <Frame build={(w) => rule(w)} /> │
│ + <ArticleBacklinks refs={refs} /> │
│ + </Fragment> │
│ + )} │
│ <Pre>{" "}</Pre> │
│ <ArticlePagination prev={prev} next={next} /> │
│ <Frame build={(w) => rule(w)} /> │
└────────────────────────────────────────────────────────────────────────────────────┘ ┌─ ...rc/pages/docs/[...slug].astro ── [ COPY ] ─┐
│ diff --git a/ooknet-design/src/pages/docs/[. │
│ ..slug].astro b/ooknet-design/src/pages/docs │
│ /[...slug].astro │
│ index c4e1332..97c33ee 100644 │
│ --- a/ooknet-design/src/pages/docs/[...slug] │
│ .astro │
│ +++ b/ooknet-design/src/pages/docs/[...slug] │
│ .astro │
│ @@ -10,12 +10,15 @@ import Prose from "../.. │
│ /components/Prose/Prose.astro"; │
│ import ArticlePagination from "../../compon │
│ ents/ArticlePagination/ArticlePagination.ast │
│ ro"; │
│ import type { PageLink } from "../../compon │
│ ents/ArticlePagination/ArticlePagination"; │
│ import DocsNav from "../../components/DocsN │
│ av/DocsNav.astro"; │
│ +import ArticleBacklinks from "../../compone │
│ nts/ArticleBacklinks/ArticleBacklinks.astro" │
│ ; │
│ import { box, center, len, rule, ruleD, row │
│ 2, wrap } from "../../lib/ascii"; │
│ import { published } from "../../lib/notes" │
│ ; │
│ import { docsOrder, type DocEntry } from ". │
│ ./../lib/docs"; │
│ +import { buildBacklinks, type Backlink } fr │
│ om "../../lib/wikilinks"; │
│ │
│ export async function getStaticPaths() { │
│ const docs = await getCollection("docs", │
│ published); │
│ + const backlinks = buildBacklinks(); │
│ const ordered = docsOrder( │
│ docs.map((d) => ({ id: d.id, title: d.d │
│ ata.title, order: d.data.order })), │
│ ); │
│ @@ -27,7 +30,7 @@ export async function getS │
│ taticPaths() { │
│ if (!id) return null; │
│ const title = byId.get(id)!.data.title. │
│ toUpperCase(); │
│ const note = len(title) > 18 ? [...titl │
│ e].slice(0, 17).join("") + "..." : title; │
│ - return { id, note }; │
│ + return { href: `/docs/${id}/`, note }; │
│ }; │
│ │
│ return ordered.map((d, i) => ({ │
│ @@ -37,6 +40,7 @@ export async function getS │
│ taticPaths() { │
│ prev: link(ordered[i - 1]?.id), │
│ next: link(ordered[i + 1]?.id), │
│ all: ordered, │
│ + refs: backlinks.get(`/docs/${d.id}/`) │
│ ?? [], │
│ }, │
│ })); │
│ } │
│ @@ -46,8 +50,9 @@ interface Props { │
│ prev: PageLink | null; │
│ next: PageLink | null; │
│ all: DocEntry[]; │
│ + refs: Backlink[]; │
│ } │
│ -const { doc, prev, next, all } = Astro.prop │
│ s as Props; │
│ +const { doc, prev, next, all, refs } = Astr │
│ o.props as Props; │
│ const { Content, headings } = await render( │
│ doc); │
│ │
│ const buildTitleCard = (w: number) => { │
│ @@ -71,6 +76,12 @@ const buildTitleCard = (w │
│ : number) => { │
│ <DocsNav docs={all} currentId={doc.id} /> │
│ {headings.length >= 3 && <TableOfContents │
│ headings={headings} />} │
│ <Prose><Content /></Prose> │
│ + {refs.length > 0 && ( │
│ + <Fragment> │
│ + <Frame build={(w) => rule(w)} /> │
│ + <ArticleBacklinks refs={refs} /> │
│ + </Fragment> │
│ + )} │
│ <Pre>{" "}</Pre> │
│ <ArticlePagination prev={prev} next={next │
│ } /> │
│ <Frame build={(w) => rule(w)} /> │
└──────────────────────────────────────────────┘ ┌─ ooknet-design/src/pages/notes/[slug].astro ────────────────────────────── [ COPY ] ─┐
│ diff --git a/ooknet-design/src/pages/notes/[slug].astro b/ooknet-design/src/pages/ │
│ notes/[slug].astro │
│ index b7c1968..fe783ff 100644 │
│ --- a/ooknet-design/src/pages/notes/[slug].astro │
│ +++ b/ooknet-design/src/pages/notes/[slug].astro │
│ @@ -13,16 +13,24 @@ import ArticlePagination from "../../components/ArticlePaginat │
│ ion/ArticlePaginat │
│ import ArticleFooter from "../../components/ArticleFooter/ArticleFooter.astro"; │
│ import Prose from "../../components/Prose/Prose.astro"; │
│ import Pre from "../../components/Pre/Pre.astro"; │
│ +import ArticleBacklinks from "../../components/ArticleBacklinks/ArticleBacklinks. │
│ astro"; │
│ import type { PageLink } from "../../components/ArticlePagination/ArticlePaginati │
│ on"; │
│ import { published, byNoteNum } from "../../lib/notes"; │
│ +import { buildBacklinks, type Backlink } from "../../lib/wikilinks"; │
│ │
│ export async function getStaticPaths() { │
│ const notes = (await getCollection("notes", published)).sort(byNoteNum); │
│ + const backlinks = buildBacklinks(); │
│ const link = (n?: (typeof notes)[number]): PageLink | null => │
│ - n ? { id: n.id, note: n.data.note } : null; │
│ + n ? { href: `/notes/${n.id}/`, note: n.data.note } : null; │
│ return notes.map((n, i) => ({ │
│ params: { slug: n.id }, │
│ - props: { note: n, prev: link(notes[i - 1]), next: link(notes[i + 1]) }, │
│ + props: { │
│ + note: n, │
│ + prev: link(notes[i - 1]), │
│ + next: link(notes[i + 1]), │
│ + refs: backlinks.get(`/notes/${n.id}/`) ?? [], │
│ + }, │
│ })); │
│ } │
│ │
│ @@ -30,8 +38,9 @@ interface Props { │
│ note: CollectionEntry<"notes">; │
│ prev: PageLink | null; │
│ next: PageLink | null; │
│ + refs: Backlink[]; │
│ } │
│ -const { note, prev, next } = Astro.props as Props; │
│ +const { note, prev, next, refs } = Astro.props as Props; │
│ const { Content, headings } = await render(note); │
│ --- │
│ <ArticleLayout title={note.data.title}> │
│ @@ -43,6 +52,7 @@ const { Content, headings } = await render(note); │
│ {headings.length >= 3 && <TableOfContents headings={headings} />} │
│ <Prose><Content /></Prose> │
│ <ArticleSeeAlso fm={note.data} /> │
│ + <ArticleBacklinks refs={refs} /> │
│ <ArticleTags fm={note.data} /> │
│ <Pre>{" "}</Pre> │
│ <ArticlePagination prev={prev} next={next} /> │
└────────────────────────────────────────────────────────────────────────────────────┘ ┌─ .../src/pages/notes/[slug].astro ── [ COPY ] ─┐
│ diff --git a/ooknet-design/src/pages/notes/[ │
│ slug].astro b/ooknet-design/src/pages/notes/ │
│ [slug].astro │
│ index b7c1968..fe783ff 100644 │
│ --- a/ooknet-design/src/pages/notes/[slug].a │
│ stro │
│ +++ b/ooknet-design/src/pages/notes/[slug].a │
│ stro │
│ @@ -13,16 +13,24 @@ import ArticlePagination │
│ from "../../components/ArticlePagination/Ar │
│ ticlePaginat │
│ import ArticleFooter from "../../components │
│ /ArticleFooter/ArticleFooter.astro"; │
│ import Prose from "../../components/Prose/P │
│ rose.astro"; │
│ import Pre from "../../components/Pre/Pre.a │
│ stro"; │
│ +import ArticleBacklinks from "../../compone │
│ nts/ArticleBacklinks/ArticleBacklinks.astro" │
│ ; │
│ import type { PageLink } from "../../compon │
│ ents/ArticlePagination/ArticlePagination"; │
│ import { published, byNoteNum } from "../.. │
│ /lib/notes"; │
│ +import { buildBacklinks, type Backlink } fr │
│ om "../../lib/wikilinks"; │
│ │
│ export async function getStaticPaths() { │
│ const notes = (await getCollection("notes │
│ ", published)).sort(byNoteNum); │
│ + const backlinks = buildBacklinks(); │
│ const link = (n?: (typeof notes)[number]) │
│ : PageLink | null => │
│ - n ? { id: n.id, note: n.data.note } : n │
│ ull; │
│ + n ? { href: `/notes/${n.id}/`, note: n. │
│ data.note } : null; │
│ return notes.map((n, i) => ({ │
│ params: { slug: n.id }, │
│ - props: { note: n, prev: link(notes[i - │
│ 1]), next: link(notes[i + 1]) }, │
│ + props: { │
│ + note: n, │
│ + prev: link(notes[i - 1]), │
│ + next: link(notes[i + 1]), │
│ + refs: backlinks.get(`/notes/${n.id}/` │
│ ) ?? [], │
│ + }, │
│ })); │
│ } │
│ │
│ @@ -30,8 +38,9 @@ interface Props { │
│ note: CollectionEntry<"notes">; │
│ prev: PageLink | null; │
│ next: PageLink | null; │
│ + refs: Backlink[]; │
│ } │
│ -const { note, prev, next } = Astro.props as │
│ Props; │
│ +const { note, prev, next, refs } = Astro.pr │
│ ops as Props; │
│ const { Content, headings } = await render( │
│ note); │
│ --- │
│ <ArticleLayout title={note.data.title}> │
│ @@ -43,6 +52,7 @@ const { Content, headings │
│ } = await render(note); │
│ {headings.length >= 3 && <TableOfContents │
│ headings={headings} />} │
│ <Prose><Content /></Prose> │
│ <ArticleSeeAlso fm={note.data} /> │
│ + <ArticleBacklinks refs={refs} /> │
│ <ArticleTags fm={note.data} /> │
│ <Pre>{" "}</Pre> │
│ <ArticlePagination prev={prev} next={next │
│ } /> │
└──────────────────────────────────────────────┘ [ BACK TO LOG ] ──────────────────────────────────────────────────────────────────────────────────────
OOKNET ────────────────────────────────────────────────
OOKNET