deed.page

Blog · 2026-10-02

SPA mode for client-side routes on deed.page

Static hosts serve real files. That works for a report with index.html and a few assets. It breaks for a client-side router: the user refreshes on /dashboard or shares /item/42, and the host looks for those paths on disk. Without a matching file you get a 404, even though the app would have rendered the route in the browser.

deed.page fixes that with an opt-in SPA flag. Turn it on and unknown paths fall back to root index.html. Leave it off and the host keeps exact-file serving, including an optional root 404.html.

What SPA mode changes

Default serving (no SPA):

With SPA mode on (X-Spa: 1 / spa=true):

SPA mode is stored on the site metadata (spa: true in publish and get_site responses). Send it on the publish that should own the flag.

REST: X-Spa or ?spa=1

Publish a tiny app with a client route. JSON map (handy in agents that already hold strings):

curl -sS -X POST https://deed.page/v1/sites \
  -H 'content-type: application/json' \
  -H 'X-Spa: 1' \
  -d '{
    "slug": "spa-demo",
    "files": {
      "index.html": "<!doctype html><html><body><div id=\"app\"></div><script>const r=location.pathname;document.getElementById(\"app\").textContent=\"route: \"+r;</script></body></html>",
      "assets/app.js": "/* real files still served as files */"
    }
  }'

Equivalent query form: ?slug=spa-demo&spa=1. A gzip tarball works the same way — add -H "X-Spa: 1" to the PUT:

tar -C ./dist -czf /tmp/app.tar.gz .
curl -sS -T /tmp/app.tar.gz \
  "https://deed.page/v1/sites?slug=spa-demo" \
  -H "Content-Type: application/gzip" \
  -H "X-Spa: 1" \
  -H "Authorization: Bearer $DEEDPAGE_TOKEN"

The helper script in the repo accepts --spa and sets that header for you.

On first anonymous publish, store the one-time deed_tok_… token. Later updates need Authorization: Bearer. The response includes spa: true when the flag is set.

After publish, both of these should return HTML from index.html (not a plain 404):

MCP: spa on publish_site

The Streamable HTTP MCP server at https://deed.page/mcp exposes the same flag as a boolean on publish_site and update_site:

{
  "name": "publish_site",
  "arguments": {
    "slug": "spa-demo",
    "spa": true,
    "files": {
      "index.html": "<!doctype html><h1>spa</h1><script>/* router */</script>"
    }
  }
}

Omit spa (or send false) for exact-file hosting. Pair with merge: true when you are adding chunks under the 4 MB per-request body cap; SPA is independent of merge and of opt-in ttl_seconds.

When to use it — and when not to

Use SPA mode for Vite/React/Vue/Svelte builds that rely on History API routes, canvas demos with deep links, or any static export where “missing path” should mean “load the app shell.”

Skip SPA mode for multi-page static sites, docs trees, or anything that should return a real 404 (or your custom 404.html) for unknown URLs. SPA fallback hides those misses by design.

Still upload real assets. CSS, JS, images, and fonts must be real paths in the publish. SPA only changes the HTML fallback for paths that are not files.

Keep the token. SPA does not make the slug world-writable. Strangers still get 409 slug_taken. Only the Bearer token updates or deletes the site.

Related

Questions or corrections: ops@avatar33.com.