From 152f4e1eecef8d45563362c4826228749a59e45b Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 22 Aug 2026 16:00:56 +0000 Subject: [PATCH 1/3] Fix markdown Accept 500s and publish crawlable event pages The Vercel proxy cannot read the YAML corpus, so markdown negotiation now rewrites to a Node handler. Home and category HTML no longer start with a loading shell, and every sourced event gets a durable path. Co-authored-by: ben <0thernet@users.noreply.github.com> --- app/AGENTS.md | 5 +- app/globals.css | 36 +++++ .../[category]/[eventId]/page.test.tsx | 55 +++++++ app/history/[category]/[eventId]/page.tsx | 149 ++++++++++++++++++ app/history/[category]/page.test.tsx | 4 + app/history/history-event-article.tsx | 100 ++++++++++++ app/history/history-view.tsx | 82 +--------- app/history/payment-volume/page.tsx | 4 +- app/loading.tsx | 14 -- app/metadata.test.ts | 14 ++ app/page.test.tsx | 4 + app/robots.ts | 1 + app/runtime-surfaces.test.tsx | 15 +- app/seo.test.ts | 42 ++++- app/seo.ts | 43 ++++- app/sitemap.ts | 4 + app/x-markdown/[[...path]]/route.test.ts | 75 +++++++++ app/x-markdown/[[...path]]/route.ts | 71 +++++++++ lib/AGENTS.md | 1 + lib/accept.test.ts | 2 + lib/accept.ts | 6 + lib/history-urls.test.ts | 49 ++++++ lib/history-urls.ts | 55 +++++++ lib/llms-txt.ts | 2 +- lib/page-markdown.test.ts | 19 ++- lib/page-markdown.ts | 57 ++++++- next.config.ts | 2 + proxy.test.ts | 51 ++++++ proxy.ts | 22 ++- 29 files changed, 858 insertions(+), 126 deletions(-) create mode 100644 app/history/[category]/[eventId]/page.test.tsx create mode 100644 app/history/[category]/[eventId]/page.tsx create mode 100644 app/history/history-event-article.tsx delete mode 100644 app/loading.tsx create mode 100644 app/x-markdown/[[...path]]/route.test.ts create mode 100644 app/x-markdown/[[...path]]/route.ts create mode 100644 lib/history-urls.test.ts create mode 100644 lib/history-urls.ts create mode 100644 proxy.test.ts diff --git a/app/AGENTS.md b/app/AGENTS.md index ccc51b3..5529d41 100644 --- a/app/AGENTS.md +++ b/app/AGENTS.md @@ -1,11 +1,12 @@ # Contents - `page.tsx` – the canonical unified Stripe history timeline. -- `history/` – category pages, the appearances projection, annual-volume and valuation pages, and shared timeline rendering. +- `history/` – category pages, durable event pages, the appearances projection, annual-volume and valuation pages, and shared timeline rendering. +- `x-markdown/` – the Node handler that renders the same public URLs as Markdown. - `data/` – the crawlable history and research dataset index. - `about/`, `contact/`, and `privacy/` – sourcing, review, independence, corrections, public contact channels, and privacy. - `llms.txt/` – the agent index with when-to-use guidance. -- Root `proxy.ts` – Accept negotiation that serves Markdown for the same public URLs. +- Root `proxy.ts` – Accept negotiation that rewrites Markdown requests to the Node corpus handler. Do not read YAML in the proxy. - `site.ts`, `site-copy.ts`, `site-header.tsx`, and `site-footer.tsx` – canonical identity, shared editorial copy, and shared page chrome. - `analytics.ts`, `posthog.ts`, and `posthog-analytics.tsx` – the finite public-route analytics contract, strict PostHog boundary, and client provider. - `layout.tsx`, `globals.css`, and `support/` – the document, appearance, structured-data, and portable styling boundaries. diff --git a/app/globals.css b/app/globals.css index 6c20405..3e66614 100644 --- a/app/globals.css +++ b/app/globals.css @@ -37,6 +37,7 @@ } .stripedex-section h2, +.history-event h1, .history-event h3, .history-volume h2, .history-year-heading h2 { @@ -401,6 +402,7 @@ } .history-event-kicker, +.history-event h1, .history-event h3, .history-event article > p { margin: 0; @@ -413,10 +415,43 @@ gap: 0.35rem 0.5rem; } +.history-event h1, .history-event h3 { margin-top: 0.25rem; } +.history-event h3 a { + color: inherit; + text-decoration: none; +} + +.history-event h3 a:hover, +.history-event h3 a:focus-visible { + text-decoration: underline; +} + +.history-related { + margin-top: 2rem; +} + +.history-related h2, +.history-related ul, +.history-related li { + margin: 0; +} + +.history-related ul { + display: grid; + gap: 0.4rem; + padding-left: 1.1rem; +} + +.history-independence { + color: var(--plain-muted); + margin-top: 2rem; + max-width: 38rem; +} + .history-event-type { align-items: center; background: var(--history-category-soft); @@ -1026,6 +1061,7 @@ padding: 0.8rem 0 0.8rem 0.65rem; } + .history-event h1, .history-event h3 { font-size: 0.95rem; line-height: 1.4; diff --git a/app/history/[category]/[eventId]/page.test.tsx b/app/history/[category]/[eventId]/page.test.tsx new file mode 100644 index 0000000..015fde3 --- /dev/null +++ b/app/history/[category]/[eventId]/page.test.tsx @@ -0,0 +1,55 @@ +import { describe, expect, test } from "bun:test"; +import { INDEXABLE_ROBOTS } from "@hraness/web-discovery"; +import { renderToStaticMarkup } from "react-dom/server"; + +import { loadHistory } from "@/lib/content"; + +import HistoryEventPage, { + generateMetadata, + generateStaticParams, +} from "./page"; + +describe("stripedex.com event history pages", () => { + test("generates a durable path for every sourced event", async () => { + const history = await loadHistory(); + const params = await generateStaticParams(); + expect(params).toContainEqual({ + category: "acquisitions", + eventId: "openrouter-acquisition-talks-reported", + }); + expect(params).toHaveLength(history.events.length); + }); + + test("publishes crawlable, sourced event HTML without a loading shell", async () => { + const metadata = await generateMetadata({ + params: Promise.resolve({ + category: "acquisitions", + eventId: "openrouter-acquisition-talks-reported", + }), + }); + const html = renderToStaticMarkup(await HistoryEventPage({ + params: Promise.resolve({ + category: "acquisitions", + eventId: "openrouter-acquisition-talks-reported", + }), + })); + + expect(metadata).toMatchObject({ + alternates: { + canonical: "/history/acquisitions/openrouter-acquisition-talks-reported", + }, + robots: INDEXABLE_ROBOTS, + title: "Stripe reportedly discusses acquiring OpenRouter", + }); + expect(html).toContain("

Stripe reportedly discusses acquiring OpenRouter

"); + expect(html).toContain(""); + expect(html).toContain("AI-model marketplace OpenRouter"); + expect(html).toContain("not affiliated with, endorsed by, or operated by"); + expect(html).toContain('href="/history/acquisitions"'); + expect(html).toContain('id="stripedex-history-event-structured-data"'); + expect(html).toContain('data-theme-value="system"'); + expect(html).toContain('class="hraness-brand stripedex-footer-hraness"'); + expect(html).not.toContain("Loading Stripe company history"); + expect(html.match(/data-presentation="menu"/gu)).toHaveLength(1); + }); +}); diff --git a/app/history/[category]/[eventId]/page.tsx b/app/history/[category]/[eventId]/page.tsx new file mode 100644 index 0000000..5c5164d --- /dev/null +++ b/app/history/[category]/[eventId]/page.tsx @@ -0,0 +1,149 @@ +import { loadHistory, type HistoryCollection } from "@/lib/content"; +import { + timelineCategoryIds, + type TimelineCategoryId, +} from "@/lib/history-schema"; +import { historyCategoryPath, historyEventPath } from "@/lib/history-urls"; +import { INDEXABLE_ROBOTS } from "@hraness/web-discovery"; +import { JsonLdScript } from "@hraness/web-discovery/json-ld"; +import type { Metadata } from "next"; +import Link from "next/link"; +import { notFound } from "next/navigation"; + +import { independenceSentence } from "../../../site-copy"; +import { breadcrumbJsonLd, historyEventJsonLd } from "../../../seo"; +import { SiteFooter } from "../../../site-footer"; +import { SiteHeader } from "../../../site-header"; +import { site, socialMetadata } from "../../../site"; +import { historyFilterVisualStyle } from "../../category-visuals"; +import { HistoryEventArticle } from "../../history-event-article"; + +interface HistoryEventPageProps { + readonly params: Promise<{ category: string; eventId: string }>; +} + +export const dynamic = "force-static"; +export const dynamicParams = false; + +export async function generateStaticParams() { + const history = await loadHistory(); + return history.events + .filter(({ categoryId }) => timelineCategoryIds.includes(categoryId)) + .map((event) => ({ + category: event.categoryId, + eventId: event.id, + })); +} + +function resolveEvent( + history: HistoryCollection, + categoryId: string, + eventId: string, +) { + if (!timelineCategoryIds.includes(categoryId as TimelineCategoryId)) { + return undefined; + } + return history.events.find((event) => ( + event.categoryId === categoryId && event.id === eventId + )); +} + +export async function generateMetadata({ + params, +}: HistoryEventPageProps): Promise { + const { category, eventId } = await params; + const history = await loadHistory(); + const event = resolveEvent(history, category, eventId); + if (event === undefined) return {}; + const path = historyEventPath(event.categoryId, event.id); + return { + title: event.title, + description: event.summary, + alternates: { canonical: path }, + robots: INDEXABLE_ROBOTS, + ...socialMetadata(`${event.title} | ${site.domain}`, event.summary, path), + }; +} + +export default async function HistoryEventPage({ + params, +}: HistoryEventPageProps) { + const { category, eventId } = await params; + const history = await loadHistory(); + const event = resolveEvent(history, category, eventId); + if (event === undefined) notFound(); + const path = historyEventPath(event.categoryId, event.id); + const categoryPath = historyCategoryPath(event.categoryId); + const relatedEvents = (event.related_events ?? []).flatMap((relatedId) => { + const related = history.events.find(({ id }) => id === relatedId); + return related === undefined ? [] : [related]; + }); + + return ( + <> + +
+ + +
+
+ +
+ {relatedEvents.length === 0 ? null : ( +
+ +
    + {relatedEvents.map((related) => ( +
  • + + {related.title} + + + +
  • + ))} +
+
+ )} +

{independenceSentence}

+
+ +
+ + ); +} diff --git a/app/history/[category]/page.test.tsx b/app/history/[category]/page.test.tsx index c46f304..50d859a 100644 --- a/app/history/[category]/page.test.tsx +++ b/app/history/[category]/page.test.tsx @@ -85,7 +85,11 @@ describe("stripedex.com category history", () => { expect(html.indexOf('data-filter-id="all"')).toBeLessThan( html.indexOf('data-filter-id="acquisitions"'), ); + expect(html).not.toContain("Loading Stripe company history"); expect(html).toContain("Stripe reportedly discusses acquiring OpenRouter"); + expect(html).toContain( + 'href="/history/acquisitions/openrouter-acquisition-talks-reported"', + ); expect(html).toContain('id="stripedex-history-category-structured-data"'); expect(html).not.toContain('class="stripedex-selector"'); expect(html).toContain('class="stripedex-header"'); diff --git a/app/history/history-event-article.tsx b/app/history/history-event-article.tsx new file mode 100644 index 0000000..8a189bd --- /dev/null +++ b/app/history/history-event-article.tsx @@ -0,0 +1,100 @@ +import type { CategorizedHistoryEvent } from "@/lib/content"; +import { historyCategoryPath, historyEventPath } from "@/lib/history-urls"; +import Link from "next/link"; + +import { HistoryCategoryIcon } from "./category-icon"; + +export function HistoryEventArticle({ + event, + headingLevel = "h3", + linkedTitle = true, +}: Readonly<{ + event: CategorizedHistoryEvent; + headingLevel?: "h1" | "h3"; + linkedTitle?: boolean; +}>) { + const categoryLabel = event.categoryLabel.toLocaleLowerCase("en-US"); + const Heading = headingLevel; + const title = linkedTitle + ? ( + + {event.title} + + ) + : event.title; + + return ( +
+
+

+ + + + {categoryLabel} + + {event.status === undefined ? null : ( + {event.status} + )} + {event.confidence === "confirmed" ? null : ( + + {event.confidence} + + )} +

+ {title} +
+

{event.summary}

+ {event.amount === undefined + && event.metrics === undefined + && event.details === undefined + ? null + : ( +
+ {event.amount === undefined ? null : ( +
+
amount
+
{event.amount.display}
+
+ )} + {event.metrics?.map((metric) => ( +
+
{metric.label}
+
+ {metric.value} + {metric.context === undefined ? null : ` · ${metric.context}`} +
+
+ ))} + {event.details?.map((detail) => ( +
+
{detail.label}
+
{detail.value}
+
+ ))} +
+ )} +

+ {event.sources.map((source, index) => ( + + {index === 0 ? null : } + + {source.publisher} + + + ))} +

+
+ ); +} diff --git a/app/history/history-view.tsx b/app/history/history-view.tsx index caa9f04..26e34bf 100644 --- a/app/history/history-view.tsx +++ b/app/history/history-view.tsx @@ -5,8 +5,11 @@ import type { ValuationHeadlinePoint, } from "@/lib/content"; import type { TimelineCategoryId } from "@/lib/history-schema"; +import { historyCategoryPath, historyEventPath } from "@/lib/history-urls"; import Link from "next/link"; +import { HistoryEventArticle } from "./history-event-article"; + import { HistoryCategoryIcon } from "./category-icon"; import { historyFilterVisualStyle, @@ -45,7 +48,7 @@ interface HistoryYear { function categoryHref( categoryId: TimelineCategoryId, ): `/history/${TimelineCategoryId}` { - return `/history/${categoryId}`; + return historyCategoryPath(categoryId); } function groupEventsByYear( @@ -116,7 +119,7 @@ function HistoryMeasuresSidebar({
  • {point.calendarYear} {point.display} @@ -190,86 +193,13 @@ function HistoryMeasuresSidebar({ function HistoryEventItem({ event, }: Readonly<{ event: CategorizedHistoryEvent }>) { - const categoryLabel = event.categoryLabel.toLocaleLowerCase("en-US"); - return (
  • -
    -
    -

    - - - - {categoryLabel} - - {event.status === undefined ? null : ( - {event.status} - )} - {event.confidence === "confirmed" ? null : ( - - {event.confidence} - - )} -

    -

    {event.title}

    -
    -

    {event.summary}

    - {event.amount === undefined - && event.metrics === undefined - && event.details === undefined - ? null - : ( -
    - {event.amount === undefined ? null : ( -
    -
    amount
    -
    {event.amount.display}
    -
    - )} - {event.metrics?.map((metric) => ( -
    -
    {metric.label}
    -
    - {metric.value} - {metric.context === undefined ? null : ` · ${metric.context}`} -
    -
    - ))} - {event.details?.map((detail) => ( -
    -
    {detail.label}
    -
    {detail.value}
    -
    - ))} -
    - )} -

    - {event.sources.map((source, index) => ( - - {index === 0 ? null : } - - {source.publisher} - - - ))} -

    -
    +
  • ); } diff --git a/app/history/payment-volume/page.tsx b/app/history/payment-volume/page.tsx index cde69b9..6ee5867 100644 --- a/app/history/payment-volume/page.tsx +++ b/app/history/payment-volume/page.tsx @@ -1,4 +1,5 @@ import { loadHistory } from "@/lib/content"; +import { historyEventPath } from "@/lib/history-urls"; import { JsonLdScript } from "@hraness/web-discovery/json-ld"; import type { Metadata } from "next"; @@ -44,6 +45,7 @@ export default async function PaymentVolumePage() { data={[ historyCollectionJsonLd( records.map(({ event, point }) => ({ + categoryId: event.categoryId, id: event.id, title: `${point.calendarYear}: ${point.display} ${point.kind === "total-volume" ? "total volume" : "payment volume"}`, })), @@ -92,7 +94,7 @@ export default async function PaymentVolumePage() { {records.map(({ point }) => (
  • @@ -47,7 +35,7 @@ export function HistoryEventArticle({ )}

    - {title} +

    {event.title}

    {event.summary}

    {event.amount === undefined diff --git a/app/history/history-view.tsx b/app/history/history-view.tsx index 26e34bf..9d4b57f 100644 --- a/app/history/history-view.tsx +++ b/app/history/history-view.tsx @@ -5,7 +5,7 @@ import type { ValuationHeadlinePoint, } from "@/lib/content"; import type { TimelineCategoryId } from "@/lib/history-schema"; -import { historyCategoryPath, historyEventPath } from "@/lib/history-urls"; +import { historyCategoryPath } from "@/lib/history-urls"; import Link from "next/link"; import { HistoryEventArticle } from "./history-event-article"; @@ -119,7 +119,7 @@ function HistoryMeasuresSidebar({
  • {point.calendarYear} {point.display} diff --git a/app/history/payment-volume/page.tsx b/app/history/payment-volume/page.tsx index 6ee5867..cde69b9 100644 --- a/app/history/payment-volume/page.tsx +++ b/app/history/payment-volume/page.tsx @@ -1,5 +1,4 @@ import { loadHistory } from "@/lib/content"; -import { historyEventPath } from "@/lib/history-urls"; import { JsonLdScript } from "@hraness/web-discovery/json-ld"; import type { Metadata } from "next"; @@ -45,7 +44,6 @@ export default async function PaymentVolumePage() { data={[ historyCollectionJsonLd( records.map(({ event, point }) => ({ - categoryId: event.categoryId, id: event.id, title: `${point.calendarYear}: ${point.display} ${point.kind === "total-volume" ? "total volume" : "payment volume"}`, })), @@ -94,7 +92,7 @@ export default async function PaymentVolumePage() { {records.map(({ point }) => (
  • valuation by year

    @@ -147,7 +177,7 @@ export default async function ValuationPage() {