|
1 | | -# Markdown content negotiation — serve the .md sibling of a page when the |
2 | | -# client asks for it via `Accept: text/markdown`. Home maps to `/index.md`; |
3 | | -# every other page `/foo/bar/` maps to `/foo/bar.md`. We need both the |
4 | | -# trailing-slash and no-slash variants so the `:splat` placeholder doesn't |
5 | | -# produce `/foo/.md`. |
6 | | -# / /index.md 301 Header:Accept=text/markdown |
7 | | -# /*/ /:splat.md 301 Header:Accept=text/markdown |
8 | | -# /* /:splat.md 301 Header:Accept=text/markdown |
| 1 | +# Markdown content negotiation — serve the `.md` sibling of a page when |
| 2 | +# the client requests it via `Accept: text/markdown`. Home maps to |
| 3 | +# `/index.md`; every other page `/a/b/.../z/` maps to `/a/b/.../z.md`. |
| 4 | +# |
| 5 | +# Three properties of these rules and why they are required: |
| 6 | +# |
| 7 | +# 1. `!` (force) — the source path has an underlying static file (the |
| 8 | +# HTML page) that would otherwise be served; the force flag is what |
| 9 | +# lets the rule fire at all. |
| 10 | +# |
| 11 | +# 2. `303` (See Other) — the rule MUST be a 3xx redirect, not a 2xx |
| 12 | +# rewrite. With a 2xx rewrite the response body for the `.md` content |
| 13 | +# gets cached by Cloudflare under the page's URL key (Cloudflare does |
| 14 | +# not honour `Vary: Accept`), poisoning the cache for subsequent |
| 15 | +# HTML clients. A redirect is fetched anew by the client, so the |
| 16 | +# Markdown response is cached under the `.md` URL key and the HTML |
| 17 | +# response stays cached under the page URL key. Among 3xx codes, |
| 18 | +# `303` is the semantically correct match for content negotiation |
| 19 | +# ("the representation you asked for lives at another URI"). |
| 20 | +# |
| 21 | +# 3. One rule per nesting depth, no splat. `_redirects` placeholders |
| 22 | +# (`:name`) match a single path segment — verified empirically |
| 23 | +# against Sevalla (a single `/:slug/` rule served HTML, not Markdown, |
| 24 | +# for `/ai-capabilities/text-generation/`). Covering the site |
| 25 | +# therefore requires one rule per depth. The splat form |
| 26 | +# (`/* /:splat.md`) would be greedy but empirically crashes the |
| 27 | +# Sevalla edge worker (HTTP 1101) when combined with `Header:Accept` |
| 28 | +# — bug in their parser. Current site max depth is 3 segments; one |
| 29 | +# extra level is included as headroom. |
| 30 | +# |
| 31 | +# Loop safety: the source pattern always ends in `/`, the rewrite target |
| 32 | +# always ends in `.md` (no slash). Those two URL spaces are disjoint, so |
| 33 | +# the client's follow-up `GET /foo.md` does not re-match the rule. This |
| 34 | +# only holds because all incoming page requests carry a trailing slash |
| 35 | +# (Sevalla's Pretty URLs normalizes bare paths first) AND because Pretty |
| 36 | +# URLs skips paths whose final segment contains a dot — so `/foo.md` is |
| 37 | +# never normalized into `/foo.md/`, which would otherwise loop. |
| 38 | +/ /index.md 303! Header:Accept=text/markdown |
| 39 | +/:a/ /:a.md 303! Header:Accept=text/markdown |
| 40 | +/:a/:b/ /:a/:b.md 303! Header:Accept=text/markdown |
| 41 | +/:a/:b/:c/ /:a/:b/:c.md 303! Header:Accept=text/markdown |
| 42 | +/:a/:b/:c/:d/ /:a/:b/:c/:d.md 303! Header:Accept=text/markdown |
9 | 43 |
|
10 | 44 | # Sevalla CDN does not resolve directory-to-index.html for path segments |
11 | 45 | # containing dots (e.g. `v0.7.x`). The `:version` placeholder matches a |
12 | | -# single path segment between slashes — so the trailing-slash variant |
13 | | -# captures the version without the trailing slash, avoiding the double- |
14 | | -# slash issue that the splat (`*`) has. |
| 46 | +# single path segment between slashes; the `200` rule rewrites each |
| 47 | +# versioned page's trailing-slash form to its `index.html`. |
| 48 | +# |
| 49 | +# Sevalla's Pretty URLs feature normally 301-redirects bare paths to the |
| 50 | +# trailing-slash form, but empirically it SKIPS paths whose final segment |
| 51 | +# contains a dot (treats them as file requests). So we cannot rely on it |
| 52 | +# for `vX.Y.Z` segments — we provide an explicit bare→with-slash 301 |
| 53 | +# below. |
| 54 | +# |
| 55 | +# ORDER MATTERS (first-match-wins). The `200` rewrites for the |
| 56 | +# with-slash source MUST come before the `301` redirects for the bare |
| 57 | +# source. Sevalla's placeholder matcher is lenient about trailing slash |
| 58 | +# in the source pattern: a source ending in `:version` (no slash) will |
| 59 | +# match both `/foo/X` AND `/foo/X/`. If the `301` rule appeared first, |
| 60 | +# it would match the with-slash request too and 301 to itself — an |
| 61 | +# infinite redirect loop (verified empirically). Putting the `:version/` |
| 62 | +# rule first, with literal trailing slash, makes the more-specific |
| 63 | +# pattern win for with-slash requests, leaving the `301` rule to handle |
| 64 | +# only the bare form. |
15 | 65 | # |
16 | 66 | # Syntax mirrors the doc example |
17 | 67 | # (https://docs.sevalla.com/static-sites/redirects#placeholders): |
18 | 68 | # /store/:category/:item /products/:category/:item |
19 | | -# No explicit status code — defaults to 301 redirect. |
20 | | - |
21 | | -/reference/api/:version/ /reference/api/:version/index.html 200 |
22 | | -/reference/api/:version /reference/api/:version/index.html 200 |
23 | 69 |
|
| 70 | +/reference/api/:version/ /reference/api/:version/index.html 200 |
24 | 71 | /reference/release-notes/:version/ /reference/release-notes/:version/index.html 200 |
25 | | -/reference/release-notes/:version /reference/release-notes/:version/index.html 200 |
| 72 | +/reference/api/:version /reference/api/:version/ 301 |
| 73 | +/reference/release-notes/:version /reference/release-notes/:version/ 301 |
26 | 74 |
|
27 | 75 | # ====================================================================== |
28 | 76 | # PERMANENT REDIRECTS DUE TO CONTENT MOVE |
29 | 77 | # ---------------------------------------------------------------------- |
30 | 78 | # Everything above is pattern-based (wildcards / :placeholders). |
31 | 79 | # Everything below is a 1:1 path move kept for backward compatibility |
32 | 80 | # with links from qvac.tether.io and older bookmarks (301 → current IA). |
| 81 | +# Sevalla's Pretty URLs 301-normalizes bare-path requests to the |
| 82 | +# trailing-slash form before these rules run, so only the with-slash |
| 83 | +# variants are listed. |
33 | 84 | # ====================================================================== |
34 | 85 |
|
35 | 86 | # Section rename: /about-qvac/* → /about/* (+ deletions folded into home) |
36 | 87 | /about-qvac/welcome/ / 301 |
37 | | -/about-qvac/welcome / 301 |
38 | 88 | /about-qvac/flagship-apps/ / 301 |
39 | | -/about-qvac/flagship-apps / 301 |
40 | 89 | /about-qvac/how-it-works/ /about/how-it-works/ 301 |
41 | | -/about-qvac/how-it-works /about/how-it-works/ 301 |
42 | 90 | /about-qvac/public-launch/ /about/public-launch/ 301 |
43 | | -/about-qvac/public-launch /about/public-launch/ 301 |
44 | 91 | /about-qvac/vision/ /about/vision/ 301 |
45 | | -/about-qvac/vision /about/vision/ 301 |
46 | 92 |
|
47 | 93 | # Section rename: /sdk/getting-started/* → top-level guides |
48 | 94 | /sdk/getting-started/ /introduction/ 301 |
49 | | -/sdk/getting-started /introduction/ 301 |
50 | 95 | /sdk/getting-started/quickstart/ /quickstart/ 301 |
51 | | -/sdk/getting-started/quickstart /quickstart/ 301 |
52 | 96 | /sdk/getting-started/installation/ /installation/ 301 |
53 | | -/sdk/getting-started/installation /installation/ 301 |
54 | 97 | /sdk/getting-started/configuration/ /configuration/ 301 |
55 | | -/sdk/getting-started/configuration /configuration/ 301 |
56 | 98 |
|
57 | 99 | # Section rename: /sdk/examples/ai-tasks/* → /ai-capabilities/* |
58 | 100 | /sdk/examples/ai-tasks/completion/ /ai-capabilities/text-generation/ 301 |
59 | | -/sdk/examples/ai-tasks/completion /ai-capabilities/text-generation/ 301 |
60 | 101 | /sdk/examples/ai-tasks/fine-tuning/ /ai-capabilities/fine-tuning/ 301 |
61 | | -/sdk/examples/ai-tasks/fine-tuning /ai-capabilities/fine-tuning/ 301 |
62 | 102 | /sdk/examples/ai-tasks/image-generation/ /ai-capabilities/image-generation/ 301 |
63 | | -/sdk/examples/ai-tasks/image-generation /ai-capabilities/image-generation/ 301 |
64 | 103 | /sdk/examples/ai-tasks/multimodal/ /ai-capabilities/multimodal/ 301 |
65 | | -/sdk/examples/ai-tasks/multimodal /ai-capabilities/multimodal/ 301 |
66 | 104 | /sdk/examples/ai-tasks/ocr/ /ai-capabilities/ocr/ 301 |
67 | | -/sdk/examples/ai-tasks/ocr /ai-capabilities/ocr/ 301 |
68 | 105 | /sdk/examples/ai-tasks/rag/ /ai-capabilities/rag/ 301 |
69 | | -/sdk/examples/ai-tasks/rag /ai-capabilities/rag/ 301 |
70 | 106 | /sdk/examples/ai-tasks/text-embeddings/ /ai-capabilities/text-embeddings/ 301 |
71 | | -/sdk/examples/ai-tasks/text-embeddings /ai-capabilities/text-embeddings/ 301 |
72 | 107 | /sdk/examples/ai-tasks/text-to-speech/ /ai-capabilities/text-to-speech/ 301 |
73 | | -/sdk/examples/ai-tasks/text-to-speech /ai-capabilities/text-to-speech/ 301 |
74 | 108 | /sdk/examples/ai-tasks/transcription/ /ai-capabilities/transcription/ 301 |
75 | | -/sdk/examples/ai-tasks/transcription /ai-capabilities/transcription/ 301 |
76 | 109 | /sdk/examples/ai-tasks/translation/ /ai-capabilities/translation/ 301 |
77 | | -/sdk/examples/ai-tasks/translation /ai-capabilities/translation/ 301 |
78 | 110 | /sdk/examples/ai-tasks/voice-assistant/ /ai-capabilities/voice-assistant/ 301 |
79 | | -/sdk/examples/ai-tasks/voice-assistant /ai-capabilities/voice-assistant/ 301 |
80 | 111 |
|
81 | 112 | # Section rename: /sdk/examples/p2p/* → /p2p-capabilities/* |
82 | 113 | /sdk/examples/p2p/blind-relays/ /p2p-capabilities/blind-relays/ 301 |
83 | | -/sdk/examples/p2p/blind-relays /p2p-capabilities/blind-relays/ 301 |
84 | 114 | /sdk/examples/p2p/delegated-inference/ /p2p-capabilities/delegated-inference/ 301 |
85 | | -/sdk/examples/p2p/delegated-inference /p2p-capabilities/delegated-inference/ 301 |
86 | 115 |
|
87 | 116 | # Section rename: /sdk/examples/utilities/* → /runtime/*, /models/*, /configuration/plugins/* |
88 | | -/sdk/examples/utilities/logging/ /runtime/logging/ 301 |
89 | | -/sdk/examples/utilities/logging /runtime/logging/ 301 |
90 | | -/sdk/examples/utilities/profiler/ /runtime/profiler/ 301 |
91 | | -/sdk/examples/utilities/profiler /runtime/profiler/ 301 |
92 | | -/sdk/examples/utilities/runtime-lifecycle/ /runtime/lifecycle/ 301 |
93 | | -/sdk/examples/utilities/runtime-lifecycle /runtime/lifecycle/ 301 |
94 | | -/sdk/examples/utilities/download-lifecycle/ /models/download-lifecycle/ 301 |
95 | | -/sdk/examples/utilities/download-lifecycle /models/download-lifecycle/ 301 |
96 | | -/sdk/examples/utilities/sharded-models/ /models/sharded-models/ 301 |
97 | | -/sdk/examples/utilities/sharded-models /models/sharded-models/ 301 |
98 | | -/sdk/examples/utilities/plugin-system/ /configuration/plugins/ 301 |
99 | | -/sdk/examples/utilities/plugin-system /configuration/plugins/ 301 |
| 117 | +/sdk/examples/utilities/logging/ /runtime/logging/ 301 |
| 118 | +/sdk/examples/utilities/profiler/ /runtime/profiler/ 301 |
| 119 | +/sdk/examples/utilities/runtime-lifecycle/ /runtime/lifecycle/ 301 |
| 120 | +/sdk/examples/utilities/download-lifecycle/ /models/download-lifecycle/ 301 |
| 121 | +/sdk/examples/utilities/sharded-models/ /models/sharded-models/ 301 |
| 122 | +/sdk/examples/utilities/plugin-system/ /configuration/plugins/ 301 |
100 | 123 | /sdk/examples/utilities/write-custom-plugin/ /configuration/plugins/write-custom-plugin/ 301 |
101 | | -/sdk/examples/utilities/write-custom-plugin /configuration/plugins/write-custom-plugin/ 301 |
102 | 124 |
|
103 | 125 | # Section rename: /sdk/api → /reference/api, /sdk/release-notes → /reference/release-notes |
104 | 126 | /sdk/api/ /reference/api/ 301 |
105 | | -/sdk/api /reference/api/ 301 |
106 | 127 |
|
107 | 128 | # Section rename: /sdk/tutorials/* → /tutorials/* |
108 | 129 | /sdk/tutorials/electron/ /tutorials/electron/ 301 |
109 | | -/sdk/tutorials/electron /tutorials/electron/ 301 |
110 | 130 | /sdk/tutorials/expo/ /tutorials/expo/ 301 |
111 | | -/sdk/tutorials/expo /tutorials/expo/ 301 |
112 | 131 |
|
113 | 132 | # Section rename: /http-server → /cli/http-server |
114 | 133 | /http-server/ /cli/http-server/ 301 |
115 | | -/http-server /cli/http-server/ 301 |
116 | 134 |
|
117 | 135 | # ====================================================================== |
118 | 136 | # CATCH-ALL 404 — MUST BE THE LAST RULE |
119 | 137 | # ---------------------------------------------------------------------- |
120 | 138 | # Sevalla serves /404.html automatically for unresolved paths, but with |
121 | 139 | # HTTP 200, which breaks SEO, link checkers, analytics and HTTP clients. |
122 | 140 | # This explicit rule forces a real `404 Not Found` status while still |
123 | | -# rendering the same /404.html body. Only reached when no static file and |
124 | | -# no rule above matches. |
| 141 | +# rendering the same /404.html body. Reached in two distinct cases: |
| 142 | +# |
| 143 | +# 1. No static file matches the request AND no earlier rule matches. |
| 144 | +# 2. An earlier `200` rewrite points at a target that doesn't exist |
| 145 | +# (e.g. `/reference/api/notaversion/` → rewrite to |
| 146 | +# `/reference/api/notaversion/index.html` which isn't built). |
| 147 | +# Sevalla's pipeline continues evaluating rules in this case, and |
| 148 | +# this catch-all gives those orphaned rewrites a clean 404 instead |
| 149 | +# of falling back to Sevalla's soft-200 default. |
| 150 | +# |
| 151 | +# Known edge case (intentionally not fixed): a direct request to |
| 152 | +# `/404.html` returns 200 because Sevalla's static-file resolution runs |
| 153 | +# before `_redirects` and serves the file as-is, bypassing this rule. |
| 154 | +# `/404.html` is not in the sitemap, not linked from any page, and not |
| 155 | +# referenced in any external surface; practical traffic to it is zero. |
125 | 156 | # ====================================================================== |
126 | 157 |
|
127 | 158 | /* /404.html 404 |
0 commit comments