Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions app/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@
- `page.tsx` – the canonical unified Stripe history timeline.
- `history/` – category pages, the appearances projection, annual-volume and valuation pages, and shared timeline rendering.
- `data/` – the crawlable history and research dataset index.
- `about/` – sourcing, review, independence, corrections, and privacy.
- `site.ts`, `site-header.tsx`, and `site-footer.tsx` – canonical identity and shared page chrome.
- `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.
- `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.
- `robots.ts`, `sitemap.ts`, `manifest.ts`, and `opengraph-image.tsx` – public discovery and sharing surfaces.
Expand Down
2 changes: 2 additions & 0 deletions app/about/page.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ describe("stripedex.com about page", () => {
expect(html).toContain('aria-label="Appearance: System"');
expect(html).toContain('href="https://hraness.com/"');
expect(html).toContain('href="https://github.com/hraness/stripedex"');
expect(html).toContain('href="/contact"');
expect(html).toContain('href="/privacy"');
expect(html).not.toContain("Atom feed");
expect(html).not.toContain("news summaries");
expect(html).toContain('type="application/ld+json"');
Expand Down
8 changes: 7 additions & 1 deletion app/about/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,9 @@ export default function AboutPage() {
Published and maintained by <a href={HRANESS_URL}>Hraness</a>. To
suggest a correction, add a source, or improve the project, open an
issue or contribution in the{" "}
<a href={GITHUB_REPOSITORY_URL}>Stripedex repository</a>.
<a href={GITHUB_REPOSITORY_URL}>Stripedex repository</a>. The same
public channels are listed on the{" "}
<Link href="/contact">contact page</Link>.
</p>

<h2>Privacy</h2>
Expand All @@ -105,6 +107,10 @@ export default function AboutPage() {
no user accounts or authentication. Requests are still subject to the
ordinary logs and security controls of the hosting provider.
</p>
<p>
The dedicated <Link href="/privacy">privacy page</Link> repeats this
policy for agents and other readers who look for <code>/privacy</code>.
</p>
</section>
<SiteFooter />
</main>
Expand Down
2 changes: 2 additions & 0 deletions app/analytics.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ describe("Stripedex analytics routes", () => {
expect(JSON.stringify(PUBLIC_ANALYTICS_PATHS)).toBe(JSON.stringify([
"/",
"/about",
"/contact",
"/privacy",
"/data",
"/history/payment-volume",
"/history/valuation",
Expand Down
2 changes: 2 additions & 0 deletions app/analytics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ const CANONICAL_ORIGIN = `https://${CANONICAL_DOMAIN}`;
const STATIC_ROUTES = [
["/", "history_timeline"],
["/about", "about"],
["/contact", "contact"],
["/privacy", "privacy"],
["/data", "data_index"],
["/history/payment-volume", "payment_volume"],
["/history/valuation", "valuation"],
Expand Down
26 changes: 26 additions & 0 deletions app/contact/page.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { describe, expect, test } from "bun:test";
import { renderToStaticMarkup } from "react-dom/server";

import ContactPage, { metadata } from "./page";

function visibleText(html: string): string {
return html.replace(/<script[\s\S]*?<\/script>/gu, " ").replace(/<[^>]+>/gu, " ").replace(/\s+/gu, " ").trim();
}

describe("stripedex.com contact page", () => {
test("lists the existing public correction and security channels", () => {
const html = renderToStaticMarkup(<ContactPage />);
expect(metadata).toMatchObject({
alternates: { canonical: "/contact" },
title: "Contact",
});
expect(html).toContain("<h1 id=\"contact-heading\">Contact stripedex.com</h1>");
expect(html).toContain("https://github.com/hraness/stripedex/issues");
expect(html).toContain("private vulnerability reporting");
expect(html).toContain("not affiliated with, endorsed by, or operated by");
expect(html).toContain("There is no reader account, contact form, or product inbox");
expect(html).not.toContain("@stripedex.com");
expect(html).toContain('aria-label="Appearance: System"');
expect(visibleText(html).length).toBeGreaterThan(500);
});
});
94 changes: 94 additions & 0 deletions app/contact/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import { JsonLdScript } from "@hraness/web-discovery/json-ld";
import type { Metadata } from "next";
import Link from "next/link";

import { breadcrumbJsonLd } from "../seo";
import { SiteFooter } from "../site-footer";
import { SiteHeader } from "../site-header";
import {
contactDescription,
contactSocialTitle,
contactTitle,
independenceSentence,
} from "../site-copy";
import {
GITHUB_REPOSITORY_URL,
HRANESS_URL,
site,
socialMetadata,
} from "../site";

export const dynamic = "force-static";

export const metadata: Metadata = {
title: contactTitle,
description: contactDescription,
alternates: { canonical: "/contact" },
...socialMetadata(contactSocialTitle, contactDescription, "/contact", {
alt: `Contact channels for the independent Stripe company history at ${site.domain}`,
}),
};

export default function ContactPage() {
return (
<main className="plain-page stripedex-main" id="main-content">
<JsonLdScript
data={breadcrumbJsonLd([
{ name: "History", path: "/" },
{ name: "Contact", path: "/contact" },
])}
id="stripedex-contact-structured-data"
/>
<SiteHeader />
<nav aria-label="Breadcrumb" className="stripedex-breadcrumbs">
<Link href="/">history</Link>
<span aria-hidden="true"> / </span>
<span>contact</span>
</nav>
<section
aria-labelledby="contact-heading"
className="stripedex-about stripedex-section"
>
<div className="stripedex-section-heading">
<h1 id="contact-heading">Contact {site.domain}</h1>
<span>public channels</span>
</div>
<h2>Corrections and sources</h2>
<p>
Use public GitHub issues for ordinary historical corrections, missing
events, stronger sources, and focused software improvements. Include
the event date, a concise factual claim, its category, the proposed
confidence and status, and at least one source URL. Prefer primary
sources. If a claim was only proposed or reported, keep that
uncertainty in the record.
</p>
<p>
Open those reports in the{" "}
<a href={`${GITHUB_REPOSITORY_URL}/issues`}>
Stripedex issue tracker
</a>.
</p>
<h2>Security</h2>
<p>
Report suspected vulnerabilities through GitHub&apos;s private
vulnerability reporting for this repository. Do not include sensitive
details in a public issue.
</p>
<h2>Publisher</h2>
<p>
There is no reader account, contact form, or product inbox on{" "}
{site.domain}. The project does not process payments, issue API keys,
or operate a Stripe integration. {independenceSentence}
</p>
<p>
Published and maintained by <a href={HRANESS_URL}>Hraness</a>. The
complete sourced records and website code are in the{" "}
<a href={GITHUB_REPOSITORY_URL}>Stripedex repository</a>. Read{" "}
<Link href="/about">about</Link> for editorial method and{" "}
<Link href="/privacy">privacy</Link> for analytics limits.
</p>
</section>
<SiteFooter />
</main>
);
}
7 changes: 5 additions & 2 deletions app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import type { Metadata, Viewport } from "next";
import type { ReactNode } from "react";
import "./globals.css";
import { PostHogAnalytics } from "./posthog-analytics";
import { websiteJsonLd } from "./seo";
import { siteOrganizationJsonLd, websiteJsonLd } from "./seo";
import { SITE_ORIGIN, site } from "./site";

export const metadata: Metadata = {
Expand Down Expand Up @@ -49,7 +49,10 @@ export default function RootLayout({ children }: Readonly<{ children: ReactNode
return (
<html data-theme="light" lang="en-US" suppressHydrationWarning>
<body className={siteThemes.plain.bodyClassName}>
<JsonLdScript data={websiteJsonLd()} id="stripedex-website-structured-data" />
<JsonLdScript
data={[websiteJsonLd(), siteOrganizationJsonLd()]}
id="stripedex-website-structured-data"
/>
<PostHogAnalytics
apiHost={process.env.NEXT_PUBLIC_POSTHOG_HOST}
apiKey={process.env.NEXT_PUBLIC_POSTHOG_KEY}
Expand Down
14 changes: 14 additions & 0 deletions app/llms.txt/route.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { describe, expect, test } from "bun:test";

import { GET } from "./route";

describe("llms.txt route", () => {
test("serves the agent index as plain text", async () => {
const response = await GET();
const body = await response.text();
expect(response.status).toBe(200);
expect(response.headers.get("content-type")).toBe("text/plain; charset=utf-8");
expect(body).toContain("## When to use this");
expect(body).toContain("# Stripedex");
});
});
12 changes: 12 additions & 0 deletions app/llms.txt/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { llmsTxt } from "@/lib/llms-txt";

export const dynamic = "force-static";

export async function GET() {
return new Response(await llmsTxt(), {
headers: {
"Content-Type": "text/plain; charset=utf-8",
"Cache-Control": "public, max-age=0, must-revalidate",
},
});
}
10 changes: 10 additions & 0 deletions app/metadata.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ describe("stripedex.com public identity", () => {
`${SITE_ORIGIN}/history/payment-volume`,
`${SITE_ORIGIN}/history/valuation`,
`${SITE_ORIGIN}/about`,
`${SITE_ORIGIN}/contact`,
`${SITE_ORIGIN}/privacy`,
`${SITE_ORIGIN}/data`,
...timelineCategoryIds.map((id) => `${SITE_ORIGIN}/history/${id}`),
]));
Expand Down Expand Up @@ -99,6 +101,14 @@ describe("stripedex.com public identity", () => {
headers: [{ key: "X-Robots-Tag", value: "noindex, follow" }],
source: "/research/:path*",
});
expect(await nextConfig.headers?.()).toContainEqual({
headers: [{ key: "Vary", value: "Accept" }],
source: "/",
});
expect(await nextConfig.headers?.()).toContainEqual({
headers: [{ key: "Vary", value: "Accept" }],
source: "/:path*",
});
});

test("redirects former and www hosts directly to the canonical origin", async () => {
Expand Down
19 changes: 15 additions & 4 deletions app/not-found.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@ import Link from "next/link";

import { SiteFooter } from "./site-footer";
import { SiteHeader } from "./site-header";

const notFoundTitle = "Page not found";
const notFoundDescription = "The requested Stripe history page does not exist.";
import {
notFoundDescription,
notFoundTitle,
recoveryLinks,
} from "./site-copy";

export const metadata: Metadata = {
title: notFoundTitle,
Expand All @@ -21,7 +23,16 @@ export default function NotFound() {
<section aria-labelledby="not-found-heading" className="stripedex-section">
<h1 id="not-found-heading">{notFoundTitle}</h1>
<p>{notFoundDescription}</p>
<p><Link href="/">Browse Stripe company history</Link></p>
<p>Continue from:</p>
<ul>
{recoveryLinks.map((link) => (
<li key={link.href}>
<Link href={new URL(link.href).pathname === "/" ? "/" : new URL(link.href).pathname}>
{link.label}
</Link>
</li>
))}
</ul>
</section>
<SiteFooter />
</main>
Expand Down
25 changes: 25 additions & 0 deletions app/privacy/page.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { describe, expect, test } from "bun:test";
import { renderToStaticMarkup } from "react-dom/server";

import PrivacyPage, { metadata } from "./page";

function visibleText(html: string): string {
return html.replace(/<script[\s\S]*?<\/script>/gu, " ").replace(/<[^>]+>/gu, " ").replace(/\s+/gu, " ").trim();
}

describe("stripedex.com privacy page", () => {
test("publishes the existing analytics policy at /privacy", () => {
const html = renderToStaticMarkup(<PrivacyPage />);
expect(metadata).toMatchObject({
alternates: { canonical: "/privacy" },
title: "Privacy",
});
expect(html).toContain('<h1 id="privacy-heading">Privacy</h1>');
expect(html).toContain("anonymous, cookieless pageview events for public pages");
expect(html).toContain("does not save an analytics cookie or identifier");
expect(html).toContain("no user accounts or authentication");
expect(html).toContain('href="/contact"');
expect(html).toContain('aria-label="Appearance: System"');
expect(visibleText(html).length).toBeGreaterThan(500);
});
});
Loading