Router: Compared to Astro and Next
phaze-cloudflare’s structure is a deliberate hybrid: file-based routing like both, an explicit Layout component + named slots like Astro, URL-driven parallel routes (@name/) like Next, and a single persistent root (the Phaze App + currentRoute() signal) that delivers Next’s “layout persists across navigation” win without Next’s folder-nesting machinery. The parallel routes are where phaze pulls ahead: each @name/ slot is a reactive per-binding island — a soft-nav re-runs only the slot whose route changed, leaving the layout, page, and sibling slots mounted (Astro serializes slots eagerly; Next reconciles a VDOM / refetches RSC).
| Axis | Astro | Next (App Router) | phaze-cloudflare |
|---|---|---|---|
| Routing | file-based src/pages/** (any basename → URL) | folder segments under app/**, [slug], (group)/, page.tsx allowlist | strict basename allowlist under src/pages/** — index.{tsx,phaze} / page.{tsx,phaze} / layout.{tsx,phaze} / <name>.<content-ext>.ts |
| Helper-file safety | every file routes; _* prefix opts out | only reserved filenames route (page.tsx, route.ts, layout.tsx, …) | only allowlisted basenames route — co-located helpers (Hero.tsx, _internal.ts) NEVER become URLs |
| API handlers | src/pages/**/*.ts (mixed with pages) | app/**/route.ts (mixed with pages, allowlisted filename) | src/api/** — separate top-level dir; URL auto-prefixed with /api/ |
| Layout definition | explicit — import <Layout>, wrap per page | convention — layout.tsx per folder, automatic ancestor cascade | convention OR explicit — co-located layout.tsx pairs with same-folder sibling page; explicit export const layout = X also supported |
| Layout cascade | manual (you wrap; forward via <slot name slot/>) | automatic by folder (root → section → page, ancestor chain) | same-folder only — no ancestor cascade by design; nested chains compose explicitly via re-export |
| Persists across nav | no (MPA) unless <ClientRouter/> | layouts persist, don’t re-render | reference-equality persistence — c(currentRoute()?.module.layout) only fires when the layout component reference changes; same layout across navs → no remount |
| Multiple holes | named slots (<slot name>), eager | children + parallel routes (@folder → named-slot props) | both — caller-filled named slots (<Fragment slot>, lazy () => thunks) AND URL-driven parallel routes (@name/ → per-slot loader, reactive per-binding island) |
| Page data | frontmatter / top-level await | async Server Component + params / searchParams | loader export → data prop / currentRoute().data |
The throughline: Astro = permissive routing + explicit composition; Next = restrictive routing + automatic cascade + persistence. phaze takes Next’s restrictive routing (only specific basenames route — the security model) and Astro’s explicit composition (no ancestor cascade — you compose nested chains yourself) and gets persistence for free from the signal router’s reference-equality on module.layout. It offers both slot-fill mechanisms too — Astro’s caller-filled named slots (<Fragment slot> → a lazy header={() => …} prop) and Next’s URL-filled parallel routes (@name/ → a per-folder panel={() => …} prop) — but because the router is signal-based, the URL-filled ones resolve to per-binding islands that re-run independently on nav, not an eager serialization (Astro) or a VDOM/RSC reconcile (Next). Co-located layout.tsx is a same-folder convenience; full ancestor cascade is reserved for a future non-breaking opt-in.