Skip to content

Commit a5403a4

Browse files
TMHSDigitalclaude
andcommitted
site: fixes from the entry pages review
- Unescape any backslash-escaped ASCII punctuation, so quoted evidence on four pages no longer shows \" literally. - Undo YAML quote escapes in frontmatter strings; the ccmixter credit line now copies without backslashes. - llms-full.txt carries each entry's whole first paragraph and absolute links in place of repo-relative ones. - Restore the homepage scroll position only on Back or Forward, and do not save it on a click that opens a new tab. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Wc67NX9MM964uaVGn4bpZJ
1 parent 2f110ad commit a5403a4

6 files changed

Lines changed: 103 additions & 19 deletions

File tree

‎site/build.mjs‎

Lines changed: 27 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import fs from "node:fs";
77
import path from "node:path";
88
import { fileURLToPath } from "node:url";
9-
import { commercialLabel, esc, PERSPECTIVE_LABELS, verifiedAge } from "./lib/shared.mjs";
9+
import { commercialLabel, esc, PERSPECTIVE_LABELS, unquoteScalar, verifiedAge } from "./lib/shared.mjs";
1010
import { entryPageHtml } from "./lib/entry-page.mjs";
1111
import { LinkError, makeLinkResolver } from "./lib/links.mjs";
1212
import { llmsFullTxt, llmsTxt } from "./lib/llms.mjs";
@@ -38,7 +38,7 @@ function parseScalar(raw) {
3838
(v.startsWith('"') && v.endsWith('"')) ||
3939
(v.startsWith("'") && v.endsWith("'"))
4040
) {
41-
return v.slice(1, -1);
41+
return unquoteScalar(v);
4242
}
4343
if (v.startsWith("[") && v.endsWith("]")) {
4444
const inner = v.slice(1, -1).trim();
@@ -382,16 +382,22 @@ function copyDir(src, dest) {
382382
}
383383
}
384384

385-
/** Writes dist/entry/<id>/index.html for every entry. */
386-
function writeEntryPages({ entries, bodies, config, stats, stamp, hasCard, now }) {
387-
const errors = [];
388-
const written = [];
385+
/** Returns entry => link resolver for links in that entry's body. */
386+
function linkResolvers(entries, config) {
389387
const idByPath = new Map(entries.map((e) => [e.path, e.id]));
390388
const kindOf = (repoPath) => {
391389
const full = path.join(ROOT, repoPath);
392390
if (!fs.existsSync(full)) return null;
393391
return fs.statSync(full).isDirectory() ? "dir" : "file";
394392
};
393+
return (entry) => makeLinkResolver({ entryPath: entry.path, idByPath, repo: config.site.repo, kindOf });
394+
}
395+
396+
/** Writes dist/entry/<id>/index.html for every entry. */
397+
function writeEntryPages({ entries, bodies, config, stats, stamp, hasCard, now }) {
398+
const errors = [];
399+
const written = [];
400+
const resolverFor = linkResolvers(entries, config);
395401
// Prev/next follow the homepage's default order: category, then name.
396402
const visible = entries.filter((e) => e.status !== "deprecated");
397403
const neighbours = new Map();
@@ -403,7 +409,7 @@ function writeEntryPages({ entries, bodies, config, stats, stamp, hasCard, now }
403409
const body = bodies.get(entry.id) || "";
404410
const file = `${entry.path} (body)`;
405411
try {
406-
const resolveLink = makeLinkResolver({ entryPath: entry.path, idByPath, repo: config.site.repo, kindOf });
412+
const resolveLink = resolverFor(entry);
407413
const { lead, rest, restFirstLine } = splitEntryBody(body);
408414
const leadHtml = renderBlocks(lead, { file, resolveLink });
409415
const restHtml = renderBlocks(rest, { file, resolveLink, firstLine: restFirstLine });
@@ -530,11 +536,6 @@ function main() {
530536
fs.writeFileSync(path.join(DIST, "sitemap.xml"), sitemapXml(config.site, visible));
531537
fs.writeFileSync(path.join(DIST, "robots.txt"), robotsTxt(config.site));
532538
fs.writeFileSync(path.join(DIST, "404.html"), notFoundHtml(config.site));
533-
fs.writeFileSync(path.join(DIST, "llms.txt"), llmsTxt({ entries, site: config.site, categories: config.categories }));
534-
fs.writeFileSync(
535-
path.join(DIST, "llms-full.txt"),
536-
llmsFullTxt({ entries, site: config.site, categories: config.categories, bodies })
537-
);
538539

539540
const pages = writeEntryPages({ entries, bodies, config, stats, stamp, hasCard, now });
540541
const pageErrors = [...pages.errors, ...checkPages(["index.html", ...pages.written])];
@@ -544,6 +545,20 @@ function main() {
544545
process.exit(1);
545546
}
546547

548+
// After the page gate: every body link has resolved by now, so the llms
549+
// files cannot hit a link error of their own.
550+
fs.writeFileSync(path.join(DIST, "llms.txt"), llmsTxt({ entries, site: config.site, categories: config.categories }));
551+
fs.writeFileSync(
552+
path.join(DIST, "llms-full.txt"),
553+
llmsFullTxt({
554+
entries,
555+
site: config.site,
556+
categories: config.categories,
557+
bodies,
558+
resolverFor: linkResolvers(entries, config),
559+
})
560+
);
561+
547562
console.log(
548563
`Built ${entries.length} entries → site/dist (${stats.active} active, ${stats.commercialOk} commercial-ok, ${stats.commercialVaries} per-file)`
549564
);

‎site/lib/lib.test.mjs‎

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,30 @@ has("full carries notes", lf, "- catch one");
166166
lacks("full leaves evidence out", lf, "## Evidence");
167167
has("full carries the source", lf, "Source: https://src.test/");
168168

169+
/* final review fixes ------------------------------------------------------- */
170+
import { unquoteScalar } from "./shared.mjs";
171+
eq("escaped double quote", renderInline('a \\"q\\" b', passLink), "a &quot;q&quot; b");
172+
eq("escaped comma", renderInline("x\\,y", passLink), "x,y");
173+
eq("yaml double-quoted escapes", unquoteScalar('"Music \\"{t}\\" by a\\\\b"'), 'Music "{t}" by a\\b');
174+
eq("yaml single-quoted escape", unquoteScalar("'it''s'"), "it's");
175+
eq("yaml unquoted passes through", unquoteScalar("plain"), "plain");
176+
const longLead = `Lead ${"word ".repeat(60)}with [k](k.md) at the end.`;
177+
const lfLinks = llmsFullTxt({
178+
entries: [{ ...llmsEntries[0], summary: "Short…" }],
179+
site,
180+
categories: cats,
181+
bodies: new Map([["a1", `# A\n\n${longLead}\n\n## Notes\n\n- see [o](../audio/o.md) and [d](../../docs/p.md)\n- read [\`docs/p.md\`](../../docs/p.md); \`[x](y)\` stays literal`]]),
182+
resolverFor: () => (href) =>
183+
href.endsWith("o.md") ? { href: "../o/", external: false } : href.endsWith("k.md") ? { href: "../k/", external: false } : { href: "https://github.com/o/r/blob/main/docs/p.md", external: true },
184+
});
185+
has("full uses the whole lead", lfLinks, "word with [k](https://x.test/s/entry/k/) at the end.");
186+
lacks("full does not use the cut summary", lfLinks, "Short…");
187+
has("full notes link to entry pages", lfLinks, "[o](https://x.test/s/entry/o/)");
188+
has("full notes link to GitHub", lfLinks, "[d](https://github.com/o/r/blob/main/docs/p.md)");
189+
lacks("full has no relative links", lfLinks, "](../");
190+
has("full rewrites a link whose label is code", lfLinks, "[`docs/p.md`](https://github.com/o/r/blob/main/docs/p.md)");
191+
has("full leaves links inside code alone", lfLinks, "`[x](y)` stays literal");
192+
169193
/* report (keep last) ------------------------------------------------------ */
170194
if (failures.length) {
171195
console.error(`lib.test failed (${failures.length}):`);

‎site/lib/llms.mjs‎

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,31 @@
11
/** Plain-text indexes of the catalog for AI assistants (llmstxt.org format). */
2-
import { sectionMarkdown } from "./markdown.mjs";
2+
import { sectionMarkdown, splitEntryBody } from "./markdown.mjs";
33
import { commercialLabel, entryPageUrl } from "./shared.mjs";
44

5+
/** The first paragraph of the body before its first `##`, lines joined. */
6+
function leadParagraph(body) {
7+
const first = splitEntryBody(body)
8+
.lead.split(/\n\s*\n/)
9+
.map((p) => p.trim())
10+
.find((p) => p && !p.startsWith("#"));
11+
return first ? first.split("\n").map((l) => l.trim()).join(" ") : "";
12+
}
13+
14+
/** Markdown links made absolute: this file is read on its own, far from the repo. */
15+
function absoluteLinks(md, resolveLink, pageUrl) {
16+
if (!resolveLink) return md;
17+
// Code spans are literal: set them aside so a link-shaped span is left
18+
// alone, while a link whose label is code is still rewritten.
19+
const spans = [];
20+
const out = md
21+
.replace(/`[^`]*`/g, (span) => `\u0000${spans.push(span) - 1}\u0000`)
22+
.replace(/\[([^\]]+)\]\(([^)\s]+)\)/g, (_, label, href) => {
23+
const r = resolveLink(href);
24+
return `[${label}](${r.external ? r.href : new URL(r.href, pageUrl).href})`;
25+
});
26+
return out.replace(/\u0000(\d+)\u0000/g, (_, i) => spans[Number(i)]);
27+
}
28+
529
const creditWord = (e) =>
630
e.attribution_required === true ? "credit required" : e.attribution_required === false ? "no credit required" : "credit unclear";
731

@@ -31,14 +55,19 @@ export function llmsTxt({ entries, site, categories }) {
3155
return `${out.join("\n")}\n`;
3256
}
3357

34-
export function llmsFullTxt({ entries, site, categories, bodies }) {
58+
export function llmsFullTxt({ entries, site, categories, bodies, resolverFor }) {
3559
const out = [header(site)];
3660
for (const g of groups(entries, categories)) {
3761
out.push(`\n## ${g.label}\n`);
3862
for (const e of g.items) {
39-
const notes = sectionMarkdown(bodies.get(e.id) || "", "Notes");
63+
const body = bodies.get(e.id) || "";
64+
const page = entryPageUrl(site, e.id);
65+
const resolveLink = resolverFor ? resolverFor(e) : null;
66+
const lead = absoluteLinks(leadParagraph(body) || e.summary || "", resolveLink, page);
67+
const notesMd = sectionMarkdown(body, "Notes");
68+
const notes = notesMd ? absoluteLinks(notesMd, resolveLink, page) : null;
4069
out.push(
41-
`### ${e.name}\n\nPage: ${entryPageUrl(site, e.id)}\nSource: ${e.url}\nLicence: ${facts(e)}; verified ${e.verified}\n\n${e.summary || ""}${notes ? `\n\n${notes}` : ""}\n`
70+
`### ${e.name}\n\nPage: ${page}\nSource: ${e.url}\nLicence: ${facts(e)}; verified ${e.verified}\n\n${lead}${notes ? `\n\n${notes}` : ""}\n`
4271
);
4372
}
4473
}

‎site/lib/markdown.mjs‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,8 @@ export function renderInline(text, resolveLink) {
2727
let s = String(text);
2828
// Code spans first: their contents are literal.
2929
s = s.replace(/`([^`]+)`/g, (_, code) => keep(`<code>${esc(code)}</code>`));
30-
// Backslash escapes.
31-
s = s.replace(/\\([\\`*_[\]()#+\-.!|<>])/g, (_, ch) => keep(esc(ch)));
30+
// Backslash escapes: any ASCII punctuation, as in CommonMark.
31+
s = s.replace(/\\([!-/:-@[-`{-~])/g, (_, ch) => keep(esc(ch)));
3232
// Links. The label keeps bold, italic and code; the href goes through the resolver.
3333
s = s.replace(/\[([^\]]+)\]\(([^)\s]+)\)/g, (_, label, href) => {
3434
const { href: out, external } = resolveLink(href);

‎site/lib/shared.mjs‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,3 +49,12 @@ export function commercialLabel(v) {
4949
export function entryPageUrl(site, id) {
5050
return `${String(site.siteUrl).replace(/\/+$/, "")}/entry/${id}/`;
5151
}
52+
53+
/** A YAML scalar without its quotes, with the quote escapes undone. */
54+
export function unquoteScalar(v) {
55+
if (v.length >= 2 && v.startsWith('"') && v.endsWith('"')) {
56+
return v.slice(1, -1).replace(/\\(["\\])/g, "$1");
57+
}
58+
if (v.length >= 2 && v.startsWith("'") && v.endsWith("'")) return v.slice(1, -1).replaceAll("''", "'");
59+
return v;
60+
}

‎site/public/app.js‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -460,7 +460,12 @@
460460
} catch {
461461
return;
462462
}
463-
if (y !== null) requestAnimationFrame(() => window.scrollTo(0, Number(y)));
463+
// Only Back and Forward return to a position. A link to the homepage
464+
// (the brand, the breadcrumb) is a new visit and starts where it asks.
465+
const nav = performance.getEntriesByType("navigation")[0];
466+
if (y !== null && nav && nav.type === "back_forward") {
467+
requestAnimationFrame(() => window.scrollTo(0, Number(y)));
468+
}
464469
}
465470

466471
// Restored from the back/forward cache: the page kept its own position,
@@ -517,6 +522,8 @@
517522
// The whole card is the title link (a stretched ::after), so every click
518523
// that leaves for an entry page lands here.
519524
$("#entry-grid").addEventListener("click", (e) => {
525+
// A modified click opens a new tab and this page stays where it is.
526+
if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return;
520527
if (e.target.closest("a.entry-link")) rememberScroll();
521528
});
522529

0 commit comments

Comments
 (0)