From 98a8ad10b46f57e7939213c4dee9e0b7a9b31090 Mon Sep 17 00:00:00 2001 From: sonpiaz Date: Fri, 2 Oct 2026 08:04:21 -0700 Subject: [PATCH] feat: publish markdown twins for rankings, home, and changelog Program detail pages already had a .md file. The rankings page, the homepage, and the changelog did not, and the programs index did not advertise its markdown twin. --- .gitignore | 3 + scripts/generate-md.ts | 94 +++++++++++++++++++++++++++++- scripts/verify-machine-surfaces.ts | 16 +++++ src/app/changelog/page.tsx | 63 +++----------------- src/app/page.tsx | 10 ++++ src/app/programs/layout.tsx | 6 ++ src/app/rankings/layout.tsx | 6 ++ src/lib/changelog.ts | 55 +++++++++++++++++ 8 files changed, 195 insertions(+), 58 deletions(-) create mode 100644 src/lib/changelog.ts diff --git a/.gitignore b/.gitignore index 87b22e74..d4d544cc 100644 --- a/.gitignore +++ b/.gitignore @@ -87,6 +87,9 @@ reports/ /public/llms-full.txt /public/programs.md /public/docs.md +/public/index.md +/public/rankings.md +/public/changelog.md /public/programs/ /public/categories/ /public/networks/ diff --git a/scripts/generate-md.ts b/scripts/generate-md.ts index f15783c6..f55ee4a8 100644 --- a/scripts/generate-md.ts +++ b/scripts/generate-md.ts @@ -32,6 +32,14 @@ import { dirname, join } from "node:path" import registry from "../src/lib/registry.json" import { docsNav } from "../src/app/docs/_config" +import { changelog } from "../src/lib/changelog" +import { + programs as sitePrograms, + affiliateScore, + commissionDisplay, + getCategoryStats, + getNetworkStats, +} from "../src/lib/programs" const BASE = "https://openaffiliate.dev" const OUT = join(process.cwd(), "public") @@ -322,6 +330,8 @@ function docsMd(): string { [ `- [Entry point](${BASE}/llms.txt)`, `- [All programs by category](${BASE}/programs.md)`, + `- [Rankings by Affiliate Score](${BASE}/rankings.md)`, + `- [Changelog](${BASE}/changelog.md)`, `- [Full dump](${BASE}/llms-full.txt)`, `- Any program, category or network page, with .md appended`, ].join("\n"), @@ -365,7 +375,7 @@ function llmsTxt(): string { `${verified.length} of ${programs.length} programs (${((verified.length / programs.length) * 100).toFixed(1)}%)`, `have been verified by OpenAffiliate. The rest are community-submitted and`, `unconfirmed: the commission rate, cookie window and payout terms come from`, - `the submitter and have not been checked against the program's own page.`, + `the submitter and have not been checked against the program page.`, ``, `Every program file states its own status and verification date at the top.`, `Prefer verified entries when accuracy matters, and cite the program's`, @@ -392,7 +402,7 @@ function llmsTxt(): string { "Primary pages", [ `- [Browse programs](${BASE}/programs): Search and filter the registry.`, - `- [Rankings](${BASE}/rankings): Compare programs, categories, and networks.`, + `- [Rankings](${BASE}/rankings.md): Compare programs, categories, and networks.`, `- [Compare](${BASE}/compare): Compare up to four programs side by side.`, `- [About and data policy](${BASE}/about): Publisher identity, verification policy, and contact.`, `- [Sitemap](${BASE}/sitemap.xml): Canonical index of HTML pages.`, @@ -470,6 +480,83 @@ function programsIndexMd(): string { ]) } +// ---------- Pages that were HTML-only --------------------------------------- + +function indexMd(): string { + return joinSections([ + `# OpenAffiliate`, + [ + `> The open registry of affiliate programs. ${programs.length} programs.`, + `> Built for developers and AI agents.`, + ].join("\n"), + section( + "Read this as an agent", + [ + `- [Entry point](${BASE}/llms.txt)`, + `- [Full registry](${BASE}/llms-full.txt)`, + `- [All programs](${BASE}/programs.md)`, + `- [Rankings](${BASE}/rankings.md)`, + `- [Changelog](${BASE}/changelog.md)`, + `- [One program](${BASE}/programs/vercel.md)`, + `- HTML home: ${BASE}/`, + ].join("\n"), + ), + ]) +} + +function rankingsMd(): string { + const ranked = [...sitePrograms].sort( + (a, b) => affiliateScore(b) - affiliateScore(a) || a.name.localeCompare(b.name), + ) + const programLines = ranked + .map((p, i) => { + const mark = p.verified ? "" : " (unverified)" + return `${i + 1}. [${p.name}](${BASE}/programs/${p.slug}.md) — score ${affiliateScore(p)}, ${commissionDisplay(p.commission)} ${p.commission.type}${mark}` + }) + .join("\n") + const networkLines = getNetworkStats() + .map(n => `- [${n.network}](${BASE}/networks/${networkToSlug(n.network)}.md) — ${n.programCount} programs, best ${n.bestCommissionDisplay}, top [${n.topProgram.name}](${BASE}/programs/${n.topProgram.slug}.md)`) + .join("\n") + const categoryLines = getCategoryStats() + .map(c => `- [${c.category}](${BASE}/categories/${categoryToSlug(c.category)}.md) — ${c.programCount} programs, best ${c.highestCommissionDisplay}, top [${c.topProgram.name}](${BASE}/programs/${c.topProgram.slug}.md)`) + .join("\n") + + return joinSections([ + `# Affiliate program rankings`, + [ + `> ${ranked.length} programs ordered by Affiliate Score, the same score as`, + `> the podium on ${BASE}/rankings and the homepage preview.`, + `> The interactive table can also sort by verified content, commission,`, + `> or cookie length. Those orders depend on live filters, so they are not`, + `> copied here.`, + ].join("\n"), + section("Programs", programLines), + section("Networks", networkLines), + section("Categories", categoryLines), + section("Links", `HTML: ${BASE}/rankings`), + ]) +} + +const CHANGELOG_TAG: Record = { + new: "New", + improved: "Improved", + fixed: "Fixed", +} + +function changelogMd(): string { + const entries = changelog.map(entry => { + const items = entry.items + .map(item => `- ${CHANGELOG_TAG[item.tag] ?? item.tag}: ${item.text}`) + .join("\n") + return `## ${entry.date} — ${entry.title}\n\n${items}` + }) + return joinSections([ + `# Changelog`, + `> What's new in OpenAffiliate. Same entries as ${BASE}/changelog.`, + entries.join("\n\n"), + ]) +} + // ---------- Write ----------------------------------------------------------- let written = 0 @@ -492,6 +579,9 @@ function main(): void { write("llms-full.txt", llmsFullTxt()) write("programs.md", programsIndexMd()) write("docs.md", docsMd()) + write("index.md", indexMd()) + write("rankings.md", rankingsMd()) + write("changelog.md", changelogMd()) for (const p of programs) { write(`programs/${p.slug}.md`, programMd(p)) diff --git a/scripts/verify-machine-surfaces.ts b/scripts/verify-machine-surfaces.ts index b8348d98..d9c83b3b 100644 --- a/scripts/verify-machine-surfaces.ts +++ b/scripts/verify-machine-surfaces.ts @@ -54,6 +54,22 @@ for (const program of programs) { fail(`missing Markdown twin for ${program.slug}`); } } +if (Buffer.byteLength(llms) > 10240) { + fail(`llms.txt is ${Buffer.byteLength(llms)} bytes; the cap is 10240`); +} +for (const file of ["index.md", "programs.md", "rankings.md", "changelog.md"]) { + const text = requireText(file); + if (!text.startsWith("# ")) fail(`${file} must start with an H1`); +} +if (!requireText("index.md").includes(`${BASE_URL}/llms.txt`)) { + fail("index.md does not point at llms.txt"); +} +if (!requireText("rankings.md").includes(`${BASE_URL}/programs/`)) { + fail("rankings.md does not link program pages"); +} +if (!requireText("changelog.md").includes("April 18, 2026")) { + fail("changelog.md is missing the latest entry"); +} const sitemapEntries = sitemap(); const sitemapUrls = sitemapEntries.map((entry) => entry.url); diff --git a/src/app/changelog/page.tsx b/src/app/changelog/page.tsx index 5c0930d3..a8d4a9be 100644 --- a/src/app/changelog/page.tsx +++ b/src/app/changelog/page.tsx @@ -1,20 +1,17 @@ import type { Metadata } from "next"; import Image from "next/image"; import Link from "next/link"; +import { changelog } from "@/lib/changelog"; export const metadata: Metadata = { title: "Changelog", description: "What's new in OpenAffiliate. New features, improvements, and fixes.", -}; - -type ChangelogEntry = { - date: string; - title: string; - image?: { src: string; alt: string }; - items: { - tag: "new" | "improved" | "fixed"; - text: string; - }[]; + alternates: { + canonical: "/changelog", + types: { + "text/markdown": "https://openaffiliate.dev/changelog.md", + }, + }, }; const TAG_STYLES: Record = { @@ -23,52 +20,6 @@ const TAG_STYLES: Record = fixed: { bg: "bg-amber-500/10", text: "text-amber-600 dark:text-amber-400", label: "Fixed" }, }; -const changelog: ChangelogEntry[] = [ - { - date: "April 18, 2026", - title: "Social Listen Data Pipeline + Registry Expansion", - image: { src: "/changelog/launch-program-detail.png", alt: "Program detail page with Social Listen showing YouTube, TikTok, Reddit, and Blog content" }, - items: [ - { tag: "new", text: "Social data persistence \u2014 all raw social items (YouTube, TikTok, X, Reddit, Blog) are now stored in Supabase for future content discovery and insights." }, - { tag: "new", text: "Weekly query rotation \u2014 3 query variants per platform, rotated weekly to discover different content over time instead of re-fetching the same results." }, - { tag: "new", text: "Added 190 new programs across 6 batches: schema hardening, PartnerStack delta (+96), AI-native wave 1 (+25), AI wave 2 (+25), YC AI companies (+20), marketplace scans (+24)." }, - { tag: "new", text: "SVG badge embed \u2014 embeddable Affiliate Score badge with size variants and web component support." }, - { tag: "improved", text: "Maximized data collection per API call \u2014 YouTube RapidAPI 6\u219250 results, Apify 20\u219230, removed slice limits on TikTok and X. Zero cost increase (per-request pricing)." }, - { tag: "improved", text: "Social Listen now uses PPR (Partial Prerendering) with dynamic holes \u2014 no more API calls during build, fixing the 10-minute build time regression." }, - { tag: "improved", text: "YouTube fetch priority swapped: RapidAPI (free) first, Apify ($0.03/call) as fallback. Saves ~$22/day on Apify costs." }, - { tag: "improved", text: "Affiliate Score algorithm redesigned (0\u2013100) with better commission parsing and tier-aware scoring." }, - { tag: "fixed", text: "Build time regression (10+ minutes) caused by API calls during static generation for all 446 programs." }, - { tag: "fixed", text: "98 missing logos from Batch 2 PartnerStack delta import." }, - { tag: "fixed", text: "Vercel deploy failures \u2014 inline index build, tolerate missing workspace packages, disable lint during build." }, - ], - }, - { - date: "April 17, 2026", - title: "Launch Day", - image: { src: "/changelog/launch-homepage.png", alt: "OpenAffiliate homepage with CLI demo, rankings preview, and featured programs" }, - items: [ - { tag: "new", text: "OpenAffiliate launched at openaffiliate.dev \u2014 the open registry of affiliate programs for developers and AI agents." }, - { tag: "new", text: "446 affiliate programs with structured YAML data: commission rates, cookie duration, payout details, agent instructions." }, - { tag: "new", text: "CLI tool \u2014 search and manage programs from your terminal with npx openaffiliate." }, - { tag: "new", text: "MCP server \u2014 connect AI agents to the registry via Model Context Protocol (HTTP + stdio)." }, - { tag: "new", text: "TypeScript SDK \u2014 typed client for searching and querying programs." }, - { tag: "new", text: "REST API \u2014 no auth required. Query programs, categories, and social data." }, - { tag: "new", text: "Social Listen \u2014 aggregated social content (YouTube, TikTok, X, Reddit, Blog) for each program with quality scoring and 7-day cache." }, - { tag: "new", text: "Affiliate Score \u2014 algorithmic scoring (0\u2013100) based on commission, cookie duration, payment terms, and brand strength." }, - { tag: "new", text: "Community voting on programs with Supabase-backed vote counts." }, - { tag: "new", text: "Multi-page docs site with sidebar navigation, table of contents, and code blocks." }, - { tag: "new", text: "Rankings page with sortable table, category filters, and search." }, - { tag: "new", text: "Category and network landing pages with SEO." }, - { tag: "new", text: "Program detail pages with ConnectTabs, CapabilityCards, and related programs." }, - { tag: "new", text: "HD logos via Clearbit with graceful fallback chain." }, - { tag: "new", text: "Cmd+K search bar for instant program discovery." }, - { tag: "improved", text: "Premium dark theme UI \u2014 enterprise-style design with neutral colors and tight spacing." }, - { tag: "fixed", text: "Deep-verified all programs \u2014 removed 14 fake/unverifiable entries, corrected 6 others." }, - { tag: "fixed", text: "Updated signup URLs to actual affiliate program pages for 299 programs." }, - ], - }, -]; - function TagBadge({ tag }: { tag: string }) { const style = TAG_STYLES[tag] ?? TAG_STYLES.new; return ( diff --git a/src/app/page.tsx b/src/app/page.tsx index e5ba9c29..18a04d55 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,3 +1,4 @@ +import type { Metadata } from "next"; import Link from "next/link"; import { TrackPageView } from "@/components/track-page-view"; import { @@ -14,6 +15,15 @@ import { ProgramLogo } from "@/components/program-logo"; import { programs, categories, parseCommissionRate, commissionLabel, affiliateScore, commissionDisplay} from "@/lib/programs"; import type { Program } from "@/lib/programs"; +export const metadata: Metadata = { + alternates: { + canonical: "/", + types: { + "text/markdown": "https://openaffiliate.dev/index.md", + }, + }, +}; + function RankingsPreview() { const top5 = [...programs] .sort((a, b) => affiliateScore(b) - affiliateScore(a)) diff --git a/src/app/programs/layout.tsx b/src/app/programs/layout.tsx index 48c03605..6bfd4168 100644 --- a/src/app/programs/layout.tsx +++ b/src/app/programs/layout.tsx @@ -19,6 +19,12 @@ export const metadata: Metadata = { // drops the root opengraph-image and this page shared with no preview. images: ["/opengraph-image"], }, + alternates: { + canonical: "/programs", + types: { + "text/markdown": "https://openaffiliate.dev/programs.md", + }, + }, }; export default function ProgramsLayout({ diff --git a/src/app/rankings/layout.tsx b/src/app/rankings/layout.tsx index 5f9155bd..73d311a9 100644 --- a/src/app/rankings/layout.tsx +++ b/src/app/rankings/layout.tsx @@ -13,6 +13,12 @@ export const metadata: Metadata = { url: "https://openaffiliate.dev/rankings", siteName: "OpenAffiliate", }, + alternates: { + canonical: "/rankings", + types: { + "text/markdown": "https://openaffiliate.dev/rankings.md", + }, + }, }; export default function RankingsLayout({ diff --git a/src/lib/changelog.ts b/src/lib/changelog.ts new file mode 100644 index 00000000..7d078d78 --- /dev/null +++ b/src/lib/changelog.ts @@ -0,0 +1,55 @@ +export type ChangelogEntry = { + date: string + title: string + image?: { src: string; alt: string } + items: { + tag: "new" | "improved" | "fixed" + text: string + }[] +} + +export const changelog: ChangelogEntry[] = [ + { + date: "April 18, 2026", + title: "Social Listen Data Pipeline + Registry Expansion", + image: { src: "/changelog/launch-program-detail.png", alt: "Program detail page with Social Listen showing YouTube, TikTok, Reddit, and Blog content" }, + items: [ + { tag: "new", text: "Social data persistence \u2014 all raw social items (YouTube, TikTok, X, Reddit, Blog) are now stored in Supabase for future content discovery and insights." }, + { tag: "new", text: "Weekly query rotation \u2014 3 query variants per platform, rotated weekly to discover different content over time instead of re-fetching the same results." }, + { tag: "new", text: "Added 190 new programs across 6 batches: schema hardening, PartnerStack delta (+96), AI-native wave 1 (+25), AI wave 2 (+25), YC AI companies (+20), marketplace scans (+24)." }, + { tag: "new", text: "SVG badge embed \u2014 embeddable Affiliate Score badge with size variants and web component support." }, + { tag: "improved", text: "Maximized data collection per API call \u2014 YouTube RapidAPI 6\u219250 results, Apify 20\u219230, removed slice limits on TikTok and X. Zero cost increase (per-request pricing)." }, + { tag: "improved", text: "Social Listen now uses PPR (Partial Prerendering) with dynamic holes \u2014 no more API calls during build, fixing the 10-minute build time regression." }, + { tag: "improved", text: "YouTube fetch priority swapped: RapidAPI (free) first, Apify ($0.03/call) as fallback. Saves ~$22/day on Apify costs." }, + { tag: "improved", text: "Affiliate Score algorithm redesigned (0\u2013100) with better commission parsing and tier-aware scoring." }, + { tag: "fixed", text: "Build time regression (10+ minutes) caused by API calls during static generation for all 446 programs." }, + { tag: "fixed", text: "98 missing logos from Batch 2 PartnerStack delta import." }, + { tag: "fixed", text: "Vercel deploy failures \u2014 inline index build, tolerate missing workspace packages, disable lint during build." }, + ], + }, + { + date: "April 17, 2026", + title: "Launch Day", + image: { src: "/changelog/launch-homepage.png", alt: "OpenAffiliate homepage with CLI demo, rankings preview, and featured programs" }, + items: [ + { tag: "new", text: "OpenAffiliate launched at openaffiliate.dev \u2014 the open registry of affiliate programs for developers and AI agents." }, + { tag: "new", text: "446 affiliate programs with structured YAML data: commission rates, cookie duration, payout details, agent instructions." }, + { tag: "new", text: "CLI tool \u2014 search and manage programs from your terminal with npx openaffiliate." }, + { tag: "new", text: "MCP server \u2014 connect AI agents to the registry via Model Context Protocol (HTTP + stdio)." }, + { tag: "new", text: "TypeScript SDK \u2014 typed client for searching and querying programs." }, + { tag: "new", text: "REST API \u2014 no auth required. Query programs, categories, and social data." }, + { tag: "new", text: "Social Listen \u2014 aggregated social content (YouTube, TikTok, X, Reddit, Blog) for each program with quality scoring and 7-day cache." }, + { tag: "new", text: "Affiliate Score \u2014 algorithmic scoring (0\u2013100) based on commission, cookie duration, payment terms, and brand strength." }, + { tag: "new", text: "Community voting on programs with Supabase-backed vote counts." }, + { tag: "new", text: "Multi-page docs site with sidebar navigation, table of contents, and code blocks." }, + { tag: "new", text: "Rankings page with sortable table, category filters, and search." }, + { tag: "new", text: "Category and network landing pages with SEO." }, + { tag: "new", text: "Program detail pages with ConnectTabs, CapabilityCards, and related programs." }, + { tag: "new", text: "HD logos via Clearbit with graceful fallback chain." }, + { tag: "new", text: "Cmd+K search bar for instant program discovery." }, + { tag: "improved", text: "Premium dark theme UI \u2014 enterprise-style design with neutral colors and tight spacing." }, + { tag: "fixed", text: "Deep-verified all programs \u2014 removed 14 fake/unverifiable entries, corrected 6 others." }, + { tag: "fixed", text: "Updated signup URLs to actual affiliate program pages for 299 programs." }, + ], + }, +]