Memory Appendix
Cloudflare Pages Extensionless Routes
Any Next.js metadata route or route handler emitting binary content at an extensionless URL must be pinned via public/_headers — Cloudflare Pages + nosniff will
Source memory file:
feedback_cloudflare_pages_extensionless_routes.md· Category: Feedback / working-preference This is a verbatim dump of Claude's persistent memory for the Octopus project. Rendered inside a code block so nothing is altered.
---
name: Cloudflare Pages extensionless routes need explicit Content-Type
description: Any Next.js metadata route or route handler emitting binary content at an extensionless URL must be pinned via public/_headers — Cloudflare Pages + nosniff will silently break it otherwise
type: feedback
originSessionId: a1e9a8ef-0505-4d0d-ae0a-97b98d29076c
---
When adding an App Router metadata file (`opengraph-image.tsx`, `twitter-image.tsx`, `icon.tsx`, `sitemap.xml` as TSX, etc.) or a `route.tsx` returning binary content in `frontend/octopus-website/`, you **must** add a matching entry to `frontend/octopus-website/public/_headers` pinning `Content-Type`.
**Why:** The site deploys to Cloudflare Pages (apex `octopuscards.io`) which sets `X-Content-Type-Options: nosniff` globally. Next.js static export emits these routes as **extensionless files** (e.g., `out/opengraph-image`, `out/og/cards/free-fire-diamonds`). Cloudflare Pages serves extensionless files as `application/octet-stream`, and nosniff blocks browsers/crawlers from guessing. Result: WhatsApp, LinkedIn, Slack, etc. silently reject the resource — the meta tag points at a "file" of the wrong MIME type.
Symptom: OG preview falls back to random page-scraped image (or nothing). No error in logs. Discovered when a WhatsApp preview showed `cards/roshan-afghanistan.png` instead of the real OG card.
**How to apply:**
1. In the file, declare static-export compatibility:
```ts
export const dynamic = 'force-static';
export const revalidate = false;
```
2. Add the path to `public/_headers` with the correct MIME type:
```
/my-new-route
Content-Type: image/png
Cache-Control: public, max-age=3600
```
Use a wildcard (`/og/cards/*`) for dynamic segments.
3. Verify after build: `file out/<route-name>` to confirm content, and `grep <route-name> out/_headers` to confirm the header is present.
**Exception:** if the emitted URL has a recognized extension (e.g., the `/og/docs/[...slug]/image.webp` pattern where `image.webp` is baked into the last slug segment), Cloudflare infers MIME from the extension and no `_headers` entry is needed. This is the older pattern in `src/app/og/docs/[...slug]/route.tsx` — you can mirror it if you prefer not to touch `_headers`.