commit a38ef94914b2d5ecfc92a5799231fcc96dd28a52
parent 68f2975f7eaa6b2610a0a6a59cdace0def36621b
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date: Mon, 5 Oct 2026 13:24:42 -0400
Merge report/series (report series tag; no index of one; lean index cards)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Diffstat:
17 files changed, 107 insertions(+), 36 deletions(-)
diff --git a/ENVIRONMENT.md b/ENVIRONMENT.md
@@ -63,6 +63,7 @@ Tokens, credentials and knobs a running process reads. Most configuration is not
| `DOCKER_BUILD_MEMORY` | no cap | Per-container memory cap for a docker-mode build (`--memory`). | common/publish/build.ts |
| `DOCKER_BUILD_CPUS` | no cap | Per-container CPU cap for a docker-mode build (`--cpus`). | common/publish/build.ts |
| `ARCHIVE_CHANNEL_CONCURRENCY` | `4` | How many channels' archive zips `build archives` builds at once. | common/bin/build-archives.ts |
+| `HOST` | every interface | The address `pnpm start:export` (serve-out) listens on; `127.0.0.1` keeps a private site on this machine. | export/scripts/serve-out.mjs |
| `MAX_ARCHIVE_BYTES` | the Cloudflare-safe cap | The served-file size cap for archives, in bytes; `0` = no cap. A site's own `archiveMaxBytes` wins. | common/bin/compose-site.ts |
| `CHOUGH_BIN` | `chough` on PATH | The chough transcription engine, when a worker names no binary. | common/lib/transcriptionApps.ts |
| `CHOUGH_MODEL` | chough's own | Passed to chough from a worker's model field; chough auto-downloads one when unset. | chough (set by common/lib/transcriptionApps.ts) |
diff --git a/REPORT.md b/REPORT.md
@@ -18,6 +18,7 @@ Regenerate this file with `pnpm --filter yt-dlp-transcript-common exec tsx bin/f
| `version` | yes | `1`. |
| `id` | yes | The report's id: a lowercase slug (`[a-z0-9][a-z0-9-]*`, at most 64), its directory name under `reports/` and the last segment of its page, `/reports/<id>/`. Must match the directory. |
| `kind` | yes | `"factcheck"` — sections of claims, each with a verdict — or `"sweep"` — sections with no verdicts, or bodies with inline citations. |
+| `series` | no | The series the report belongs to: a recurring name for its kind of report. It leads the title in the accent colour, in place of the kind's label, and is joined to it as `<series>: <title>` where one line names the report (a page title, a link, a cited-in entry). |
| `title` | yes | The report's title. |
| `subtitle` | no | A line under the title. |
| `summary` | no | The report's summary, in markdown, shown before the sections. May cite inline: `[label](cite:<id>)`. |
diff --git a/common/lib/corpus.ts b/common/lib/corpus.ts
@@ -1,7 +1,7 @@
import type { PublicSiteDescriptor } from "./siteDescriptor";
import { AI_DOC_URL, PROJECT_GENERATOR } from "./project";
import { TAGS_FILENAME } from "./curatedTags";
-import { REPORTS_INDEX_PATH } from "./report/views";
+import { REPORTS_INDEX_PATH, reportFullTitle } from "./report/views";
import {
CONTRACT,
archiveUrl,
@@ -397,7 +397,7 @@ export type LlmsReport = { title: string; href: string; subtitle?: string };
function pushReports(out: string[], base: string | undefined, reports: readonly LlmsReport[]): void {
for (const r of reports.slice(0, LLMS_INLINE_LIMIT)) {
- out.push(`- [${r.title}](${join(base, r.href)})${r.subtitle ? `: ${r.subtitle}` : ""}`);
+ out.push(`- [${reportFullTitle(r)}](${join(base, r.href)})${r.subtitle ? `: ${r.subtitle}` : ""}`);
}
if (reports.length > LLMS_INLINE_LIMIT) {
out.push(`- …and ${reports.length - LLMS_INLINE_LIMIT} more — see the report index.`);
diff --git a/common/lib/envVars.ts b/common/lib/envVars.ts
@@ -99,6 +99,7 @@ const DECLARED: EnvVarDecl[] = [
{ name: "DOCKER_BUILD_MEMORY", audience: "runtime", default: "no cap", readBy: "common/publish/build.ts", doc: "Per-container memory cap for a docker-mode build (`--memory`)." },
{ name: "DOCKER_BUILD_CPUS", audience: "runtime", default: "no cap", readBy: "common/publish/build.ts", doc: "Per-container CPU cap for a docker-mode build (`--cpus`)." },
{ name: "ARCHIVE_CHANNEL_CONCURRENCY", audience: "runtime", default: "`4`", readBy: "common/bin/build-archives.ts", doc: "How many channels' archive zips `build archives` builds at once." },
+ { name: "HOST", audience: "runtime", default: "every interface", readBy: "export/scripts/serve-out.mjs", doc: "The address `pnpm start:export` (serve-out) listens on; `127.0.0.1` keeps a private site on this machine." },
{ name: "MAX_ARCHIVE_BYTES", audience: "runtime", default: "the Cloudflare-safe cap", readBy: "common/bin/compose-site.ts", doc: "The served-file size cap for archives, in bytes; `0` = no cap. A site's own `archiveMaxBytes` wins." },
{ name: "CHOUGH_BIN", audience: "runtime", default: "`chough` on PATH", readBy: "common/lib/transcriptionApps.ts", doc: "The chough transcription engine, when a worker names no binary." },
{ name: "CHOUGH_MODEL", audience: "runtime", default: "chough's own", readBy: "chough (set by common/lib/transcriptionApps.ts)", doc: "Passed to chough from a worker's model field; chough auto-downloads one when unset." },
diff --git a/common/lib/report/convertManifest.ts b/common/lib/report/convertManifest.ts
@@ -63,6 +63,7 @@ import {
import type { Claim, Report, Section } from "./schema";
import { isVerdict, type Verdict } from "./verdicts";
import { parseReport } from "./validate";
+import { reportFullTitle } from "./views";
import type { Problem } from "../citations/validate";
type Obj = Record<string, unknown>;
@@ -539,7 +540,7 @@ export async function reportToManifest(raw: unknown, opts: ManifestOptions = {})
const manifest: Record<string, unknown> = {
schemaVersion: 1,
slug: report.id,
- title: report.title,
+ title: reportFullTitle(report),
subtitle: report.subtitle ?? "",
generatedOn: opts.today ?? new Date().toISOString().slice(0, 10),
provenance: { siteOrigin: opts.siteOrigin ?? "", channelSlug, channel: "" },
diff --git a/common/lib/report/schema.ts b/common/lib/report/schema.ts
@@ -61,6 +61,7 @@ export const reportSchema = z.strictObject({
version: z.literal(REPORT_VERSION),
id: text,
kind: z.enum(REPORT_KINDS),
+ series: text.optional(),
title: text,
subtitle: text.optional(),
summary: text.optional(),
@@ -92,6 +93,7 @@ export const REPORT_FIELD_DOCS: FieldDocs<Report> = {
version: `\`${REPORT_VERSION}\`.`,
id: "The report's id: a lowercase slug (`[a-z0-9][a-z0-9-]*`, at most 64), its directory name under `reports/` and the last segment of its page, `/reports/<id>/`. Must match the directory.",
kind: '`"factcheck"` — sections of claims, each with a verdict — or `"sweep"` — sections with no verdicts, or bodies with inline citations.',
+ series: "The series the report belongs to: a recurring name for its kind of report. It leads the title in the accent colour, in place of the kind's label, and is joined to it as `<series>: <title>` where one line names the report (a page title, a link, a cited-in entry).",
title: "The report's title.",
subtitle: "A line under the title.",
summary: "The report's summary, in markdown, shown before the sections. May cite inline: `[label](cite:<id>)`.",
diff --git a/common/lib/report/views.test.ts b/common/lib/report/views.test.ts
@@ -19,6 +19,7 @@ import {
orderedCitations,
reportAssetPath,
reportIndexEntry,
+ reportFullTitle,
sourceAnchor,
sectionCitations,
spanLabel,
@@ -284,3 +285,16 @@ test("a citation with no moment key, or a source the report lacks, fails the bui
};
assert.throws(() => buildReportPageView(noSource, { record: (c) => record(c.channel, c.id) }), /does not define/);
});
+
+test("a report's series rides its views and leads its one-line name", () => {
+ const series = { ...report, series: "On the Record" };
+ assert.deepEqual(validateReport(series), []);
+ const v = buildReportPageView(series, { record: (c) => record(c.channel, c.id) });
+ assert.equal(v.series, "On the Record");
+ assert.equal(reportIndexEntry(v).series, "On the Record");
+ assert.equal(reportFullTitle(v), `On the Record: ${report.title}`);
+ assert.equal(reportFullTitle(view), report.title, "no series: the title alone");
+ const index = buildCitedIn([series]);
+ const key = Object.keys(index)[0];
+ assert.ok(citedInViews(index[key], [v]).every((e) => e.reportTitle === `On the Record: ${report.title}`));
+});
diff --git a/common/lib/report/views.ts b/common/lib/report/views.ts
@@ -227,6 +227,8 @@ export type ReportPageView = {
version: typeof REPORT_VIEWS_VERSION;
id: string;
kind: ReportKind;
+ // Shown above the title in place of the kind's label; `<series>: <title>` on one line.
+ series?: string;
title: string;
subtitle?: string;
summary?: string;
@@ -252,6 +254,8 @@ export type VerdictCount = { verdict: Verdict; count: number };
export type ReportIndexEntry = {
id: string;
kind: ReportKind;
+ // Shown above the title in place of the kind's label; `<series>: <title>` on one line.
+ series?: string;
title: string;
subtitle?: string;
published?: string;
@@ -462,6 +466,7 @@ export function buildReportPageView(report: Report, resolve: ReportViewResolver)
version: REPORT_VIEWS_VERSION,
id: report.id,
kind: report.kind,
+ series: report.series,
title: report.title,
subtitle: report.subtitle,
summary: report.summary,
@@ -570,12 +575,19 @@ export function verdictTally(view: {
return VERDICTS.filter((v) => counts.has(v)).map((verdict) => ({ verdict, count: counts.get(verdict)! }));
}
+// A report named on one line -- a page title, a link, a heading elsewhere:
+// `<series>: <title>`, or the title alone.
+export function reportFullTitle(r: { series?: string; title: string }): string {
+ return r.series ? `${r.series}: ${r.title}` : r.title;
+}
+
// The report's entry in the index.
export function reportIndexEntry(view: ReportPageView): ReportIndexEntry {
const tally = view.kind === "factcheck" ? verdictTally(view) : undefined;
return defined({
id: view.id,
kind: view.kind,
+ series: view.series,
title: view.title,
subtitle: view.subtitle,
published: view.published,
@@ -610,7 +622,7 @@ export function citedInViews(entries: readonly CitedIn[], reports: readonly Repo
defined({
...e,
href: citedInHref(e),
- reportTitle: report.title,
+ reportTitle: reportFullTitle(report),
sectionTitle: section?.title,
claimTitle: claim?.title,
claimText: claim?.text,
diff --git a/export/CHANGELOG.md b/export/CHANGELOG.md
@@ -1,7 +1,8 @@
# Changelog
## [Unreleased]
-- **A report-only site with one report opens on that report.** Its home page is the report itself and its header links nothing; with more reports the home page is the list, without repeating the site's title under the header. Pages a report-only site does not have link home.
+- **A report-only site with one report opens on that report.** Its home page is the report itself, its header links nothing, and `/reports/` forwards home: there is no index of one. With more reports the home page is the list, without repeating the site's title under the header; a list entry is the report's name, subtitle and dates (its counts and tally are on its page). Pages a report-only site does not have link home.
+- **A report can belong to a series.** `report.json` `series` leads the report's title in the accent colour, in place of the kind's label ("Fact-check"); a page title, a cited-in link, `llms.txt` and the MCP name it `<series>: <title>`.
- **`pnpm start:export` serves a built site's moment pages.** It used `serve`, which listed a video or audio moment's directory (`3126.00-3151.00`) instead of serving its page; it now runs `export/scripts/serve-out.mjs`, which serves directories as Cloudflare Pages does, on `EXPORT_DEV_PORT` (3000).
- **MCP: a site's reports can be read, and a site that publishes only reports says so instead of looking empty.** Two new tools: `list_reports` lists the reports a site publishes (id, title, kind, claim and citation counts, a fact-check's verdict tally, its page), and `get_report` reads one — its tally, then each section's claims with their verdicts and findings and, for every citation, the verbatim quote, the original (the platform at the cited second, the post, the document) and the site's moment page; `section` reads one section. On a site that publishes only its reports, `list_channels`, `list_sources` and `resolve_source` say "cited-only site: N report(s)" where they said "No channels found"; on a site with reports as well, `list_sources` and `resolve_source` say how many. The archive readers (local, remote) read `corpus.json` spec 5: a cited site is an empty corpus without an error, and a local copy of one is never read from a stale `transcripts/` folder. A hub has no reports of its own; `list_reports` says to name a member site.
- **The hub leaves out a site that publishes only its reports.** A site with search off (`search: false`) is not a searchable archive, so it is no hub member (not in federated search, the hub's `corpus.json` or `llms.txt`) and no other site's footer links to it, whatever its **List on the Archilyzer homepage and hub** setting says. A site with reports that publishes its full corpus is a member as before. Needs a hub rebuild and deploy once such a site exists.
diff --git a/export/app/components/reports/ReportArticle.tsx b/export/app/components/reports/ReportArticle.tsx
@@ -14,7 +14,7 @@ import {
type ReportPageView,
type SourceCitationView,
} from "yt-dlp-transcript-common/lib/report/views";
-import { ArchiveList, Eyebrow, SourceBlock, dateLabel, textLink } from "./parts";
+import { ArchiveList, Eyebrow, ReportName, SourceBlock, dateLabel, textLink } from "./parts";
// ONE REPORT, from its view (common/lib/report/views.ts): the header (title,
// subtitle, dates, the document under review with its archive links), a
@@ -127,9 +127,9 @@ export default function ReportArticle({ view }: { view: ReportPageView }) {
<CitationsProvider citations={view.citations}>
<article data-report={view.id} className="mx-auto flex w-full max-w-3xl flex-col gap-8">
<header className="flex flex-col gap-3 border-b border-border pb-6">
- <Eyebrow>{isFactcheck ? "Fact-check" : "Report"}</Eyebrow>
+ {!view.series && <Eyebrow>{isFactcheck ? "Fact-check" : "Report"}</Eyebrow>}
<h1 className="font-display text-3xl font-semibold leading-tight tracking-tight text-foreground sm:text-4xl">
- {view.title}
+ <ReportName series={view.series} title={view.title} />
</h1>
{view.subtitle && <p className="text-lg text-muted-foreground">{view.subtitle}</p>}
{dates.length > 0 && <p className="font-mono text-xs text-muted-foreground">{dates.join(" · ")}</p>}
diff --git a/export/app/components/reports/ReportIndex.tsx b/export/app/components/reports/ReportIndex.tsx
@@ -1,15 +1,13 @@
import Link from "next/link";
-import { VerdictTally } from "yt-dlp-transcript-common/components/report/VerdictChip";
import type { ReportIndexView } from "yt-dlp-transcript-common/lib/report/views";
-import { EmptyState, Eyebrow, dateLabel } from "./parts";
+import { EmptyState, Eyebrow, ReportName, dateLabel } from "./parts";
// The report index: `/reports/`, and a cited site's home page when it has more
// than one report. One card per published report, in the site's order
-// (site.json `reports`). `heading: null` draws no heading (the home page: the
+// (site.json `reports`): its name, subtitle and dates -- its counts and tally
+// are the report page's. `heading: null` draws no heading (the home page: the
// header already names the site).
-const plural = (n: number, noun: string) => `${n.toLocaleString()} ${noun}${n === 1 ? "" : "s"}`;
-
export default function ReportIndex({
index,
heading = "Reports",
@@ -42,19 +40,14 @@ export default function ReportIndex({
return (
<li key={r.id} data-report-entry={r.id}>
<article className="flex flex-col gap-2 rounded-lg border border-border bg-card p-4 sm:p-5">
- <Eyebrow>{r.kind === "factcheck" ? "Fact-check" : "Report"}</Eyebrow>
+ {!r.series && <Eyebrow>{r.kind === "factcheck" ? "Fact-check" : "Report"}</Eyebrow>}
<h2 className="font-display text-xl font-semibold leading-snug text-foreground">
<Link href={r.href} className="hover:text-brand transition-colors">
- {r.title}
+ <ReportName series={r.series} title={r.title} />
</Link>
</h2>
{r.subtitle && <p className="text-sm text-muted-foreground">{r.subtitle}</p>}
- {r.tally && r.verdicts && <VerdictTally tally={r.tally} styles={r.verdicts} className="mt-1" />}
- <p className="font-mono text-xs text-muted-foreground">
- {[dates, r.claimCount > 0 ? plural(r.claimCount, "claim") : null, plural(r.citationCount, "citation")]
- .filter(Boolean)
- .join(" · ")}
- </p>
+ {dates && <p className="font-mono text-xs text-muted-foreground">{dates}</p>}
</article>
</li>
);
diff --git a/export/app/components/reports/parts.tsx b/export/app/components/reports/parts.tsx
@@ -16,6 +16,21 @@ export function Eyebrow({ children }: { children: React.ReactNode }) {
return <p className="font-mono text-xs uppercase tracking-[0.18em] text-brand">{children}</p>;
}
+// A report's name as a heading. With a series, the series leads in the accent
+// (the tag, like a wordmark's lead) and the title follows in regular weight;
+// without one, the title alone (the kind's label is then the eyebrow above).
+export function ReportName({ series, title }: { series?: string; title: string }) {
+ if (!series) return <>{title}</>;
+ return (
+ <>
+ <span data-report-series="" className="font-semibold text-brand">
+ {series}
+ </span>{" "}
+ <span className="font-normal">{title}</span>
+ </>
+ );
+}
+
export const textLink =
"text-brand underline decoration-brand/40 underline-offset-2 hover:decoration-brand";
diff --git a/export/app/lib/reports.test.ts b/export/app/lib/reports.test.ts
@@ -149,10 +149,10 @@ test("an unknown moment or report under a listed route renders the empty state,
assert.match(render(await momentPage.default({ params: Promise.resolve({ moment: ["..", "x"] }) })), /No moments/);
});
-test("the index lists the report with its tally", () => {
+test("one report: /reports/ is no index, it forwards to the home page (the report)", () => {
const html = render(reportsIndexPage.default());
- assert.match(html, /data-report-entry="demo-factcheck"/);
- assert.ok(html.includes("5 claims · 6 citations"));
+ assert.match(html, /<meta http-equiv="refresh" content="0; url=\/"/);
+ assert.doesNotMatch(html, /data-report-index/);
});
test("a cited site with one report: its home IS the report, and never counts transcripts", async () => {
@@ -169,13 +169,24 @@ test("a cited site with more than one report: its home is the index, with no hea
const original = fs.readFileSync(file, "utf8");
try {
const index = JSON.parse(original);
- index.reports = [...index.reports, { ...index.reports[0], id: "demo-second", title: "A second report", href: "/reports/demo-second/" }];
+ index.reports = [...index.reports, { ...index.reports[0], id: "demo-second", series: "On the Record", title: "A second report", href: "/reports/demo-second/" }];
fs.writeFileSync(file, JSON.stringify(index));
const html = render(await home.default());
assert.match(html, /data-report-index/);
assert.match(html, /data-report-entry="demo-factcheck"/);
assert.match(html, /data-report-entry="demo-second"/);
assert.doesNotMatch(html, /<h1/, "no heading: the header already names the site");
+ // With more than one, /reports/ is the index, tally and all.
+ const idx = render(reportsIndexPage.default());
+ assert.match(idx, /data-report-entry="demo-factcheck"/);
+ assert.doesNotMatch(idx, /claims ·|data-verdict-tally/, "counts and tally are the report page's");
+ assert.doesNotMatch(idx, /http-equiv="refresh"/);
+ // A series leads its title in the accent, in place of the kind's eyebrow.
+ const second = html.slice(html.indexOf('data-report-entry="demo-second"'));
+ assert.match(second, /<span data-report-series="" class="font-semibold text-brand">On the Record<\/span> <span class="font-normal">A second report<\/span>/);
+ assert.doesNotMatch(second.slice(0, second.indexOf("</li>")), /Fact-check/);
+ const first = html.slice(html.indexOf('data-report-entry="demo-factcheck"'));
+ assert.match(first.slice(0, first.indexOf("</li>")), /Fact-check/, "no series: the kind's eyebrow as before");
} finally {
fs.writeFileSync(file, original);
}
diff --git a/export/app/reports/[reportId]/page.tsx b/export/app/reports/[reportId]/page.tsx
@@ -1,5 +1,5 @@
import type { Metadata } from "next";
-import { REPORT_PLACEHOLDER_ID } from "yt-dlp-transcript-common/lib/report/views";
+import { REPORT_PLACEHOLDER_ID, reportFullTitle } from "yt-dlp-transcript-common/lib/report/views";
import ReportArticle from "../../components/reports/ReportArticle";
import { EmptyState } from "../../components/reports/parts";
import { readReportView, reportIds } from "../../lib/reports";
@@ -20,7 +20,7 @@ type Params = { params: Promise<{ reportId: string }> };
export async function generateMetadata({ params }: Params): Promise<Metadata> {
const view = readReportView((await params).reportId);
if (!view) return { title: "No reports" };
- return { title: view.title, ...(view.subtitle ? { description: view.subtitle } : {}) };
+ return { title: reportFullTitle(view), ...(view.subtitle ? { description: view.subtitle } : {}) };
}
export default async function ReportPage({ params }: Params) {
diff --git a/export/app/reports/page.tsx b/export/app/reports/page.tsx
@@ -1,11 +1,32 @@
import type { Metadata } from "next";
import ReportIndex from "../components/reports/ReportIndex";
-import { readReportIndex } from "../lib/reports";
+import Link from "next/link";
+import { isCitedSite } from "yt-dlp-transcript-common/lib/siteSchema";
+import { textLink } from "../components/reports/parts";
+import { readReportIndex, reportIds } from "../lib/reports";
+import { currentSite } from "../lib/site";
+import { instanceMode } from "../lib/mode";
export const metadata: Metadata = { title: "Reports" };
// The report index, from /reports/index.json (common/lib/report/views.ts). A
-// cited site renders the same index as its home page.
+// cited site with more than one report renders the same index as its home
+// page. A cited site with ONE report has no index: its home page is the
+// report, so this route only forwards there (a static export has no server
+// redirect; React hoists the refresh into the head).
export default function ReportsPage() {
+ const site = currentSite();
+ if (instanceMode() === "site" && isCitedSite(site) && reportIds().length === 1) {
+ return (
+ <>
+ <meta httpEquiv="refresh" content="0; url=/" />
+ <p data-reports-forward="" className="mx-auto max-w-2xl text-sm text-muted-foreground">
+ <Link href="/" className={textLink}>
+ {site.siteTitle}
+ </Link>
+ </p>
+ </>
+ );
+ }
return <ReportIndex index={readReportIndex()} />;
}
diff --git a/export/e2e-report/report-site.spec.ts b/export/e2e-report/report-site.spec.ts
@@ -33,12 +33,9 @@ test("one report: the home page IS the report, and the header links nothing", as
await expect(page.locator("header nav a")).toHaveCount(0);
});
-test("the report index page still lists the report, and links it", async ({ page }) => {
+test("one report: /reports/ is no index, it forwards to the home page", async ({ page }) => {
await page.goto("/reports/");
- const entry = page.locator('[data-report-entry="demo-factcheck"]');
- await expect(entry).toContainText("Checking an example article");
- await entry.getByRole("link", { name: /Checking an example article/ }).first().click();
- await expect(page).toHaveURL(REPORT);
+ await expect(page).toHaveURL("/");
await expect(page.locator("h1")).toHaveText("Checking an example article");
});
@@ -213,7 +210,7 @@ for (const route of ["/ask/", "/downloads/", "/duplicates/"]) {
}
test("reading the whole site never asks for the corpus", async ({ page, traffic }) => {
- for (const route of ["/", "/reports/", REPORT, VIDEO_MOMENT, AUDIO_MOMENT, POST_MOMENT, "/ask/", "/downloads/"]) {
+ for (const route of ["/", REPORT, VIDEO_MOMENT, AUDIO_MOMENT, POST_MOMENT, "/ask/", "/downloads/"]) {
await page.goto(route);
await page.waitForLoadState("networkidle");
}
diff --git a/mcp/src/reports.ts b/mcp/src/reports.ts
@@ -16,6 +16,7 @@
import { extractCiteRefs } from "yt-dlp-transcript-common/lib/citations/inline";
import {
orderedCitations,
+ reportFullTitle,
reportPagePath,
verdictTally,
type CitationView,
@@ -215,7 +216,7 @@ export function renderReportPage(
origin: string | null,
sectionId?: string,
): string {
- const head = [`# ${view.title}`];
+ const head = [`# ${reportFullTitle(view)}`];
if (view.subtitle) head.push(view.subtitle);
const meta: string[] = [view.kind];
if (view.published) meta.push(`published ${view.published}`);