Cloudflare
Cloudflare is the clearest first platform for OpenNav: Pages already deploys a finished folder, Workers can serve static assets from a configured directory, and AI Gateway teams are already thinking about agent cost, routing, and observability.
OpenNav ships both the static agent-readable layer and runtime content
negotiation via Cloudflare Pages Functions. Start by publishing the static files
agents can discover, then add a Pages Function for on-the-fly HTML-to-Markdown
conversion when the client sends Accept: text/markdown.
Cloudflare Pages is the first platform with a built-in OpenNav platform
default. Passing platform: "cloudflare-pages" in the SDK, Astro, or Next, or
--platform cloudflare-pages in the CLI, creates the Pages _headers artifact
by default. Additional platform defaults are planned as OpenNav adds first-class
support for more static hosts.
Install
Section titled “Install”Install OpenNav in the project that owns the Cloudflare build command.
npm install @opennav-ai/opennavpnpm add @opennav-ai/opennavyarn add @opennav-ai/opennavbun add @opennav-ai/opennavChoose Your Cloudflare Path
Section titled “Choose Your Cloudflare Path”| Cloudflare path | OpenNav setup |
|---|---|
| Pages with any static output folder | Run the CLI after your normal build. |
| Pages with Astro | Use OpenNavAstro after astro build. |
| Pages with Next.js static export | Use OpenNavNext with output: "export". |
| Pages with a custom build script or monorepo | Use the TypeScript SDK from the script that knows the output folder. |
| Workers static assets | Run OpenNav before wrangler deploy uploads the configured assets directory. |
| AI Gateway or agent experiments | Fetch the generated OpenNav files from Pages or Workers assets; AI Gateway remains the AI app proxy and observability layer. |
Cloudflare Pages With The CLI
Section titled “Cloudflare Pages With The CLI”Cloudflare Pages lets you configure a build command and an output directory. Use the same output directory for both OpenNav and the Pages publish directory.
{ "scripts": { "build": "astro build && opennav build --static --output dist --site-url https://example.com --site-name \"Example Docs\" --platform cloudflare-pages" }}In Cloudflare Pages:
| Pages setting | Value |
|---|---|
| Build command | npm run build |
| Build output directory | dist |
Use the deployed production URL for --site-url, including the protocol and
host. In monorepos, make --output relative to the project root that Pages uses
for the build command.
Run a dry run locally when you want to preview the file plan before changing the build command:
opennav build --static \ --output dist \ --site-url https://example.com \ --site-name "Example Docs" \ --platform cloudflare-pages \ --dry-run--platform cloudflare-pages is enough to create or update _headers; the
older explicit --static-headers flag is not required for Cloudflare Pages.
Astro On Cloudflare Pages
Section titled “Astro On Cloudflare Pages”Astro static builds usually publish dist. Add OpenNavAstro to the Astro
config, then let Pages run the normal Astro build.
import { defineConfig } from "astro/config";import { OpenNavAstro } from "@opennav-ai/opennav/astro";
export default defineConfig({ site: "https://example.com", integrations: [ OpenNavAstro({ siteName: "Example Docs", mode: "static", platform: "cloudflare-pages", }), ],});In Cloudflare Pages:
| Pages setting | Value |
|---|---|
| Build command | npm run build |
| Build output directory | dist |
Use this path for Astro sites that produce static HTML. For server-rendered Astro routes, add a Cloudflare Pages Function alongside your static build.
To keep the Cloudflare platform setting without writing _headers, opt out in
the integration:
OpenNavAstro({ siteName: "Example Docs", mode: "static", platform: "cloudflare-pages", staticHeaders: { enabled: false, },});Next.js Static Export On Cloudflare Pages
Section titled “Next.js Static Export On Cloudflare Pages”OpenNav supports Next.js static export builds. Configure Next with
output: "export" and publish the generated out folder.
import { OpenNavNext } from "@opennav-ai/opennav/next";import type { NextConfig } from "next";
const nextConfig: NextConfig = { output: "export",};
export default OpenNavNext({ siteName: "Example Docs", siteUrl: "https://example.com", mode: "static", platform: "cloudflare-pages",})(nextConfig);In Cloudflare Pages:
| Pages setting | Value |
|---|---|
| Build command | npm run build |
| Build output directory | out |
Use this path for exported static routes. For server-rendered Next.js routes, use OpenNavServer in a Route Handler or add a Cloudflare Pages Function.
To keep the Cloudflare platform setting without writing _headers, opt out in
the config wrapper:
OpenNavNext({ siteName: "Example Docs", siteUrl: "https://example.com", mode: "static", platform: "cloudflare-pages", staticHeaders: { enabled: false, },})(nextConfig);Custom SDK Or Monorepo Builds
Section titled “Custom SDK Or Monorepo Builds”Use the root SDK when a custom script already knows which folder Cloudflare will publish.
import { OpenNavStaticSite } from "@opennav-ai/opennav";
const result = await new OpenNavStaticSite({ siteName: "Example Docs", siteUrl: "https://example.com", outputDirectory: "dist", platform: "cloudflare-pages",}).build();
if (result.isErr()) { console.error(result.error.message); process.exit(1);}Then call that script after the framework build and before Pages uploads the output folder.
{ "scripts": { "build": "npm run build:site && node scripts/opennav.mjs" }}Set staticHeaders: { enabled: false } in the SDK options when you want to keep
platform: "cloudflare-pages" for future Cloudflare-specific behavior but do
not want OpenNav to create or edit _headers.
Cloudflare Pages Headers
Section titled “Cloudflare Pages Headers”Cloudflare Pages can read a _headers file from the deployed output directory
and attach response headers to matching routes. When
platform: "cloudflare-pages" is configured, OpenNav creates or updates an
OpenNav-managed block in that file by default.
| Entry point | Cloudflare Pages header behavior |
|---|---|
CLI with --platform cloudflare-pages | Creates or updates _headers by default. |
OpenNavStaticSite({ platform: "cloudflare-pages" }) | Creates or updates _headers by default. |
OpenNavAstro({ platform: "cloudflare-pages" }) | Creates or updates dist/_headers by default. |
OpenNavNext({ platform: "cloudflare-pages" }) | Creates or updates _headers in the static export folder by default. |
SDK, Astro, or Next with staticHeaders: { enabled: false } | Does not create or edit _headers. |
The default is enabled only for currently supported platforms that have a known static header file format. Today that is Cloudflare Pages. More platform support is planned.
dist/ _headers llms.txt llms-full.txt .well-known/opennav.json index.mdThe generated _headers block sets:
| Route pattern | Response header behavior |
|---|---|
/*.md | Serves generated Markdown mirrors as text/markdown; charset=utf-8. |
/llms.txt, /llms-full.txt, and .well-known copies | Serves generated indexes as text/plain; charset=utf-8. |
/.well-known/opennav.json | Serves the compatibility manifest as application/json; charset=utf-8. |
HTML page routes such as /, /docs/page, or /docs/page/ | Adds HTTP Link headers pointing to that page’s generated Markdown alternate and the root llms.txt index. |
OpenNav also sets X-Content-Type-Options: nosniff for those generated
artifacts. In a request flow, an agent can fetch https://example.com/llms.txt
or https://example.com/docs/page/index.md, and Cloudflare Pages will return
the file with a concrete content type instead of relying on generic static-file
detection.
For HTML responses, the generated page route block looks like this:
/docs/page Link: <https://example.com/docs/page.md>; rel="alternate"; type="text/markdown" Link: <https://example.com/llms.txt>; rel="index"; type="text/plain"For a small site with a home page and one docs page, the complete generated
dist/_headers file can look like this:
# Begin OpenNav AI# opennav compatible="true" version="1.0" profile="static-agent-ready" build-fingerprint="sha256:123456789abc" manifest="/.well-known/opennav.json"/llms.txt Content-Type: text/plain; charset=utf-8 X-Content-Type-Options: nosniff
/llms-full.txt Content-Type: text/plain; charset=utf-8 X-Content-Type-Options: nosniff
/.well-known/llms.txt Content-Type: text/plain; charset=utf-8 X-Content-Type-Options: nosniff
/.well-known/llms-full.txt Content-Type: text/plain; charset=utf-8 X-Content-Type-Options: nosniff
/.well-known/opennav.json Content-Type: application/json; charset=utf-8 X-Content-Type-Options: nosniff
/*.md Content-Type: text/markdown; charset=utf-8 X-Content-Type-Options: nosniff
/ Link: <https://example.com/index.md>; rel="alternate"; type="text/markdown" Link: <https://example.com/llms.txt>; rel="index"; type="text/plain"
/docs/page Link: <https://example.com/docs/page.md>; rel="alternate"; type="text/markdown" Link: <https://example.com/llms.txt>; rel="index"; type="text/plain"# End OpenNav AIThis gives agents a response-header discovery path for the same Markdown
alternate and site index that OpenNav already advertises inside the HTML
<head>. For runtime Accept: text/markdown content negotiation, add a
Cloudflare Pages Function.
OpenNav preserves caller-owned _headers rules and replaces only the block
between # Begin OpenNav AI and # End OpenNav AI. If an existing caller-owned
route overlaps OpenNav’s generated header routes, OpenNav leaves _headers
untouched and reports a warning so the site owner can resolve the conflict
intentionally. This applies to any header on the overlapping route, not only
Content-Type.
Cloudflare documents the file format, comment behavior, and route/header limits in Pages Headers.
Cloudflare Pages Functions
Section titled “Cloudflare Pages Functions”For runtime Accept: text/markdown content negotiation, add a Pages Function
that converts HTML responses to Markdown on-the-fly. Place a functions/
directory at your project root (next to dist/, not inside it):
your-project/ functions/ [[path]].ts dist/ ...Create functions/[[path]].ts:
import { OpenNavServer } from "@opennav-ai/opennav/server";
const opennav = new OpenNavServer();
interface PagesFunctionEnv { ASSETS: { fetch(request: Request): Promise<Response> };}
export async function onRequest(context: { request: Request; env: PagesFunctionEnv; next(): Promise<Response>;}): Promise<Response> { const url = new URL(context.request.url);
const decision = opennav.accept(context.request);
// HTML (or no Accept header) — pass through with Vary + Link. if (decision === "text/html" || decision === null) { const htmlResponse = await context.next(); const result = await opennav.negotiate({ request: context.request, htmlResponse }); if (result.isErr()) return new Response("Internal error", { status: 500 }); return result.value; }
// Markdown — try static .md first to avoid conversion cost. const mdPath = url.pathname .replace(/\/$/, "") .replace(/\.html$/, "") + ".md";
const staticMd = await context.env.ASSETS.fetch( new Request(new URL(mdPath, context.request.url), context.request), );
if (staticMd.ok) { const headers = new Headers(staticMd.headers); headers.set("Vary", "Accept"); headers.set("Content-Type", "text/markdown; charset=utf-8"); return new Response(staticMd.body, { status: staticMd.status, statusText: staticMd.statusText, headers, }); }
const htmlResponse = await context.next(); if (!htmlResponse.ok) return htmlResponse;
const result = await opennav.negotiate({ request: context.request, htmlResponse }); if (result.isErr()) return new Response("Internal error", { status: 500 }); return result.value;}Deploy with the Pages Git integration or via wrangler from the project root:
npx wrangler pages deploy dist/Wrangler auto-detects functions/ alongside the output directory and deploys
both together.
How it works
Section titled “How it works”| Request | Behavior |
|---|---|
| No Accept header or prefers HTML | Serves static HTML with Vary: Accept and Link: rel="alternate" to the Markdown representation. |
Prefers Markdown, .md file exists on disk | Serves the static .md file directly (zero conversion cost — OpenNav’s build output). |
Prefers Markdown, no .md file on disk | Fetches the HTML and converts it to Markdown on-the-fly via OpenNavServer. |
| Unsupported Accept type | Returns 406 Not Acceptable with Vary: Accept. |
Cloudflare’s built-in Markdown for Agents
Section titled “Cloudflare’s built-in Markdown for Agents”Cloudflare also offers a native Markdown for Agents feature (Pro plan and above) that converts HTML to Markdown at the edge with zero application code. Use it when you want hands-off conversion. Use the OpenNav Pages Function when you need custom stripping rules, want to ship Markdown alongside your existing Cloudflare setup, or are on a plan without the native feature.
Workers Static Assets
Section titled “Workers Static Assets”Workers can deploy static assets from a configured directory. Run OpenNav before
wrangler deploy so the assets directory already contains the generated
OpenNav files.
{ "$schema": "./node_modules/wrangler/config-schema.json", "name": "example-docs", "compatibility_date": "2026-05-01", "assets": { "directory": "./dist" }}{ "scripts": { "build": "astro build && opennav build --static --output dist --site-url https://example.com --site-name \"Example Docs\"", "deploy": "npm run build && wrangler deploy" }}If your Worker code serves assets through a binding, keep the binding pointed at the same directory OpenNav updates. OpenNav writes files only; it does not change Worker routing.
AI Gateway And Agent Experiments
Section titled “AI Gateway And Agent Experiments”AI Gateway is useful for observing and controlling AI application traffic. It does not publish website content. Pair it with OpenNav by having the agent fetch the generated files from a Cloudflare Pages site or a Workers static-assets deployment:
| Agent need | OpenNav file |
|---|---|
| Find the readable site index | https://example.com/llms.txt |
| Read page content without visual HTML | https://example.com/path/index.md |
| Verify generated files and static compatibility | https://example.com/.well-known/opennav.json |
| Read content-use guidance | https://example.com/robots.txt when accessGuidance is configured |
Use this when you are testing agent workflows, token cost, or browser fallback behavior around Cloudflare-hosted sites.
What Gets Published
Section titled “What Gets Published”After OpenNav runs, the same Cloudflare deployment includes the agent-readable files beside your normal site:
dist/ llms.txt llms-full.txt _headers .well-known/llms.txt .well-known/llms-full.txt .well-known/opennav.json index.md docs/getting-started/index.md robots.txtSee the generated files reference for the full file behavior and ownership rules.