Routing: Astro's Own Catch-All, Now Database-Backed

Website Rebuild with Astro

Chapter 3 · Routing: Astro's Own Catch-All, Now Database-Backed

astro1's own Chapter 3 built [...path].astro against Content Collections, and Chapter 10 showed getStaticPaths() disappearing entirely in server mode. This chapter is where both findings actually pay off together — the catch-all route, hooked to a real database query, with no pre-declared path list anywhere.

The Catch-All Route, Database-Backed

// src/pages/[...path].astro --- import { db } from '../db/client'; import { pages } from '../db/schema'; import { eq } from 'drizzle-orm'; const { path } = Astro.params; const fullPath = path ?? ''; const [page] = await db.select().from(pages).where(eq(pages.fullPath, fullPath)); --- <h1>{page.title}</h1>

No getStaticPaths() anywhere in this file — Chapter 1's own server-mode configuration means every possible path value resolves fresh, per request, straight from the pages table.

Genuine convergence — the payoff of the whole series' own throughline
While astro1's own catch-all stayed backed by Content Collections, its final shape looked structurally different from every sibling rebuild's own routing. Here, with a real database query behind it, Astro's routing finally looks the same shape as all four completed siblings: one catch-all route, one lookup by path, resolved per request. The differences that remain are in the ORM and syntax, not in the underlying architecture — a genuine convergence, not a coincidence.

A Real Gotcha: Your Own Catch-All Shadows the Conventional 404 Page

Astro's own 404.astro never gets reached
Astro's usual convention is that a src/pages/404.astro file is served automatically for any unmatched route. But [...path].astro matches every possible URL, including genuinely nonexistent ones — so it always wins the routing match first, and 404.astro is never reached through normal routing at all. A missing page simply falls through this file's own if (!page) branch instead — that has to be handled explicitly, not left to Astro's own file-based convention.

Handling Not-Found Correctly

// src/pages/[...path].astro --- const { path } = Astro.params; const fullPath = path ?? ''; const [page] = await db.select().from(pages).where(eq(pages.fullPath, fullPath)); if (!page) { Astro.response.status = 404; return Astro.rewrite('/404'); } ---

Astro.rewrite('/404') renders the real content of src/pages/404.astro for this request, while Astro.response.status = 404 ensures the response actually carries a genuine 404 status code — both are needed together; rewrite alone would render the right content with a misleading 200 status.

Routing, Now Genuinely Converged

Next.jsDjangoLaravelRailsAstro
Mechanism[...path] folder<path:full_path>{path?} + regex*path glob[...path].astro
Resolved byA database lookupA database lookupA database lookupA database lookupA database lookup

Hands-On Exercises

Exercise 1

Build [...path].astro with a real Drizzle query resolving Astro.params.path against the pages table, and confirm a real stored page renders correctly at its own full path.

📄 View solution
Exercise 2

Visit a genuinely nonexistent path with no explicit not-found handling in place yet, and observe exactly what happens — confirming src/pages/404.astro is never actually reached.

📄 View solution
Exercise 3

Add the explicit Astro.response.status = 404 plus Astro.rewrite('/404') handling, and confirm the response now carries both a real 404 status code and the correct 404 page content together.

📄 View solution

Chapter 3 Quick Reference

  • [...path].astro — no getStaticPaths() needed; resolves fresh, per request
  • Genuine convergence — Astro's routing finally matches the same shape as all four sibling rebuilds
  • The 404 gotcha — a global catch-all shadows src/pages/404.astro; it's never reached via normal routing
  • Astro.response.status = 404 + Astro.rewrite('/404') — the correct combination for a real 404, both status and content
  • Next chapter: Views & Rendering Trusted Content