GitHub

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:

src/pages/not-found.js
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.

Every site should have one. The framework's default 404 is a bare unstyled page with no navigation — a dead end for visitors and a missed signal for search engines crawling old URLs.

How it behaves

AspectBehaviour
Status codeAlways 404 — correct for SEO; crawlers de-index dead URLs
PipelineFull normal render: meta, styles, components, hydration if it has mutations/actions
ValidationValidated at startup like every spec — a broken 404 page fails fast
MatchingOnly when no other route matches. It is a fallback, not a route — it never shadows real pages
Client-side navigationA bad link falls back to a full page load, which renders the custom 404
Caching404 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.

Three layers, three jobs: 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

PropertyWhereNotes
route: '*'specCustom not-found page — rendered with status 404 when no route matches
onViewErrorspecFallback HTML when the view throws — returns 200
onErrorcreateServer optionHTTP-level handler for unhandled server errors (500s)