Skip to content

feat(site): add markdown content negotiation via Netlify edge function#573

Merged
decepulis merged 3 commits intomainfrom
feat/markdown-content-negotiation
Feb 19, 2026
Merged

feat(site): add markdown content negotiation via Netlify edge function#573
decepulis merged 3 commits intomainfrom
feat/markdown-content-negotiation

Conversation

@decepulis
Copy link
Copy Markdown
Collaborator

@decepulis decepulis commented Feb 19, 2026

Summary

Serve pre-built markdown to AI agents via HTTP content negotiation. When a request includes Accept: text/markdown, a Netlify Edge Function rewrites to the .md file generated by the llms-markdown integration and returns it with the correct Content-Type and an x-markdown-tokens header.

Inspiration

Our implementation uses Netlify Edge Functions with declarative header matching to achieve the same effect with pre-built .md files.

Changes

  • Edge function (site/netlify/edge-functions/markdown-negotiation.ts): triggers only for /blog/* and /docs/* requests with Accept: text/markdown. Rewrites to the .md sibling, sets Content-Type: text/markdown, Vary: Accept, Cache-Control, and x-markdown-tokens.
  • CDN caching enabled via cache: "manual" with s-maxage=31536000. Netlify atomic deploys automatically invalidate, so cached responses are always fresh per deploy.
  • @netlify/edge-functions added as a dev dependency for type safety.

Test plan

Markdown response — returns markdown with correct headers:

curl -sI -H "Accept: text/markdown" \
  https://deploy-preview-573--vjs10-site.netlify.app/docs/framework/react/concepts/architecture
# Expect:
#   content-type: text/markdown; charset=utf-8
#   vary: Accept-Encoding, Accept
#   x-markdown-tokens: <number>
#   cache-status: "Netlify Edge"; ...

Normal HTML response — unaffected by edge function:

curl -sI \
  https://deploy-preview-573--vjs10-site.netlify.app/docs/framework/react/concepts/architecture
# Expect:
#   content-type: text/html; charset=UTF-8
#   (no x-markdown-tokens header)

Fallback for pages without .md — edge function passes through:

curl -sI -H "Accept: text/markdown" \
  https://deploy-preview-573--vjs10-site.netlify.app/docs/
# Expect: content-type: text/html (no .md file exists for index)

🤖 Generated with Claude Code

Serve pre-built markdown files when clients send Accept: text/markdown.
Includes token count manifest for x-markdown-tokens response header.
@vercel
Copy link
Copy Markdown

vercel bot commented Feb 19, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
vjs-10-demo-react Ignored Ignored Preview Feb 19, 2026 2:31pm

Request Review

@netlify
Copy link
Copy Markdown

netlify bot commented Feb 19, 2026

Deploy Preview for vjs10-site ready!

Name Link
🔨 Latest commit 092770c
🔍 Latest deploy log https://app.netlify.com/projects/vjs10-site/deploys/69971f56902d8300089fb3ee
😎 Deploy Preview https://deploy-preview-573--vjs10-site.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Feb 19, 2026

📦 Bundle Size Report

Package Size Diff %
@videojs/core 5.67 kB 0 B ░░░░░░░░ 0%
@videojs/element 1.60 kB 0 B ░░░░░░░░ 0%
@videojs/html 8.63 kB 0 B ░░░░░░░░ 0%
@videojs/icons 3.46 kB 0 B ░░░░░░░░ 0%
@videojs/react 12.82 kB 0 B ░░░░░░░░ 0%
@videojs/store 1.94 kB 0 B ░░░░░░░░ 0%
@videojs/utils 2.47 kB 0 B ░░░░░░░░ 0%

Total: 36.60 kB · 0 B · 0%


Entry Breakdown

Subpath sizes are the additional bytes on top of the root entry point, measured by bundling root + subpath together and subtracting the root-only size.

@videojs/core
Entry Base PR Diff %
. 3.09 kB 3.09 kB 0 B 0%
./dom 2.58 kB 2.58 kB 0 B 0%
total 5.67 kB 5.67 kB 0 B 0%
@videojs/element
Entry Base PR Diff %
. 817 B 817 B 0 B 0%
./context 823 B 823 B 0 B 0%
total 1.60 kB 1.60 kB 0 B 0%
@videojs/icons
Entry Base PR Diff %
./react 2.10 kB 2.10 kB 0 B 0%
./html 1.37 kB 1.37 kB 0 B 0%
total 3.46 kB 3.46 kB 0 B 0%
@videojs/react
Entry Base PR Diff %
. 7.69 kB 7.69 kB 0 B 0%
./audio 266 B 266 B 0 B 0%
./background 35 B 35 B 0 B 0%
./video 4.84 kB 4.84 kB 0 B 0%
total 12.82 kB 12.82 kB 0 B 0%
@videojs/store
Entry Base PR Diff %
. 1.29 kB 1.29 kB 0 B 0%
./html 468 B 468 B 0 B 0%
./react 199 B 199 B 0 B 0%
total 1.94 kB 1.94 kB 0 B 0%
@videojs/utils
Entry Base PR Diff %
./array 104 B 104 B 0 B 0%
./dom 684 B 684 B 0 B 0%
./events 227 B 227 B 0 B 0%
./function 197 B 197 B 0 B 0%
./object 119 B 119 B 0 B 0%
./predicate 265 B 265 B 0 B 0%
./string 110 B 110 B 0 B 0%
./style 185 B 185 B 0 B 0%
./time 478 B 478 B 0 B 0%
./number 158 B 158 B 0 B 0%
total 2.47 kB 2.47 kB 0 B 0%

ℹ️ How to interpret

Sizes are minified + brotli, measured with esbuild.
Package totals are computed as root size + marginal subpath costs.
Subpath marginal cost = (root + subpath bundled together) − root alone.

Icon Meaning
No change
🔺 Increased ≤ 10%
🔴 Increased > 10%
🔽 Decreased
🆕 New (no baseline)

Run pnpm size locally to check current sizes.

decepulis and others added 2 commits February 19, 2026 07:59
The pre-built token manifest added a fetch round-trip in the edge
function, negating the latency benefit. Since we already have the
markdown body in memory, compute tokens inline with chars/4.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add CDN caching for edge function responses. Netlify atomic deploys
automatically invalidate the cache, so s-maxage can be set to 1 year.
Cached responses don't count toward edge function invocations.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@decepulis decepulis force-pushed the feat/markdown-content-negotiation branch from 1a3a8fd to 092770c Compare February 19, 2026 14:33
@decepulis decepulis merged commit 52d20bf into main Feb 19, 2026
18 checks passed
@decepulis decepulis deleted the feat/markdown-content-negotiation branch February 19, 2026 14:39

const headers = new Headers();
headers.set('content-type', 'text/markdown; charset=utf-8');
headers.set('cache-control', 'public, s-maxage=31536000');
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will the content not change?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants