Skip to content

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 OpenNav in the project that owns the Cloudflare build command.

Terminal window
npm install @opennav-ai/opennav
Cloudflare pathOpenNav setup
Pages with any static output folderRun the CLI after your normal build.
Pages with AstroUse OpenNavAstro after astro build.
Pages with Next.js static exportUse OpenNavNext with output: "export".
Pages with a custom build script or monorepoUse the TypeScript SDK from the script that knows the output folder.
Workers static assetsRun OpenNav before wrangler deploy uploads the configured assets directory.
AI Gateway or agent experimentsFetch the generated OpenNav files from Pages or Workers assets; AI Gateway remains the AI app proxy and observability layer.

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 settingValue
Build commandnpm run build
Build output directorydist

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:

Terminal window
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 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 settingValue
Build commandnpm run build
Build output directorydist

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,
},
});

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 settingValue
Build commandnpm run build
Build output directoryout

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);

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 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 pointCloudflare Pages header behavior
CLI with --platform cloudflare-pagesCreates 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.md

The generated _headers block sets:

Route patternResponse header behavior
/*.mdServes generated Markdown mirrors as text/markdown; charset=utf-8.
/llms.txt, /llms-full.txt, and .well-known copiesServes generated indexes as text/plain; charset=utf-8.
/.well-known/opennav.jsonServes 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 AI

This 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.

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:

Terminal window
npx wrangler pages deploy dist/

Wrangler auto-detects functions/ alongside the output directory and deploys both together.

RequestBehavior
No Accept header or prefers HTMLServes static HTML with Vary: Accept and Link: rel="alternate" to the Markdown representation.
Prefers Markdown, .md file exists on diskServes the static .md file directly (zero conversion cost — OpenNav’s build output).
Prefers Markdown, no .md file on diskFetches the HTML and converts it to Markdown on-the-fly via OpenNavServer.
Unsupported Accept typeReturns 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 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 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 needOpenNav file
Find the readable site indexhttps://example.com/llms.txt
Read page content without visual HTMLhttps://example.com/path/index.md
Verify generated files and static compatibilityhttps://example.com/.well-known/opennav.json
Read content-use guidancehttps://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.

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.txt

See the generated files reference for the full file behavior and ownership rules.