Skip to content
AstroCraft Docs
On this theme

Routing

Urbic has thirty-two site routes. Thirty-one are prerendered HTML in dist/client/; /contact/ renders per request. The CMS mounts fifteen more under /admin/, all on demand.

The site

Route Source Built from
/ pages/index.astro Sections/Home/* — 8 bands
/about/ pages/about.astro Sections/About/*, config/aboutPageData
/work/ pages/work/index.astro the projects collection
/work/<slug>/ pages/work/[slug].astro projects — 9 entries, with a prev/next pager
/services/ pages/services/index.astro config/servicesData.json.ts
/services/<slug>/ pages/services/[slug].astro servicesData — 4 entries
/blog/ pages/blog/index.astro the blog collection + the layout toggle
/blog/<note>/ pages/blog/[note].astro blog — 7 entries
/blog/topic/<topic>/ pages/blog/topic/[topic].astro topics derived from the notes — 3 pages
/contact/ pages/contact.astro Sections/Contact/*on demand
/privacy/, /terms/ pages/privacy.astro, pages/terms.astro config/legalData.json.ts
/404 pages/404.astro Sections/NotFound/*
/examples/ui pages/examples/[catalog].astro Sections/UiCatalog/* — dev only

Every page carries export const prerender = true explicitly. That is not redundant: output: "server" makes on-demand the default, so a page without the line would silently start rendering per request. The one page that omits it does so on purpose.

The one on-demand page

/contact/ needs Astro.getActionResult, which exists only on a page that renders per request — it is how the server re-renders the form with its errors or its sent state, so the enquiry form works with JavaScript off. Everything else is a flat static tree.

If you delete the contact action, restore prerender = true on that page and the whole site is static again. The markup keeps rendering; the form simply posts nowhere.

The four generated endpoints

/robots.txt, /llms.txt, /rss.xml and /sitemap-index.xml are all generated, and all four derive their absolute URLs from site in astro.config.mjs — so setting SITE_URL once fixes them together.

robots.txt is an endpoint rather than a file in public/ precisely so its Sitemap: line resolves against the real domain instead of drifting from it. It carries one Disallow, for /admin/: the CMS routes are auth-guarded, absent from the sitemap and linked from no public page, and there is nothing in them worth a crawler’s budget.

llms.txt is a curated content map, and the split inside it is worth understanding if you extend it. The core pages are hand-written, because which pages matter is an editorial judgement. The journal and the work archive are generated from their collections, because “every note” and “every project” are facts about a collection and a hand-kept list of them goes stale the first time someone adds a folder. Services stays hand-shaped, because those four entries are editorial content that happens to live in config. The ceiling is stated in the file itself: a fifth route family means adding a line there.

The sitemap lists 31 URLs — everything indexable, including /contact/. Its filter excludes /admin/, /examples/ and /404. The /admin/ clause is not redundant: the sitemap integration knows a non-dynamic route’s URL whether or not it prerenders, so without it every admin screen, sign-in included, would be enumerated.

The CMS routes

Thirteen pages and two API endpoints under /admin/, all on demand:

/admin/ is the overview. /admin/<collection>/ lists a collection’s entries and /admin/editor/<collection>/<slug>/ is the document editor. /admin/images/ is the image library, /admin/pages/ and /admin/pages/<page>/ are the page composer, /admin/review/ is the publish queue, /admin/settings/ and /admin/team/ are the two admin screens. /admin/signin/, /admin/signup/, /admin/verify/<token>/ and /admin/invite/<token>/ are auth. /admin/api/signin and /admin/api/asset are the two endpoints.

They are mounted by adminPackage in astro.config.mjs — the file router only ever looks at srcDir/pages, so a package that owns routes hands them over through integration hooks. The route table is read from disk rather than listed by hand, which costs one thing worth knowing: a page added under src/admin/pages/ while astro dev is running answers 404 until you restart the server. Editing an existing one still hot-reloads normally.

trailingSlash is "ignore" for these routes’ sake. The admin serves three URL shapes Astro will not answer under "always": the sign-in endpoint the form POSTs, the image store’s query form, and the emailed invite and verify links whose last segment is a token. Under "always" all three 404, and the sign-in one is invisible — the auth form renders every non-ok response as “Email or password is incorrect”, so a routing 404 reaches the person as a credentials error that no password will ever fix.

The canonical URL shape did not change with that setting. SiteHref in src/config/types/configDataTypes.ts still types every internal href as trailing-slashed, the directory build still emits that shape, and canonical and OG still agree on it. "ignore" only stops the server 404-ing the other form.

Adding a page

Create the file in src/pages/, add export const prerender = true, own BaseLayout and its SEO props, and compose sections. That is the whole pattern — a route is a shell, not a place where markup accumulates.

If the page should be editable in the CMS, the composer needs to be able to read its sections’ props. That means a section states them as a self-contained interface Props, and any prop the walker cannot read gets declared in src/admin.config.ts’s overrides. See Composing pages for what the composer does with them, and Configuration for the override shape.

Two more things follow a new route family: add it to llms.txt if it deserves a line, and check whether the sitemap filter should exclude it.

NEXT STEPColors & Theming