diff --git a/app/history/valuation/valuation-page-model.ts b/app/history/valuation/valuation-page-model.ts
index 68f66b3..5f2ab6a 100644
--- a/app/history/valuation/valuation-page-model.ts
+++ b/app/history/valuation/valuation-page-model.ts
@@ -1,7 +1,12 @@
-import { deriveValuationHeadlines, type HistoryCollection } from "@/lib/content";
+import {
+ deriveValuationHeadlines,
+ type HistoryCollection,
+ type ResolvedValuationObservation,
+} from "@/lib/content";
import type { ValuationObservation } from "@/lib/research-schema";
import type { Metadata } from "next";
+import { independenceSentence } from "../../site-copy";
import { site, socialMetadata } from "../../site";
export const mechanismLabel: Readonly<
@@ -15,19 +20,64 @@ export const mechanismLabel: Readonly<
"seed-financing": "seed financing",
};
+export const basisLabel: Readonly<
+ Record
+> = {
+ "common-stock-409a": "common-stock 409A",
+ "market-indication": "market indication",
+ "post-money": "post-money",
+ "pre-money": "pre-money",
+ "transaction-implied": "transaction implied",
+ unspecified: "basis not specified",
+};
+
+export const statusLabel: Readonly> = {
+ "agreements-signed": "agreements signed",
+ "company-confirmed": "company confirmed",
+ completed: "completed",
+ reported: "reported",
+ retrospective: "retrospective",
+};
+
export interface ValuationPageSeo {
readonly description: string;
+ readonly lead: string;
readonly title: string;
readonly yearRange: string;
}
+export interface ValuationHeadlineRow {
+ readonly basisLabel: string;
+ readonly calendarYear: number;
+ readonly display: string;
+ readonly observationId: string;
+ readonly sources: ResolvedValuationObservation["sources"];
+ readonly statusLabel: string;
+}
+
+function indefiniteArticle(phrase: string): "a" | "an" {
+ return /^[aeiou]/iu.test(phrase) ? "an" : "a";
+}
+
+function valuationYearRange(
+ headlines: ReturnType,
+): string {
+ const firstHeadline = headlines[0];
+ const latestHeadline = headlines.at(-1);
+ if (firstHeadline === undefined || latestHeadline === undefined) {
+ throw new Error("Valuation page requires at least one headline observation");
+ }
+ return firstHeadline.calendarYear === latestHeadline.calendarYear
+ ? String(firstHeadline.calendarYear)
+ : `${firstHeadline.calendarYear}–${latestHeadline.calendarYear}`;
+}
+
export function deriveValuationPageSeo(
history: Pick,
): ValuationPageSeo {
const headlines = deriveValuationHeadlines(history.valuations);
- const firstHeadline = headlines[0];
const latestHeadline = headlines.at(-1);
- if (firstHeadline === undefined || latestHeadline === undefined) {
+ if (latestHeadline === undefined) {
throw new Error("Valuation page requires at least one headline observation");
}
const latestObservation = history.valuations.find(
@@ -38,17 +88,47 @@ export function deriveValuationPageSeo(
`Valuation headline references missing observation ${latestHeadline.observationId}`,
);
}
- const yearRange = firstHeadline.calendarYear === latestHeadline.calendarYear
- ? String(firstHeadline.calendarYear)
- : `${firstHeadline.calendarYear}–${latestHeadline.calendarYear}`;
+ const yearRange = valuationYearRange(headlines);
+ const latestMechanism = mechanismLabel[latestObservation.mechanism];
+ const latestStatus = statusLabel[latestObservation.status];
+ const latestBasis = basisLabel[latestObservation.valuation.basis];
return {
description:
- `Stripe valuation history from its early venture rounds through the ${latestHeadline.display} ${latestHeadline.calendarYear} ${mechanismLabel[latestObservation.mechanism]}, with sourced financing, tender, 409A, investor-secondary, and market observations.`,
+ `Stripe valuation history from its early venture rounds through the ${latestHeadline.display} ${latestHeadline.calendarYear} ${latestMechanism}, with sourced financing, tender, 409A, investor-secondary, and market observations.`,
+ lead: [
+ `Stripe’s latest sourced private-company valuation headline is ${latestHeadline.display} in ${latestHeadline.calendarYear}, from ${indefiniteArticle(latestMechanism)} ${latestMechanism} with ${latestStatus} status, recorded as ${latestBasis}.`,
+ `This page selects one observation per year from ${yearRange}.`,
+ "Financing, tender, 409A, secondary, and market-indication figures are not interchangeable.",
+ independenceSentence,
+ ].join(" "),
title: `Stripe Valuation History by Year, ${yearRange}`,
yearRange,
};
}
+export function deriveValuationHeadlineRows(
+ history: Pick,
+): readonly ValuationHeadlineRow[] {
+ return deriveValuationHeadlines(history.valuations).map((headline) => {
+ const observation = history.valuations.find(
+ ({ id }) => id === headline.observationId,
+ );
+ if (observation === undefined) {
+ throw new Error(
+ `Valuation headline references missing observation ${headline.observationId}`,
+ );
+ }
+ return {
+ basisLabel: basisLabel[observation.valuation.basis],
+ calendarYear: headline.calendarYear,
+ display: headline.display,
+ observationId: observation.id,
+ sources: observation.sources,
+ statusLabel: statusLabel[observation.status],
+ };
+ });
+}
+
export function deriveValuationPageMetadata(
history: Pick,
): Metadata {
diff --git a/app/loading.tsx b/app/loading.tsx
deleted file mode 100644
index 8f33a63..0000000
--- a/app/loading.tsx
+++ /dev/null
@@ -1,14 +0,0 @@
-import { SiteHeader } from "./site-header";
-
-export default function Loading() {
- return (
-
-
- Loading Stripe company history…
-
- );
-}
diff --git a/app/metadata.test.ts b/app/metadata.test.ts
index 7915bbf..9c091bc 100644
--- a/app/metadata.test.ts
+++ b/app/metadata.test.ts
@@ -42,15 +42,21 @@ describe("stripedex.com public identity", () => {
`${SITE_ORIGIN}/data`,
...timelineCategoryIds.map((id) => `${SITE_ORIGIN}/history/${id}`),
]));
+ expect(urls).not.toContain(
+ `${SITE_ORIGIN}/history/acquisitions/openrouter-acquisition-talks-reported`,
+ );
expect(urls).not.toContain(`${SITE_ORIGIN}/appearances`);
+ expect(urls).not.toContain(`${SITE_ORIGIN}/x-markdown`);
expect(urls).not.toContain(SITE_ORIGIN);
expect(urls).not.toContain(`${SITE_ORIGIN}/history`);
expect(urls.some((url) => url.startsWith(`${SITE_ORIGIN}/news/`))).toBe(false);
+ expect(urls.some((url) => url.startsWith(`${SITE_ORIGIN}/x-markdown`))).toBe(false);
expect(entries.every((entry) => entry.changeFrequency === undefined)).toBe(true);
expect(entries.every((entry) => entry.priority === undefined)).toBe(true);
expect(robots()).toMatchObject({
rules: {
allow: "/",
+ disallow: "/x-markdown",
userAgent: "*",
},
sitemap: `${SITE_ORIGIN}/sitemap.xml`,
@@ -101,6 +107,14 @@ describe("stripedex.com public identity", () => {
headers: [{ key: "X-Robots-Tag", value: "noindex, follow" }],
source: "/research/:path*",
});
+ expect(await nextConfig.headers?.()).toContainEqual({
+ headers: [{ key: "X-Robots-Tag", value: "noindex, follow" }],
+ source: "/x-markdown",
+ });
+ expect(await nextConfig.headers?.()).toContainEqual({
+ headers: [{ key: "X-Robots-Tag", value: "noindex, follow" }],
+ source: "/x-markdown/:path*",
+ });
expect(await nextConfig.headers?.()).toContainEqual({
headers: [{ key: "Vary", value: "Accept" }],
source: "/",
diff --git a/app/page.test.tsx b/app/page.test.tsx
index f5b0822..c36005c 100644
--- a/app/page.test.tsx
+++ b/app/page.test.tsx
@@ -61,7 +61,11 @@ describe("canonical stripedex.com history", () => {
expect(html.match(/class="history-volume-track"/gu)?.length).toBe(
history.annualVolumes.length + history.valuationHeadlines.length,
);
+ expect(html).not.toContain("Loading Stripe company history");
expect(html).toContain("Stripe reportedly discusses acquiring OpenRouter");
+ expect(html).not.toContain(
+ 'href="/history/acquisitions/openrouter-acquisition-talks-reported"',
+ );
expect(html).toContain("Tokens Are the New Dollars");
expect(html).toContain('data-category="appearances"');
expect(html).toContain("A month in Buenos Aires produces Stripe's first working prototype");
diff --git a/app/robots.ts b/app/robots.ts
index a8ae803..e41367d 100644
--- a/app/robots.ts
+++ b/app/robots.ts
@@ -6,6 +6,7 @@ export default function robots(): MetadataRoute.Robots {
rules: {
userAgent: "*",
allow: "/",
+ disallow: "/x-markdown",
},
host: SITE_ORIGIN,
sitemap: `${SITE_ORIGIN}/sitemap.xml`,
diff --git a/app/runtime-surfaces.test.tsx b/app/runtime-surfaces.test.tsx
index c52285e..ddab0ae 100644
--- a/app/runtime-surfaces.test.tsx
+++ b/app/runtime-surfaces.test.tsx
@@ -1,10 +1,11 @@
import { describe, expect, test } from "bun:test";
+import { existsSync } from "node:fs";
import { readFile } from "node:fs/promises";
+import { fileURLToPath } from "node:url";
import { renderToStaticMarkup } from "react-dom/server";
import ErrorPage from "./error";
import GlobalError from "./global-error";
-import Loading from "./loading";
import NotFound from "./not-found";
describe("standalone runtime surfaces", () => {
@@ -41,12 +42,15 @@ describe("standalone runtime surfaces", () => {
expect(`${route}${document}`).not.toContain("PostHog");
});
- test("renders loading and not-found states with useful navigation", () => {
- const loading = renderToStaticMarkup();
+ test("does not ship a root loading shell that can replace history HTML", () => {
+ expect(existsSync(fileURLToPath(new URL("./loading.tsx", import.meta.url)))).toBe(
+ false,
+ );
+ });
+
+ test("renders not-found states with useful navigation", () => {
const notFound = renderToStaticMarkup();
- expect(loading).toContain('aria-busy="true"');
- expect(loading).toContain('role="status"');
expect(notFound).toContain("Page not found");
expect(notFound).toContain('href="/"');
expect(notFound).toContain('href="/llms.txt"');
@@ -54,7 +58,6 @@ describe("standalone runtime surfaces", () => {
expect(notFound).toContain('href="/about"');
expect(notFound).toContain('href="/contact"');
expect(notFound).toContain('href="/privacy"');
- expect(loading.match(/data-presentation="menu"/gu)).toHaveLength(1);
expect(notFound.match(/data-presentation="menu"/gu)).toHaveLength(1);
expect(notFound).toContain('aria-label="hraness"');
});
diff --git a/app/seo.ts b/app/seo.ts
index 912b026..79b0327 100644
--- a/app/seo.ts
+++ b/app/seo.ts
@@ -1,3 +1,5 @@
+import type { HistoryCollection } from "@/lib/content";
+
import {
GITHUB_REPOSITORY_URL,
HRANESS_URL,
@@ -5,7 +7,6 @@ import {
site,
type SitePath,
} from "./site";
-import type { HistoryCollection } from "@/lib/content";
export interface BreadcrumbItem {
readonly name: string;
diff --git a/app/x-markdown/[[...path]]/route.test.ts b/app/x-markdown/[[...path]]/route.test.ts
new file mode 100644
index 0000000..d067616
--- /dev/null
+++ b/app/x-markdown/[[...path]]/route.test.ts
@@ -0,0 +1,73 @@
+import { describe, expect, test } from "bun:test";
+import { loadHistory } from "@/lib/content";
+import { MARKDOWN_CONTENT_TYPE } from "@/lib/accept";
+
+import { GET, HEAD, generateStaticParams } from "./route";
+
+describe("Node markdown corpus handler", () => {
+ test("serves homepage, category, and recovery markdown", async () => {
+ const history = await loadHistory();
+ const openRouter = history.events.find(
+ ({ id }) => id === "openrouter-acquisition-talks-reported",
+ );
+ const root = await GET(new Request("https://stripedex.com/x-markdown"), {
+ params: Promise.resolve({}),
+ });
+ const acquisitions = await GET(
+ new Request("https://stripedex.com/x-markdown/history/acquisitions"),
+ { params: Promise.resolve({ path: ["history", "acquisitions"] }) },
+ );
+ const missing = await GET(
+ new Request("https://stripedex.com/x-markdown/this-does-not-exist"),
+ { params: Promise.resolve({ path: ["this-does-not-exist"] }) },
+ );
+ const missingEvent = await GET(
+ new Request(
+ "https://stripedex.com/x-markdown/history/acquisitions/openrouter-acquisition-talks-reported",
+ ),
+ {
+ params: Promise.resolve({
+ path: [
+ "history",
+ "acquisitions",
+ "openrouter-acquisition-talks-reported",
+ ],
+ }),
+ },
+ );
+ const head = await HEAD(new Request("https://stripedex.com/x-markdown"), {
+ params: Promise.resolve({}),
+ });
+
+ expect(root.status).toBe(200);
+ expect(root.headers.get("content-type")).toBe(MARKDOWN_CONTENT_TYPE);
+ expect(await root.text()).toContain(
+ `# Stripe Company History: ${history.events.length} Sourced Events`,
+ );
+ expect(acquisitions.status).toBe(200);
+ expect(await acquisitions.text()).toContain(
+ openRouter?.title ?? "missing-openrouter",
+ );
+ const missingBody = await missing.text();
+ expect(missing.status).toBe(404);
+ expect(missingBody).toContain("https://stripedex.com/sitemap.xml");
+ expect(missingBody).toContain("https://stripedex.com/llms.txt");
+ expect(missingEvent.status).toBe(404);
+ expect(head.status).toBe(200);
+ expect(await head.text()).toBe("");
+ expect(head.headers.get("content-type")).toBe(MARKDOWN_CONTENT_TYPE);
+ });
+
+ test("prebuilds public document paths without per-event routes", async () => {
+ const history = await loadHistory();
+ const params = await generateStaticParams();
+ expect(params).toContainEqual({ path: [] });
+ expect(params).toContainEqual({ path: ["about"] });
+ expect(params).toContainEqual({ path: ["history", "acquisitions"] });
+ expect(params).toContainEqual({ path: ["history", "valuation"] });
+ expect(params).not.toContainEqual({
+ path: ["history", "acquisitions", "openrouter-acquisition-talks-reported"],
+ });
+ expect(params.length).toBe(8 + history.categories.length);
+ });
+});
diff --git a/app/x-markdown/[[...path]]/route.ts b/app/x-markdown/[[...path]]/route.ts
new file mode 100644
index 0000000..d053be0
--- /dev/null
+++ b/app/x-markdown/[[...path]]/route.ts
@@ -0,0 +1,69 @@
+import { MARKDOWN_CONTENT_TYPE } from "@/lib/accept";
+import { loadHistory } from "@/lib/content";
+import {
+ markdownRewritePath,
+ publicPathFromMarkdownRewrite,
+} from "@/lib/history-urls";
+import { markdownForPath, markdownHeaders } from "@/lib/page-markdown";
+
+export const dynamic = "force-static";
+export const dynamicParams = true;
+
+function pathnameFromSegments(path: readonly string[] | undefined): string {
+ return path === undefined || path.length === 0 ? "/" : `/${path.join("/")}`;
+}
+
+export async function generateStaticParams() {
+ const history = await loadHistory();
+ const publicPaths = [
+ "/",
+ "/about",
+ "/contact",
+ "/data",
+ "/history",
+ "/privacy",
+ "/history/payment-volume",
+ "/history/valuation",
+ ...history.categories.map(({ id }) => `/history/${id}`),
+ ];
+ return publicPaths.map((pathname) => {
+ const rewrite = markdownRewritePath(pathname);
+ const publicPath = publicPathFromMarkdownRewrite(rewrite);
+ const segments = publicPath === "/"
+ ? []
+ : publicPath === null
+ ? []
+ : publicPath.slice(1).split("/");
+ return { path: segments };
+ });
+}
+
+async function markdownResponse(path: readonly string[] | undefined): Promise {
+ const document = await markdownForPath(pathnameFromSegments(path));
+ return new Response(document.body, {
+ headers: markdownHeaders(),
+ status: document.status,
+ });
+}
+
+export async function GET(
+ _request: Request,
+ context: Readonly<{ params: Promise<{ path?: string[] }> }>,
+) {
+ const { path } = await context.params;
+ return markdownResponse(path);
+}
+
+export async function HEAD(
+ _request: Request,
+ context: Readonly<{ params: Promise<{ path?: string[] }> }>,
+) {
+ const response = await markdownResponse((await context.params).path);
+ return new Response(null, {
+ headers: {
+ "Content-Type": MARKDOWN_CONTENT_TYPE,
+ Vary: "Accept",
+ },
+ status: response.status,
+ });
+}
diff --git a/lib/AGENTS.md b/lib/AGENTS.md
index 3d33344..d7c2171 100644
--- a/lib/AGENTS.md
+++ b/lib/AGENTS.md
@@ -5,6 +5,7 @@
- `automated-publication-schema.ts` – reviewed model policy and hash-only publication attestation contracts.
- `content.ts` – deterministic loading, source resolution, validation, categorization, chronology, annual-volume extraction, and valuation selection.
- `accept.ts` – Accept parsing and markdown negotiation decisions.
+- `history-urls.ts` – durable category paths and the internal Markdown rewrite.
- `page-markdown.ts` and `llms-txt.ts` – Markdown representations of existing public pages and the agent index.
- `*.test.ts` – schema, ordering, uniqueness, and source-provenance regressions.
diff --git a/lib/accept.test.ts b/lib/accept.test.ts
index af6d417..62a219e 100644
--- a/lib/accept.test.ts
+++ b/lib/accept.test.ts
@@ -43,6 +43,8 @@ describe("agent representation negotiation", () => {
expect(shouldSkipNegotiation("/llms.txt")).toBe(true);
expect(shouldSkipNegotiation("/about.md")).toBe(false);
expect(shouldSkipNegotiation("/about")).toBe(false);
+ expect(shouldSkipNegotiation("/x-markdown")).toBe(true);
+ expect(shouldSkipNegotiation("/x-markdown/about")).toBe(true);
expect(decideRepresentation({
accept: "text/markdown",
method: "POST",
diff --git a/lib/accept.ts b/lib/accept.ts
index b3e1732..2b1c662 100644
--- a/lib/accept.ts
+++ b/lib/accept.ts
@@ -1,4 +1,9 @@
+import { isMarkdownRewritePath } from "./history-urls";
+
export const PRODUCED_MEDIA_TYPES = ["text/html", "text/markdown"] as const;
+export const MARKDOWN_CONTENT_TYPE = "text/markdown; charset=utf-8";
+export const NOT_ACCEPTABLE_BODY =
+ "Not Acceptable\n\nAvailable: text/html, text/markdown\n";
export type ProducedMediaType = (typeof PRODUCED_MEDIA_TYPES)[number];
@@ -107,6 +112,7 @@ const SKIP_NEGOTIATION_PATHS = new Set([
export function shouldSkipNegotiation(pathname: string): boolean {
if (SKIP_NEGOTIATION_PATHS.has(pathname)) return true;
+ if (isMarkdownRewritePath(pathname)) return true;
if (SKIP_NEGOTIATION_PREFIXES.some((prefix) => pathname.startsWith(prefix))) {
return true;
}
diff --git a/lib/history-urls.test.ts b/lib/history-urls.test.ts
new file mode 100644
index 0000000..6c6862d
--- /dev/null
+++ b/lib/history-urls.test.ts
@@ -0,0 +1,30 @@
+import { describe, expect, test } from "bun:test";
+
+import {
+ historyCategoryPath,
+ isMarkdownRewritePath,
+ markdownRewritePath,
+ publicPathFromMarkdownRewrite,
+} from "./history-urls";
+
+describe("history URL helpers", () => {
+ test("builds durable category paths", () => {
+ expect(historyCategoryPath("acquisitions")).toBe("/history/acquisitions");
+ });
+
+ test("maps public document paths onto the internal markdown rewrite", () => {
+ expect(markdownRewritePath("/")).toBe("/x-markdown");
+ expect(markdownRewritePath("/about")).toBe("/x-markdown/about");
+ expect(markdownRewritePath("/history/acquisitions")).toBe(
+ "/x-markdown/history/acquisitions",
+ );
+ expect(isMarkdownRewritePath("/x-markdown")).toBe(true);
+ expect(isMarkdownRewritePath("/x-markdown/about")).toBe(true);
+ expect(isMarkdownRewritePath("/about")).toBe(false);
+ expect(publicPathFromMarkdownRewrite("/x-markdown")).toBe("/");
+ expect(publicPathFromMarkdownRewrite("/x-markdown/history/acquisitions")).toBe(
+ "/history/acquisitions",
+ );
+ expect(publicPathFromMarkdownRewrite("/about")).toBeNull();
+ });
+});
diff --git a/lib/history-urls.ts b/lib/history-urls.ts
new file mode 100644
index 0000000..f72e8d5
--- /dev/null
+++ b/lib/history-urls.ts
@@ -0,0 +1,27 @@
+import { type TimelineCategoryId } from "./history-schema";
+
+export const MARKDOWN_REWRITE_PREFIX = "/x-markdown" as const;
+
+export type HistoryCategoryPath = `/history/${TimelineCategoryId}`;
+
+export function historyCategoryPath(
+ categoryId: TimelineCategoryId,
+): HistoryCategoryPath {
+ return `/history/${categoryId}`;
+}
+
+export function markdownRewritePath(pathname: string): string {
+ if (pathname === "/") return MARKDOWN_REWRITE_PREFIX;
+ return `${MARKDOWN_REWRITE_PREFIX}${pathname}`;
+}
+
+export function isMarkdownRewritePath(pathname: string): boolean {
+ return pathname === MARKDOWN_REWRITE_PREFIX
+ || pathname.startsWith(`${MARKDOWN_REWRITE_PREFIX}/`);
+}
+
+export function publicPathFromMarkdownRewrite(pathname: string): string | null {
+ if (!isMarkdownRewritePath(pathname)) return null;
+ const rest = pathname.slice(MARKDOWN_REWRITE_PREFIX.length);
+ return rest === "" ? "/" : rest;
+}
diff --git a/lib/page-markdown.test.ts b/lib/page-markdown.test.ts
index 8d0d519..0246516 100644
--- a/lib/page-markdown.test.ts
+++ b/lib/page-markdown.test.ts
@@ -28,6 +28,13 @@ describe("agent markdown representations", () => {
expect(page.body).toContain("not affiliated with, endorsed by, or operated by");
expect(page.body).toContain("https://stripedex.com/history/acquisitions");
expect(page.body).not.toContain(history.events[0]?.title ?? "missing-event");
+ expect(page.body).not.toContain("/history/acquisitions/openrouter-acquisition-talks-reported");
+ });
+
+ test("treats invented per-event routes as missing pages", async () => {
+ expect((await markdownForPath(
+ "/history/acquisitions/openrouter-acquisition-talks-reported",
+ )).status).toBe(404);
});
test("renders category, volume, about, contact, and privacy pages from the same records", async () => {
@@ -42,6 +49,13 @@ describe("agent markdown representations", () => {
expect(volume.body).toContain("2025");
expect(volume.body).toContain("total volume");
+ const valuation = await markdownForPath("/history/valuation");
+ expect(valuation.status).toBe(200);
+ expect(valuation.body).toContain("| year | valuation | basis | status | sources |");
+ expect(valuation.body).toContain("$159 billion");
+ expect(valuation.body).toContain("transaction implied");
+ expect(valuation.body).toContain("not affiliated with, endorsed by, or operated by");
+
const about = await markdownForPath("/about");
expect(about.body).toContain("founder side projects and aesthetics programs");
expect(visibleText((await markdownForPath("/privacy")).body).length).toBeGreaterThan(500);
diff --git a/lib/page-markdown.ts b/lib/page-markdown.ts
index 9a6c5cd..9bc9032 100644
--- a/lib/page-markdown.ts
+++ b/lib/page-markdown.ts
@@ -1,3 +1,6 @@
+import {
+ MARKDOWN_CONTENT_TYPE,
+} from "./accept";
import {
aboutDescription,
aboutSections,
@@ -20,6 +23,10 @@ import {
recoveryLinks,
} from "@/app/site-copy";
import { GITHUB_REPOSITORY_URL, SITE_ORIGIN, site } from "@/app/site";
+import {
+ deriveValuationHeadlineRows,
+ deriveValuationPageSeo,
+} from "@/app/history/valuation/valuation-page-model";
import {
loadHistory,
type CategorizedHistoryEvent,
@@ -28,9 +35,7 @@ import {
import { timelineCategoryIds, type TimelineCategoryId } from "./history-schema";
import { llmsTxt } from "./llms-txt";
-export const MARKDOWN_CONTENT_TYPE = "text/markdown; charset=utf-8";
-export const NOT_ACCEPTABLE_BODY =
- "Not Acceptable\n\nAvailable: text/html, text/markdown\n";
+export { MARKDOWN_CONTENT_TYPE, NOT_ACCEPTABLE_BODY } from "./accept";
export interface MarkdownDocument {
readonly body: string;
@@ -257,16 +262,37 @@ function paymentVolumeMarkdown(history: HistoryCollection): string {
].join("\n");
}
+function markdownTableCell(value: string): string {
+ return value.replaceAll("|", "\\|").replaceAll("\n", " ");
+}
+
function valuationMarkdown(history: HistoryCollection): string {
+ const seo = deriveValuationPageSeo(history);
+ const rows = deriveValuationHeadlineRows(history);
+ const table = [
+ "| year | valuation | basis | status | sources |",
+ "| --- | --- | --- | --- | --- |",
+ ...rows.map((row) => {
+ const sources = row.sources
+ .map((source) => `[${source.publisher}](${source.url})`)
+ .join(" · ");
+ return `| ${row.calendarYear} | ${markdownTableCell(row.display)} | ${markdownTableCell(row.basisLabel)} | ${markdownTableCell(row.statusLabel)} | ${markdownTableCell(sources)} |`;
+ }),
+ ];
return [
- heading(
- "Stripe private-company valuation history",
- "Sourced private-company valuation observations for Stripe, with status, basis, and linked evidence.",
- ),
+ heading(seo.title, seo.description),
+ seo.lead,
+ "",
+ "## Yearly headlines",
+ "",
+ ...table,
+ "",
+ "## Observations and sources",
+ "",
...history.valuations.map((observation) => {
- const sources = observation.sources.map((source) => `[${source.title}](${source.url})`).join(" · ");
+ const sources = observation.sources.map((source) => `[${source.publisher}](${source.url})`).join(" · ");
return [
- `### ${observation.valuation.display}`,
+ `### ${observation.title}`,
"",
`${observation.effective_date} · ${observation.status} · ${observation.valuation.basis}`,
"",
diff --git a/next.config.ts b/next.config.ts
index 18377a0..f30ca09 100644
--- a/next.config.ts
+++ b/next.config.ts
@@ -10,6 +10,8 @@ const nextConfig: NextConfig = {
return [
{ headers: noindexHeaders, source: "/history/:category.yml" },
{ headers: noindexHeaders, source: "/research/:path*" },
+ { headers: noindexHeaders, source: "/x-markdown" },
+ { headers: noindexHeaders, source: "/x-markdown/:path*" },
{ headers: [{ key: "Vary", value: "Accept" }], source: "/" },
{ headers: [{ key: "Vary", value: "Accept" }], source: "/:path*" },
];
diff --git a/proxy.test.ts b/proxy.test.ts
new file mode 100644
index 0000000..9e26245
--- /dev/null
+++ b/proxy.test.ts
@@ -0,0 +1,51 @@
+import { describe, expect, test } from "bun:test";
+import { readFile } from "node:fs/promises";
+import { NextRequest } from "next/server";
+
+import { proxy } from "./proxy";
+
+function request(
+ pathname: string,
+ accept: string,
+ method = "GET",
+): NextRequest {
+ return new NextRequest(new URL(pathname, "https://stripedex.com"), {
+ headers: { accept },
+ method,
+ });
+}
+
+describe("Accept negotiation proxy", () => {
+ test("stays filesystem-free so Vercel can run it without the YAML corpus", async () => {
+ const source = await readFile(new URL("./proxy.ts", import.meta.url), "utf8");
+ expect(source).not.toContain("markdownForPath");
+ expect(source).not.toContain("loadHistory");
+ expect(source).not.toContain("node:fs");
+ expect(source).not.toContain("@/lib/content");
+ expect(source).not.toContain("@/lib/page-markdown");
+ });
+
+ test("rewrites markdown Accept and .md siblings to the Node corpus handler", async () => {
+ const root = await proxy(request("/", "text/markdown"));
+ expect(root.headers.get("vary")).toBe("Accept");
+ expect(root.headers.get("x-middleware-rewrite")).toContain("/x-markdown");
+ expect(root.headers.get("x-middleware-rewrite")).not.toContain("/x-markdown/");
+
+ const about = await proxy(request("/about", "text/markdown"));
+ expect(about.headers.get("x-middleware-rewrite")).toContain("/x-markdown/about");
+
+ const sibling = await proxy(request("/about.md", "text/html"));
+ expect(sibling.headers.get("x-middleware-rewrite")).toContain("/x-markdown/about");
+
+ const missing = await proxy(request("/this-does-not-exist", "text/markdown"));
+ expect(missing.headers.get("x-middleware-rewrite")).toContain(
+ "/x-markdown/this-does-not-exist",
+ );
+ });
+
+ test("returns 406 without inventing an API when no produced type is accepted", async () => {
+ const response = await proxy(request("/", "application/pdf"));
+ expect(response.status).toBe(406);
+ expect(await response.text()).toContain("text/html, text/markdown");
+ });
+});
diff --git a/proxy.ts b/proxy.ts
index d840f23..a3b7a67 100644
--- a/proxy.ts
+++ b/proxy.ts
@@ -4,12 +4,9 @@ import {
appendVaryAccept,
decideRepresentation,
isNextRscRequest,
-} from "./lib/accept";
-import {
- MARKDOWN_CONTENT_TYPE,
NOT_ACCEPTABLE_BODY,
- markdownForPath,
-} from "./lib/page-markdown";
+} from "./lib/accept";
+import { markdownRewritePath } from "./lib/history-urls";
export async function proxy(request: NextRequest) {
const decision = decideRepresentation({
@@ -34,14 +31,15 @@ export async function proxy(request: NextRequest) {
}
if (decision.kind === "markdown") {
- const document = await markdownForPath(decision.pathname);
- return new NextResponse(document.body, {
- headers: {
- "Content-Type": MARKDOWN_CONTENT_TYPE,
- Vary: "Accept",
- },
- status: document.status,
+ const url = request.nextUrl.clone();
+ url.pathname = markdownRewritePath(decision.pathname);
+ const headers = new Headers(request.headers);
+ headers.set("x-stripedex-representation", "markdown");
+ const response = NextResponse.rewrite(url, {
+ request: { headers },
});
+ response.headers.set("Vary", "Accept");
+ return response;
}
const response = NextResponse.next();