{"id":51162142,"url":"https://github.com/dodopayments/dualmark","last_synced_at":"2026-06-28T08:03:15.153Z","repository":{"id":357360501,"uuid":"1229555898","full_name":"dodopayments/dualmark","owner":"dodopayments","description":"Open-source AEO (Answer Engine Optimization) infrastructure — every page, dual-marked. Markdown twins for AI agents alongside HTML for humans, picked by HTTP content negotiation.","archived":false,"fork":false,"pushed_at":"2026-06-17T18:29:33.000Z","size":1569,"stargazers_count":88,"open_issues_count":16,"forks_count":27,"subscribers_count":1,"default_branch":"main","last_synced_at":"2026-06-17T20:23:34.842Z","etag":null,"topics":["aeo","ai-bots","ai-search","ai-seo","answer-engine-optimization","astro-integration","cloudflare-workers","content-negotiation","generative-engine-optimization","geo","llms-txt","markdown","nextjs","open-source","typescript"],"latest_commit_sha":null,"homepage":"https://dualmark.dev","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/dodopayments.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":"CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":".github/CODEOWNERS","security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":"NOTICE","maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-05-05T06:55:19.000Z","updated_at":"2026-06-17T18:39:09.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/dodopayments/dualmark","commit_stats":null,"previous_names":["dodopayments/dualmark"],"tags_count":11,"template":false,"template_full_name":null,"purl":"pkg:github/dodopayments/dualmark","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dodopayments%2Fdualmark","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dodopayments%2Fdualmark/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dodopayments%2Fdualmark/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dodopayments%2Fdualmark/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/dodopayments","download_url":"https://codeload.github.com/dodopayments/dualmark/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dodopayments%2Fdualmark/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34881384,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-28T02:00:05.809Z","response_time":54,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["aeo","ai-bots","ai-search","ai-seo","answer-engine-optimization","astro-integration","cloudflare-workers","content-negotiation","generative-engine-optimization","geo","llms-txt","markdown","nextjs","open-source","typescript"],"created_at":"2026-06-26T15:00:26.467Z","updated_at":"2026-06-28T08:03:15.148Z","avatar_url":"https://github.com/dodopayments.png","language":"TypeScript","funding_links":[],"categories":["技术实现"],"sub_categories":["内容协商（向 AI 返回 Markdown）"],"readme":"# Dualmark\n\n\u003cp align=\"left\"\u003e\n  \u003ca href=\"https://www.npmjs.com/package/@dualmark/core\"\u003e\n    \u003cimg src=\"https://img.shields.io/npm/v/@dualmark/core?label=npm\u0026color=blue\" alt=\"npm version\" /\u003e\n  \u003c/a\u003e\n  \u003ca href=\"https://github.com/dodopayments/dualmark/blob/main/LICENSE\"\u003e\n    \u003cimg src=\"https://img.shields.io/badge/license-Apache%202.0-green\" alt=\"License\" /\u003e\n  \u003c/a\u003e\n  \u003ca href=\"https://www.npmjs.com/package/@dualmark/core\"\u003e\n    \u003cimg src=\"https://img.shields.io/badge/npm-provenance-blueviolet?logo=npm\" alt=\"npm provenance\" /\u003e\n  \u003c/a\u003e\n  \u003ca href=\"https://discord.gg/bYqAp4ayYh\"\u003e\n    \u003cimg src=\"https://img.shields.io/discord/1305511580854779984?label=Join%20Discord\u0026logo=discord\" alt=\"Join Discord\" /\u003e\n  \u003c/a\u003e\n\u003c/p\u003e\n\n\u003e The AEO infrastructure your marketing site is missing.\n\nYour blog ranks #1 on Google. ChatGPT cites your competitor.\n\nThat's not a content problem. It's an **infrastructure problem**. AI search engines (ChatGPT, Claude, Perplexity, Gemini, Google AI Overviews) read the web differently from humans — they want clean markdown without nav chrome, JavaScript, or cookie banners. Most marketing sites give them HTML soup and wonder why they get ignored.\n\n**Dualmark gives every page a markdown twin.** Same URL. Two formats. Picked by HTTP content negotiation. Drop it into your Astro/Next.js/SvelteKit/Cloudflare stack in 30 seconds. Score it with `dualmark verify`.\n\n```diff\n- npm install @next-seo/some-meta-tag-thing\n+ bun add @dualmark/astro\n```\n\n[Quickstart](#quickstart) · [Why](#why-marketing-teams-need-this) · [Examples](./examples) · [Spec](./spec) · [Docs](https://dualmark.dev)\n\n---\n\n## Quickstart\n\n### Astro (30 seconds)\n\n```bash\nbun add @dualmark/astro\n```\n\n```ts\n// astro.config.mjs\nimport { defineConfig } from \"astro/config\";\nimport dualmark from \"@dualmark/astro\";\n\nexport default defineConfig({\n  site: \"https://yourcompany.com\",\n  integrations: [\n    dualmark({\n      siteUrl: \"https://yourcompany.com\",\n      collections: {\n        blog: { converter: \"blog\" }, // /blog/*.md auto-generated\n        glossary: { converter: \"glossary\" }, // /glossary/*.md auto-generated\n      },\n      llmsTxt: { enabled: true }, // /llms.txt auto-generated\n    }),\n  ],\n});\n```\n\n```bash\nbun run build \u0026\u0026 bunx dualmark verify https://localhost:4321/blog/your-post\n# → Score 80/80 ✓\n```\n\nThat's it. Every blog post has a markdown twin at `/blog/\u003cslug\u003e.md`. `llms.txt` is generated. Every HTML response advertises its twin via `Link: \u003c…\u003e; rel=\"alternate\"; type=\"text/markdown\"`. ChatGPT crawler sees clean markdown. Your existing pages don't change.\n\n### Next.js App Router (60 seconds)\n\n```bash\nbun add @dualmark/nextjs\n```\n\n```ts\n// proxy.ts (or middleware.ts on Next ≤15)\nimport { createDualmarkMiddleware } from \"@dualmark/nextjs\";\n\nexport default createDualmarkMiddleware({ siteUrl: \"https://yourcompany.com\" });\n\nexport const config = {\n  matcher: [\n    {\n      source: \"/((?!_next/|favicon.ico|md/).*)\",\n      missing: [{ type: \"header\", key: \"next-router-prefetch\" }],\n    },\n  ],\n};\n```\n\n```ts\n// app/md/[...path]/route.ts\nimport { createDualmarkRouteHandler } from \"@dualmark/nextjs\";\nimport { POSTS } from \"@/lib/posts\";\n\nconst handler = createDualmarkRouteHandler({\n  siteUrl: \"https://yourcompany.com\",\n  collections: {\n    blog: { converter: \"blog\", getEntries: () =\u003e POSTS.map(toEntry) },\n  },\n});\n\nexport const dynamic = \"force-static\";\nexport const GET = handler.GET;\nexport const generateStaticParams = handler.generateStaticParams;\n```\n\nThat's it. Bot UAs get markdown, browsers get HTML with a `Link rel=\"alternate\"` header, direct `.md` URLs serve markdown. Full example with `next dev` → 120/125 conformance score:\n\n[Full Next.js example →](./examples/nextjs-app-router)\n\n### SvelteKit (60 seconds)\n\n```bash\nbun add @dualmark/sveltekit\n```\n\n```ts\n// vite.config.ts\nimport { sveltekit } from \"@sveltejs/kit/vite\";\nimport { defineConfig } from \"vite\";\nimport dualmark from \"@dualmark/sveltekit\";\nimport dualmarkConfig from \"./src/dualmark.config\";\n\nexport default defineConfig({\n  plugins: [dualmark(dualmarkConfig), sveltekit()],\n});\n```\n\n```ts\n// src/hooks.server.ts\nimport { createDualmarkHandle } from \"@dualmark/sveltekit\";\nimport dualmarkConfig from \"./dualmark.config\";\n\nexport const handle = createDualmarkHandle(dualmarkConfig);\n```\n\nFull example with `vite dev` → 125/125 conformance score:\n\n[Full SvelteKit example →](./examples/sveltekit-blog)\n\n### Cloudflare Workers (60 seconds)\n\nWrap your existing Worker. AI bots get markdown at the edge — single-digit-ms first-byte from 300+ cities.\n\n```ts\nimport { createAEOWorker } from \"@dualmark/cloudflare\";\nimport upstream from \"./your-existing-worker.js\";\n\nexport default createAEOWorker({\n  upstream,\n  trailingSlash: \"never\",\n  analytics: { binding: \"AI_AGENT_ANALYTICS\" },\n});\n```\n\n[Full example with `wrangler dev` → 125/125 conformance score →](./examples/astro-cloudflare-full)\n\n### Deno Deploy (60 seconds)\n\nWrap any Deno fetch handler — a static-file reader or an existing app.\n\n```ts\n// main.ts\nimport { createAEOHandler } from \"@dualmark/deno\";\nimport myApp from \"./app.ts\";\n\nDeno.serve(createAEOHandler({ upstream: myApp.fetch }));\n```\n\n[Full example with `deno run` → 125/125 conformance score →](./examples/deno-deploy)\n\n### Vercel Edge (60 seconds)\n\nDrop a middleware that serves your pre-built `.md` twins to AI bots at the edge.\n\n```ts\n// proxy.ts\nimport { NextResponse } from \"next/server\";\nimport { createAEOMiddleware } from \"@dualmark/vercel\";\n\nexport default createAEOMiddleware({\n  upstream: async () =\u003e NextResponse.next(),\n  fetchAsset: async (url, init) =\u003e fetch(url.toString(), init),\n});\n\nexport const config = { matcher: [\"/((?!_next/|favicon.ico).*)\"] };\n```\n\n[Full Next.js + Vercel example → 120/125 →](./examples/vercel-edge-full)\n\n---\n\n## Why marketing teams need this\n\nYou already invested in SEO. Now invest in AEO — for **a fraction of the effort**.\n\n| Problem | Without Dualmark | With Dualmark |\n|---|---|---|\n| **AI cites competitors instead of you** | Bots scrape your HTML, get nav menus + JS errors, pick the cleaner source | Same URL serves clean markdown to bots, polished HTML to humans |\n| **No way to know if you're discoverable** | \"We hope ChatGPT can read this\" | `dualmark verify` returns a 0–125 score with line-item failures |\n| **`llms.txt` proposal keeps changing** | Hand-maintained, drifts from sitemap | Auto-generated from the same config that drives your routes |\n| **Every team rebuilds this** | Custom middleware in every repo, none of them quite right | One battle-tested package, conforms to a public spec |\n| **No analytics for AI traffic** | \"Was that a bot or a human?\" | `onAIRequest` hook + Cloudflare Analytics Engine integration: bot name, vendor, page, tokens, country |\n| **Slow to roll out across pages** | Marketing waits weeks for engineering | Add `converter: \"compare\"` to a collection — done. Production-tested converters bundled. |\n\n**Built and battle-tested at [Dodo Payments](https://dodopayments.com)** for our own marketing site. Now extracted as OSS so you don't have to write the same content negotiation, bot detection, and edge wrapping over and over.\n\n---\n\n## What you actually ship\n\n```\nyourcompany.com/pricing             ← human visitors get this\nyourcompany.com/pricing.md          ← AI agents get this\nyourcompany.com/llms.txt            ← AI agents discover everything\n```\n\nSame URL. Same content. Different rendering. Picked automatically by:\n\n- `Accept: text/markdown` header → markdown\n- Known AI bot User-Agent (GPTBot, ClaudeBot, PerplexityBot, +21 more) → markdown\n- Direct `.md` URL → markdown\n- Anything else → HTML, with `Link rel=\"alternate\"` pointing to the twin\n\nNo duplicate content penalties (markdown twin sets `X-Robots-Tag: noindex`). No JS framework rewrites. No content team retraining. **Your existing pages stay the same.**\n\n---\n\n## Built-in converters (`@dualmark/converters`)\n\nDrop-in markdown generation for the page types every marketing site has:\n\n| Converter | What it's for | Marketing examples |\n|---|---|---|\n| `blog` | Long-form posts | Engineering blog, customer stories |\n| `case-study` | Customer wins | Logos with stats and pull-quote |\n| `changelog` | Release notes | \"What's new in v1.4\" with grouped changes |\n| `compare` | Us vs. competitor | \"Stripe alternative\" pages |\n| `docs` | Documentation | Getting started, API guides |\n| `feature` | Product/feature pages | \"Webhooks\", \"SSO\" — problem/solution + FAQ |\n| `glossary` | Term definitions | \"What is a payment gateway?\" |\n| `integration` | App marketplace / integrations | \"Connect Stripe to Acme\", Slack connector pages |\n| `legal` | Policy pages | Terms, Privacy, DPA |\n| `pricing` | Pricing tables | Tier comparison with CTAs |\n| `pseo` | Programmatic SEO | \"SEO services in San Francisco\" with facts + cross-links |\n| `status-page` | Uptime / status | Public component health + incidents |\n| `tool` | Standalone calculators | \"Currency converter\" |\n| `video` | Video landing pages | Webinar replays |\n\nEach converter takes your collection entry → returns clean markdown with the right structure for AI consumption (title, description, breadcrumbs, FAQ extraction, related links). No prompt engineering required.\n\n```ts\nimport { compareConverter } from \"@dualmark/converters\";\n\nconst convert = compareConverter({\n  siteUrl: \"https://yourcompany.com\",\n  basePath: \"/compare\",\n});\n\nconst md = convert(yourComparePage); // → battle-tested markdown layout\n```\n\n---\n\n## Verify any site against the spec\n\n```bash\nbunx @dualmark/cli verify https://yourcompany.com/pricing\n```\n\n```\nDualmark Conformance Report\nURL:         https://yourcompany.com/pricing\nMarkdown:    https://yourcompany.com/pricing.md\nScore:       125/125\nDuration:    107ms\n\nPassed:\n  [+20] md.fetch         — Markdown twin URL is reachable\n  [+10] md.contentType   — Content-Type is text/markdown; charset=utf-8\n  [+10] md.tokensHeader  — X-Markdown-Tokens header is present\n  [+10] md.noindex       — X-Robots-Tag includes noindex\n  [+10] md.vary          — Vary header includes Accept\n  [+10] md.body          — Body is non-empty markdown\n  [+10] html.linkAlternate — HTML response advertises markdown twin\n  [+10] negotiation.botUa — GPTBot UA receives text/markdown\n  [+10] negotiation.acceptHeader — Accept: text/markdown receives text/markdown\n  ...\n```\n\nThree conformance levels — **Basic** (60%), **Standard** (80%), **Advanced** (95%). Drop the score in your CI to prevent regressions.\n\n```yaml\n# .github/workflows/ci.yml\n- run: bunx @dualmark/cli verify https://staging.yourcompany.com/pricing\n  # exits non-zero if any required check fails\n```\n\n---\n\n## What's in the box\n\n| Package | npm | Size | What it does |\n|---|---|---|---|\n| [`@dualmark/core`](./packages/core) | `npm i @dualmark/core` | 14 KB | Framework-agnostic primitives: content negotiation (RFC 7231), AI-bot detection (24 known bots), markdown response builder, token estimation, composition helpers, `llms.txt` rendering. Zero runtime deps. |\n| [`@dualmark/converters`](./packages/converters) | `npm i @dualmark/converters` | 16 KB | Production-tested converter factories. |\n| [`@dualmark/astro`](./packages/astro) | `npm i @dualmark/astro` | 22 KB | Astro 5 integration. Auto-generates `.md` endpoints, ships middleware, generates `llms.txt`. |\n| [`@dualmark/nextjs`](./packages/nextjs) | `npm i @dualmark/nextjs` | 15 KB | Next.js App Router adapter. `withDualmark()`, `createDualmarkMiddleware()`, `createDualmarkRouteHandler()`, `createLlmsTxtHandler()`. |\n| [`@dualmark/sveltekit`](./packages/sveltekit) | `npm i @dualmark/sveltekit` | 19 KB | SvelteKit adapter. Vite route generator, `createDualmarkHandle()`, generated `.md` endpoints, and `llms.txt`. |\n| [`@dualmark/cloudflare`](./packages/cloudflare) | `npm i @dualmark/cloudflare` | 9 KB | Workers edge adapter. Wraps any upstream Worker. Hooks for analytics + telemetry. |\n| [`@dualmark/fastly`](./packages/fastly) | `npm i @dualmark/fastly` | 9 KB | Fastly Compute edge adapter. Natively proxies to Fastly backends with background hooks. |\n| [`@dualmark/vercel`](./packages/vercel) | `npm i @dualmark/vercel` | 9 KB | Vercel Edge Middleware adapter. `createAEOMiddleware()` wraps any upstream handler; serves pre-built `.md` with lifecycle hooks. |\n| [`@dualmark/deno`](./packages/deno) | `npm i @dualmark/deno` | 8 KB | Deno Deploy edge adapter. Wraps any Deno fetch handler. Lifecycle hooks scheduled on `info.completed`. |\n| [`@dualmark/cli`](./packages/cli) | `npm i -g @dualmark/cli` | 16 KB | `dualmark verify \u003curl\u003e`. Programmatic API too. |\n\nPlus:\n\n- [**`spec/`**](./spec) — the **AEO Specification v1.0**. Public, framework-agnostic, RFC-2119-compliant. Implement it in Go, Rust, PHP, Ruby — your call.\n- [**`apps/docs/`**](./apps/docs) — Fumadocs site at [dualmark.dev](https://dualmark.dev)\n- [**`apps/docs/app/play`**](./apps/docs/app/play) — interactive Accept-header + UA tester. Live at [dualmark.dev/play](https://dualmark.dev/play).\n- [**`examples/`**](./examples) — seven end-to-end working examples (Astro, Astro+Cloudflare, Sanity+Astro, Next.js, SvelteKit, Deno, Vercel).\n\n---\n\n## End-to-end verified\n\n| Surface | Status |\n|---|---|\n| `@dualmark/core` | 174 tests pass (vitest + fast-check property tests) |\n| `@dualmark/converters` | 31 tests pass |\n| `@dualmark/cloudflare` | 23 tests pass |\n| `@dualmark/fastly` | 19 tests pass |\n| `@dualmark/deno` | 23 tests pass |\n| `@dualmark/cli` | 25 tests pass |\n| `@dualmark/astro` | 39 tests pass |\n| `@dualmark/nextjs` | 47 tests pass |\n| `@dualmark/sveltekit` | 17 tests pass |\n| `@dualmark/vercel` | 28 tests pass |\n| `examples/astro-blog` | **80/80** under `astro dev` (`--skip-negotiation`) |\n| `examples/sanity-astro` | **80/80** under `astro dev` (`--skip-negotiation`) for fixture-backed blog + glossary docs |\n| `examples/astro-cloudflare-full` | **125/125 perfect** under `wrangler dev` (full negotiation) |\n| `examples/nextjs-app-router` | **120/125** under `next dev` (now using `@dualmark/nextjs`) |\n| `examples/sveltekit-blog` | **125/125 perfect** under `vite dev` (full negotiation) |\n| `examples/deno-deploy` | **125/125 perfect** under `deno run` (full negotiation) |\n| `examples/fastly-compute` | **125/125 perfect** under `fastly compute serve` (full negotiation) |\n| `examples/vercel-edge-full` | **120/125** under `next dev` (`@dualmark/vercel`) |\n| `apps/docs` | 26 routes prerendered, all serve 200 |\n| `/play` route | Live at dualmark.dev/play, integrated into the docs app |\n\n```bash\nbun install\nbun run build \u0026\u0026 bun run test \u0026\u0026 bun run typecheck   # 407 tests across 9 packages\n```\n\n---\n\n## Where it goes from here\n\nWe're building toward Dualmark being **the** AEO infrastructure for marketing sites — the same way Tailwind became the default for marketing CSS or Vercel for marketing hosting. The roadmap:\n\n- **More framework adapters**: Remix/React Router, Nuxt\n- **More edge adapters**: Netlify\n- **More converters**: pricing tables, changelog, docs/API reference, status pages, integrations\n- **AEO Analytics**: a hosted dashboard on top of the `onAIRequest` hook, so marketing can see which bot reads which page, when\n- **Spec evolution toward AEO 1.1+** with structured data hints, per-section markdown anchors, and sitemap.md\n- **CMS integrations**: Sanity, Contentful, Builder.io plugins so non-engineers can author dual-marked content\n\nIf you're a marketing engineer reading this and any of those would land in your stack, [open an issue](https://github.com/dodopayments/dualmark/issues) or [+1 an existing one](https://github.com/dodopayments/dualmark/issues).\n\n---\n\n## Contributing\n\nWe're early. Issues, PRs, and \"I tried it on $framework and it broke\" reports are all welcome.\n\n- Read [CONTRIBUTING.md](./CONTRIBUTING.md) for the dev loop and release flow.\n- The AEO Spec is authoritative — if you implement it elsewhere (in any language), we want to link to your implementation.\n\n```bash\nbun install\nbun run build   # turbo-orchestrated build\nbun run test    # vitest across all packages\nbun run typecheck\n```\n\n## License\n\nApache 2.0 — see [LICENSE](./LICENSE) and [NOTICE](./NOTICE). Includes a patent grant. Use it for anything; attribution appreciated, never required.\n\n## Status\n\n**Pre-1.0.** APIs may change in patch releases until 1.0. The AEO Spec v1.0 is authoritative; framework code follows. Production-ready for early adopters; we're [running it on dodopayments.com](https://dodopayments.com).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdodopayments%2Fdualmark","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdodopayments%2Fdualmark","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdodopayments%2Fdualmark/lists"}