Commits

The stylish 404, the caught 500, and the first sketch of the migrator

Custom error pages, an audit that turns 500 into 404, and the first draft of the migrator

A 404 that doesn't look like an error

Until now, if you opened a URL that didn't exist, Next.js (the web framework) served you its usual gray page. This week we made our own. Two pieces: a not-found.tsx at the root, for unknown URLs like /xyz, and one inside the (frontend) group, for the notFound() calls thrown by the fiches when they don't find content.

And an error.tsx for real errors: a client component (which runs in the browser), role=alert (the signal that screen readers announce as an alert), a retry button. We don't show the technical message to the user; in the console (the browser's log) it does stay recorded.

The curious part: the first deployment served the root 404 with the correct markup but with no stylesheet at all. The layout with the CSS lives in the (frontend) group, and the root not-found hangs directly off app/layout. Next doesn't propagate CSS imports upward in the route tree. Unglamorous and correct solution: re-import tokens.css, base.css and ui.css inside the file itself. The 404 bundle (the code package) (49KB) now carries the .notfound classes, and the home one stays clean — this proves the import is scoped to the 404 chunk.

Hunting the 500

The fiches audit caught a serious one: /consells/incorrecta/com-reduir... — a category that doesn't exist — returned a 500 (the internal server error). Postgres (the database) rejected the value outside the enum (the allowed values) and the exception bubbled up to the page. Now lib/format.ts exports CONSELL_CATEGORIES and if the category isn't in the list, it cuts off with notFound() before touching the database. The raw URL returns 404, which is what's expected.

Second attempt on the same day: filtering by _status = 'published'. Also a 500 — Payload doesn't allow querying _status. And it's redundant: the onlyPublicat access already filtered out drafts. We've left it commented out on all six pages so nobody falls into the same trap.

Listing details

The listings audit (spec 03j) left four fixes:

  • listingHref didn't reset page=1 when changing filter.
  • Pages 2+ with an active filter were marked as indexable; now they're noindex. Only page 1 without filters can be indexed (appear in search engines).
  • The county map in the /agenda hero didn't exist: three links with each county's color.
  • The agenda days now show the day of the week in red: Mon., Tue., Wed...

And all the listings switch from force-dynamic to revalidate = 3600 (it pre-builds once an hour and reuses it), as the specification requires. The 404 also has to be force-dynamic; otherwise, Next prerenders it statically along with the landings and serves its default "404" instead of ours.

And we already have the migrator sketched out

Phase 3 in sight. The advertising import will already have its own chapter (Advertising gets imported: Advanced Ads into Payload); today is the big job: we've created scripts/migrate-wp/wp_to_payload.py, the foundation for what the migrator will learn to do with Payload (The migrator learns to talk to Payload). The file carries the mapping of each post_type of WordPress (the old CMS) to its Payload collection (the new CMS), the discard rules (D1-D4, with detection of Elementor, the old page builder) and the enum lists per collection. With tests: 64 checks, all green.

The next step is the pilot import of one county. We'll talk about that tomorrow.