Error Pages
A spec with route: '*' is your site's custom 404 page. It renders through the normal pipeline — layout, styles, components, hydration — with status 404 whenever no route matches. Without one, visitors to a bad URL get the framework's unbranded default.
The custom 404 page
Create a spec like any other page, with '*' as the route. Use the shared layout() so the page carries the site's nav and footer:
import { layout } from '../components/layout.js'
export default {
route: '*',
meta: {
title: 'Page not found',
styles: ['/pulse-ui.css', '/theme.css', '/app.css'],
},
view: () => layout({
content: `
<h1>Page not found</h1>
<p>That page doesn't exist. <a href="/">Back home</a></p>
`,
}),
}
The filename doesn't matter — not-found.js is just a readable convention. The route: '*' property is what registers it as the fallback.
How it behaves
| Aspect | Behaviour |
|---|---|
| Status code | Always 404 — correct for SEO; crawlers de-index dead URLs |
| Pipeline | Full normal render: meta, styles, components, hydration if it has mutations/actions |
| Validation | Validated at startup like every spec — a broken 404 page fails fast |
| Matching | Only when no other route matches. It is a fallback, not a route — it never shadows real pages |
| Client-side navigation | A bad link falls back to a full page load, which renders the custom 404 |
| Caching | 404 responses are never stored in the in-process page cache |
Making the 404 useful
The best 404 pages help the visitor recover. Since the page is a normal spec, it can use any component — search, popular links, even server data:
export default {
route: '*',
meta: { title: 'Page not found', styles: ['/pulse-ui.css', '/theme.css', '/app.css'] },
server: {
popular: async () => getPopularPages(), // suggest somewhere to go
},
view: (state, server) => layout({
content: `
<h1>Page not found</h1>
<p>Try one of these instead:</p>
${list({ items: (server.popular ?? []).map(p => `<a href="${p.href}">${p.title}</a>`) })}
`,
}),
}
500s and other errors
Server errors are handled by createServer's onError option, not a spec — when a render throws, the spec pipeline itself may be the thing that is broken, so the handler works at the HTTP level:
await createServer(pages, {
onError: (err, req, res) => {
console.error(err)
if (res.headersSent) return
res.writeHead(500, { 'Content-Type': 'text/html; charset=utf-8' })
res.end('<h1>Something went wrong</h1><p><a href="/">Back home</a></p>')
},
})
For errors thrown inside a view specifically, prefer onViewError on the spec — it returns a 200 with fallback HTML instead of a 500, keeping the page alive when one piece of data is malformed.
route: '*' for URLs that don't exist, onViewError for views that throw on bad data, onError for everything else that goes wrong server-side.Reference
| Property | Where | Notes |
|---|---|---|
route: '*' | spec | Custom not-found page — rendered with status 404 when no route matches |
onViewError | spec | Fallback HTML when the view throws — returns 200 |
onError | createServer option | HTTP-level handler for unhandled server errors (500s) |