# SPA fallback: why every URL needs its own document

> Per-page content separation (no SPA fallback): A catch-all route returning the same skeleton for every URL makes pages indistinguishable to an agent.

- Source: /checks/spa-fallback
- Site: AgenticWebScore
- Language: English

---
A catch-all route returning the same skeleton for every URL makes pages indistinguishable to an agent.

| Property | Value |
| --- | --- |
| Check id | `spa-fallback` |
| Dimension | Content Accessibility (22%) |
| Tier | Emerging — pool 5 pts |
| Weight inside tier | 2 |
| Impact | Medium |
| Maturity | Experimental — An early convention that may still change shape. |
| Conditional | No |
| Publishing skill | `server-rendering` |
| Standards | — |

## What is measured

This check declares no probe path; the scanner compares the homepage `/` with a second same-origin URL. Pass when the two responses have different `<title>` values and different main text (each route ships its own document); partial when the titles differ but the main content region is identical, or when an unknown path answers 200 with the app shell instead of 404; fail when two different URLs return the same title and near-identical markup — the catch-all rewrite to one `index.html`.

## Reproduce it yourself

You do not have to take our word for any of this. Run the same measurement against your own domain:

```bash
DOMAIN="https://example.com"
for p in / /about; do printf '%s\t' "$p"; curl -sL -o /tmp/page.html -w '%{http_code}\t' "$DOMAIN$p"; perl -0777 -ne 'm{<title[^>]*>(.*?)</title>}si and do { my $t=$1; $t=~s/\s+/ /g; print "title=$t\t" }; s{<(script|style)[^>]*>.*?</\1>}{}gsi; m{<main[^>]*>(.*?)</main>}si and $_=$1; s/<[^>]*>//g; s/\s+/ /g; print "text=", length($_), "\n"' /tmp/page.html; done
```

## The fix

This is the artifact to ship. It is the complete change, not a description of one.

```javascript
// Serve dist/<route>/index.html per URL; never rewrite every path to one shell.
import express from 'express';
import { existsSync } from 'node:fs';

const app = express();

app.use((req, res) => {
  if (req.method !== 'GET') return res.status(405).end();
  const file = new URL(`dist${req.path.replace(/\/$/, '/index')}.html`, import.meta.url).pathname;
  if (existsSync(file)) return res.sendFile(file);
  res.status(404).type('html')
    .send('<!doctype html><html lang="en"><title>404</title><h1>Not found</h1></html>');
});

app.listen(3000);
```

> Prerender each route at build time or render it on the server, and return a real 404 for unknown paths — delete the `/* -> /index.html` rewrite.

## Related checks

| Check | Tier | Impact | Same skill |
| --- | --- | --- | --- |
| `markdown-negotiation` — Markdown content negotiation | Recommended | Critical | — |
| `headings` — Semantic heading structure | Essential | High | — |
| `meta` — Title and summary metadata | Essential | High | — |
| `jsonld` — JSON-LD structured data | Essential | Critical | — |
| `faq-schema` — FAQ / question-answer schema | Recommended | High | — |
| `breadcrumb` — Breadcrumb / navigation schema | Emerging | Low | — |
| `summary` — Prominent summary block | Recommended | Medium | — |
| `lang` — Language declaration and multilingual access | Recommended | Medium | — |
