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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -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/
Expand Down
94 changes: 92 additions & 2 deletions scripts/generate-md.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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"),
Expand Down Expand Up @@ -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`,
Expand All @@ -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.`,
Expand Down Expand Up @@ -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<string, string> = {
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
Expand All @@ -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))
Expand Down
16 changes: 16 additions & 0 deletions scripts/verify-machine-surfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
63 changes: 7 additions & 56 deletions src/app/changelog/page.tsx
Original file line number Diff line number Diff line change
@@ -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<string, { bg: string; text: string; label: string }> = {
Expand All @@ -23,52 +20,6 @@ const TAG_STYLES: Record<string, { bg: string; text: string; label: string }> =
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 (
Expand Down
10 changes: 10 additions & 0 deletions src/app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { Metadata } from "next";
import Link from "next/link";
import { TrackPageView } from "@/components/track-page-view";
import {
Expand All @@ -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))
Expand Down
6 changes: 6 additions & 0 deletions src/app/programs/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down
6 changes: 6 additions & 0 deletions src/app/rankings/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down
55 changes: 55 additions & 0 deletions src/lib/changelog.ts
Original file line number Diff line number Diff line change
@@ -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." },
],
},
]
Loading