commit 192a13e1aff217f861194186ad7f3e3795553bef
parent 509770004976fe4fd124e5a5e35d28ca2cb5ec00
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date: Fri, 9 Oct 2026 17:53:36 -0400
export: a report's Timeline, and Subscribe: RSS · JSON when it has feeds
The Timeline sits after In brief, before what the check found: dated blocks
newest first on a rail, the date first and in the accent, linking to the
entry's own anchor. The header links the feeds and the page's metadata names
them (<link rel="alternate">, absolute). A moment cited in an entry says so
under "Cited in".
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Diffstat:
3 files changed, 91 insertions(+), 6 deletions(-)
diff --git a/export/app/components/reports/MomentArticle.tsx b/export/app/components/reports/MomentArticle.tsx
@@ -150,7 +150,7 @@ export default function MomentArticle({ view }: { view: MomentPageView }) {
<ul data-cited-in="" className="flex flex-col gap-2">
{view.citedIn.map((e) => (
<li
- key={`${e.reportId}/${e.sectionId}/${e.claimId}/${e.citationId}`}
+ key={`${e.reportId}/${e.sectionId}/${e.claimId}/${e.entryId ?? ""}/${e.citationId}`}
className="flex flex-col gap-1 rounded-lg border border-border bg-card p-3 text-sm"
>
<a href={e.href} className={`font-medium ${textLink}`}>
@@ -158,7 +158,13 @@ export default function MomentArticle({ view }: { view: MomentPageView }) {
</a>
<div className="flex flex-wrap items-center gap-2 text-muted-foreground">
{e.verdict && e.verdictStyle && <VerdictChip verdict={e.verdict} style={e.verdictStyle} />}
- {e.sectionTitle ? <span>{e.sectionTitle}</span> : !e.sectionId && <span>Summary</span>}
+ {e.sectionTitle ? (
+ <span>{e.sectionTitle}</span>
+ ) : e.entryId ? (
+ <span>Timeline{e.entryTitle ? ` — ${e.entryTitle}` : ""}</span>
+ ) : (
+ !e.sectionId && <span>Summary</span>
+ )}
{(e.claimTitle ?? e.claimText) && (
<span className="text-foreground">— {e.claimTitle ?? e.claimText}</span>
)}
diff --git a/export/app/components/reports/ReportArticle.tsx b/export/app/components/reports/ReportArticle.tsx
@@ -1,4 +1,4 @@
-import { ArrowDownToLine } from "lucide-react";
+import { ArrowDownToLine, Rss } from "lucide-react";
import { AddedMark, CitationCard } from "yt-dlp-transcript-common/components/citations/CitationCard";
import { CitationsProvider } from "yt-dlp-transcript-common/components/citations/CitationsContext";
import { CitedMarkdown } from "yt-dlp-transcript-common/components/citations/CitedMarkdown";
@@ -9,6 +9,7 @@ import { REPORT_PAGE_ANCHOR_IDS } from "yt-dlp-transcript-common/lib/report/slid
import { hasSlides } from "yt-dlp-transcript-common/lib/report/slides";
import type { SourceArchive } from "yt-dlp-transcript-common/lib/citations/schema";
import {
+ entryDateLabel,
foundGroups,
orderedCitations,
reportDateParts,
@@ -18,6 +19,7 @@ import {
verdictTally,
type CitationView,
type ClaimView,
+ type EntryView,
type ReportPageView,
type SourceCitationView,
} from "yt-dlp-transcript-common/lib/report/views";
@@ -28,7 +30,8 @@ import { ArchiveList, ReportName, SourceBlock, SubjectCard, textLink } from "./p
// revision, linked to its history; the document under review as a card on its
// colour's rail, with its archive links; the subtitle), the three tiers each
// opened by a depth marker — the quick take (a fact-check's tally, the
-// summary), what the check found, and every claim with its evidence: each
+// summary), the timeline when the report keeps one (dated entries, newest
+// first, each its own anchor), what the check found, and every claim with its evidence: each
// claim its verdict and flag, the document's own sentence (its still), the
// findings with their inline citations, and the evidence cards (what the
// report added first, marked; what the document gave itself folded away
@@ -36,6 +39,10 @@ import { ArchiveList, ReportName, SourceBlock, SubjectCard, textLink } from "./p
// the downloads: the report as files (HTML, PDF, Markdown, the evidence pack)
// and its citations as data.
//
+// A report with a timeline on a site with a public URL publishes feeds of it
+// (common/lib/report/feeds.ts): the header links them ("Subscribe"), and the
+// page's metadata names them (lib/reports.ts reportMetadata).
+//
// ONE ARTICLE, THREE SHAPES (release 22): the header carries the view switch
// (Article | Slides | Overview — common/components/report/slides/ReportReader),
// and every place a slide stands for has its id here: the header, the quick
@@ -144,6 +151,34 @@ function TierMarker({ depth }: { depth: 1 | 2 | 3 }) {
);
}
+// A timeline entry: its date first and largest (what a timeline is read by),
+// linking to its own anchor, then its title and its body. A dot on the
+// timeline's rail marks it.
+function Entry({ entry }: { entry: EntryView }) {
+ return (
+ <li id={entry.id} data-entry={entry.id} className="relative flex scroll-mt-20 flex-col gap-1.5">
+ <span
+ aria-hidden="true"
+ className="absolute top-[0.45rem] -left-[calc(1rem+4.5px)] size-2 rounded-full bg-brand ring-4 ring-background sm:-left-[calc(1.25rem+4.5px)]"
+ />
+ <p className="flex flex-wrap items-baseline gap-x-2 font-mono text-sm">
+ <a href={`#${entry.id}`} data-entry-date="" className="font-semibold text-brand hover:underline">
+ <time dateTime={entry.date}>{entryDateLabel(entry.date)}</time>
+ </a>
+ {entry.updated && (
+ <span className="text-xs text-muted-foreground">
+ updated <time dateTime={entry.updated}>{entryDateLabel(entry.updated)}</time>
+ </span>
+ )}
+ </p>
+ <h3 className="font-display text-lg font-semibold leading-snug text-foreground">{entry.title}</h3>
+ <CitedMarkdown className={proseClass}>
+ {entry.body}
+ </CitedMarkdown>
+ </li>
+ );
+}
+
// A claim: its verdict, flag and title; the document's own sentence (its
// still, else its verbatim words) when the claim cites one, else the claim as
// the report states it, plain; the findings; the evidence.
@@ -278,6 +313,19 @@ export default function ReportArticle({ view }: { view: ReportPageView }) {
)}
</p>
)}
+ {view.feeds && (
+ <p data-report-feeds="" className="flex items-center gap-1.5 font-mono text-xs text-muted-foreground">
+ <Rss className="size-3.5" aria-hidden />
+ Subscribe:
+ <a href={view.feeds.rss} type="application/rss+xml" data-feed="rss" className={textLink}>
+ RSS
+ </a>
+ <span aria-hidden="true">·</span>
+ <a href={view.feeds.json} type="application/feed+json" data-feed="json" className={textLink}>
+ JSON
+ </a>
+ </p>
+ )}
{hasSlides(view) && (
<div data-report-views="" className="flex flex-wrap items-center gap-3">
<ReportReader pagePath={reportViewPath(view.id)} title={view.title} series={view.series} />
@@ -337,6 +385,20 @@ export default function ReportArticle({ view }: { view: ReportPageView }) {
</p>
</section>
+ {/* The timeline: dated entries, newest first, on a rail. */}
+ {view.entries && view.entries.length > 0 && (
+ <section data-report-timeline="" aria-labelledby="timeline-heading" className="flex flex-col gap-4">
+ <h2 id="timeline-heading" className="font-display text-2xl font-semibold tracking-tight text-foreground">
+ Timeline
+ </h2>
+ <ol className="ml-1 flex flex-col gap-7 border-l border-border pl-4 sm:pl-5">
+ {view.entries.map((e) => (
+ <Entry key={e.id} entry={e} />
+ ))}
+ </ol>
+ </section>
+ )}
+
{/* What the check found: every ruled claim by verdict, one line each. */}
{groups.length > 0 && (
<section id="found" data-report-tier="2" aria-labelledby="found-heading" className="flex scroll-mt-20 flex-col gap-4">
diff --git a/export/app/lib/reports.ts b/export/app/lib/reports.ts
@@ -16,6 +16,7 @@ import {
REPORT_INDEX_FORMAT,
REPORT_PAGE_FORMAT,
REPORTS_INDEX_PATH,
+ REPORT_FEED_MIME,
momentViewPath,
reportFullTitle,
reportViewPath,
@@ -83,9 +84,25 @@ export function onlyReportView(): ReportPageView | null {
// A report's page metadata — its page's, and a one-report cited site's home's,
// so the tab reads the same on both: the title as one line names it
// (`<series>: <title>`, the layout's template adds the site), the subtitle as
-// the description.
+// the description, and its timeline's feeds when it publishes them (a
+// `<link rel="alternate">` each, by absolute URL: the layout sets no
+// metadataBase).
export function reportMetadata(view: ReportPageView): Metadata {
- return { title: reportFullTitle(view), ...(view.subtitle ? { description: view.subtitle } : {}) };
+ const title = reportFullTitle(view);
+ return {
+ title,
+ ...(view.subtitle ? { description: view.subtitle } : {}),
+ ...(view.feeds
+ ? {
+ alternates: {
+ types: {
+ [REPORT_FEED_MIME.rss]: [{ url: view.feeds.rss, title }],
+ [REPORT_FEED_MIME.json]: [{ url: view.feeds.json, title }],
+ },
+ },
+ }
+ : {}),
+ };
}
// The moment keys to build pages for (only keys exactly as momentKey spells