Use Accept header for skills HTML/markdown negotiation (#1454)

## Summary

Follow-up to #1452. `Sec-Fetch-Mode` / `Sec-Fetch-Dest` didn't reliably
split HTML vs. markdown at the CDN edge, so curl could still get the
HTML landing page. Switch to the `Accept` header:

- Browsers send `Accept: text/html,...` on top-level navigations.
- `curl`, `fetch()`, and agent fetchers send `*/*` or omit `Accept`.
- Serve HTML only when `text/html` is explicitly listed; everything else
gets `SKILL.md`.
- `Vary` updated to `Accept` to match.

## Test plan

- [ ] Deploy preview
- [ ] `curl -sSL https://skill.stack-auth.com/ | head -3` returns
markdown frontmatter
- [ ] Browser load of `https://skill.stack-auth.com/` still shows the
HTML landing page
- [ ] Purge Vercel cache if stale variants persist

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Bug Fixes**
* Improved content format negotiation for skill resources to correctly
serve HTML or markdown based on client requests.

* **Chores**
* Optimized caching behavior for edge and CDN services to enhance global
content distribution efficiency.

<!-- review_stack_entry_start -->

[![Review Change
Stack](https://storage.googleapis.com/coderabbit_public_assets/review-stack-in-coderabbit-ui.svg)](https://app.coderabbit.ai/change-stack/hexclave/stack-auth/pull/1454?utm_source=github_walkthrough&utm_medium=github&utm_campaign=change_stack)

<!-- review_stack_entry_end -->

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
Mantra 2026-05-20 13:15:13 -07:00 committed by GitHub
parent 055304d3fd
commit a84c7814de
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

View File

@ -211,7 +211,7 @@ For the full, current flag list and any commands added after this skill was gene
const COMMON_HEADERS = {
"Cache-Control": "public, max-age=3600, s-maxage=3600",
// CDN must cache markdown (curl/agents) and HTML (browser navigate) separately.
"Vary": "Sec-Fetch-Mode, Sec-Fetch-Dest",
"Vary": "Accept",
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "GET, HEAD, OPTIONS",
"Access-Control-Allow-Headers": "*",
@ -431,12 +431,18 @@ function renderHtml(): string {
</html>`;
}
const MARKDOWN_PREFERRING_TYPES = new Set(["*/*", "text/plain", "text/markdown", "text/x-markdown"]);
function wantsHtml(req: Request): boolean {
// Browsers navigating to a top-level URL send Sec-Fetch-Mode: navigate.
// curl, fetch(), and agent fetchers do not, so they keep getting markdown.
if (req.headers.get("sec-fetch-mode") === "navigate") return true;
if (req.headers.get("sec-fetch-dest") === "document") return true;
return false;
// Browsers send `Accept: text/html,...` before `*/*`; curl/fetch/agents send
// `*/*` (or omit Accept). Serve HTML only when text/html appears AND is
// listed before any markdown-preferring type that would otherwise win.
const accept = req.headers.get("accept") ?? "";
const types = accept.split(",").map((part) => part.trim().split(";")[0].trim().toLowerCase());
const htmlIndex = types.indexOf("text/html");
if (htmlIndex === -1) return false;
const competitorIndex = types.findIndex((t) => MARKDOWN_PREFERRING_TYPES.has(t));
return competitorIndex === -1 || htmlIndex < competitorIndex;
}
export function GET(req: Request) {