Skip to content
AstroCraft Docs
On this theme

Routing

Medice has sixteen route files under src/pages/ and they build 52 pages. Fifteen of the sixteen prerender; one is an endpoint that does not.

Every route file is a thin shell: it owns its URL, its BaseLayout props and its SEO, and delegates its markup to sections. None of them contains layout.

The routes

Route File Built from
/ index.astro Sections/Home/* + homeData
/about/ about.astro Sections/About/* + aboutData
/services/ services/index.astro all 18 specialties
/services/<slug>/ services/[service].astro the 9 with a detail
/doctors/ doctors/index.astro the roster
/doctors/<slug>/ doctors/[doctor].astro 9 clinicians
/book/ book.astro Sections/Book/* + bookData
/careers/ careers/index.astro 14 roles
/careers/<role>/ careers/[role].astro all 14
/faq/ faq.astro 26 questions
/resources/health-library/ resources/health-library/index.astro the blog collection
/resources/health-library/<post>/ resources/health-library/[post].astro 10 articles
/privacy/, /terms/ privacy.astro, terms.astro legalData
/404 404.astro Sections/NotFound/*
/examples/ui examples/[catalog].astro the dev-only catalog
POST /api/book/ api/book.ts the one on-demand route

Plus four generated endpoints: /robots.txt, /llms.txt, /rss.xml and /sitemap-index.xml.

Trailing slashes

trailingSlash: "always"

One URL shape, enforced by config. The directory build emits trailing slashes, and canonical, OG and every internal link agree on that shape.

It covers endpoints too. /api/book redirects; /api/book/ is the route. A form’s action is a URL like any other and takes the same shape as every link on the site — worth knowing if you point the booking form somewhere else.

This was loosened to "ignore" at one point, only because "always" broke a CMS’s extensionless API calls. The CMS is gone and the setting is back to strict.

Dynamic routes

Four routes use getStaticPaths, and each derives its paths from the data rather than from a list.

services/[service].astro builds from servicePages — the nine specialties that carry a detail. Because hasPage() is exported once from servicesData.json.ts, adding a tenth detail file is the only edit needed; the route, its type predicate and its guards all read the same answer.

doctors/[doctor].astro builds from the roster.

careers/[role].astro builds all fourteen roles.

resources/health-library/[post].astro builds from getPosts(), which drops drafts — so a draft has no page, and the related-articles list on a real post cannot link to one.

The generated endpoints

robots.txt is a dynamic endpoint rather than a file in public/, so its Sitemap: line resolves against site and cannot drift. It allows everything except /api/ — the one path worth disallowing, since POST /api/book/ answers a GET with nothing and a crawler that finds it spends budget on a dead end. It prerenders to a static /robots.txt.

llms.txt emits a small markdown content map for AI crawlers, built from siteData and site. It is a curated map rather than an auto-generated sitemap.

rss.xml is hand-rolled, escaped RSS 2.0 over getPosts() — no feed dependency, same stance as the rest of the SEO layer.

sitemap-index.xml comes from @astrojs/sitemap with a filter that drops the dev-only /examples/ catalog and 404s, so the sitemap never contradicts a page’s own noindex.

All four derive their absolute URLs from site, so setting SITE_URL once fixes them together.

The dev-only catalog

/examples/ui renders every UI primitive in every variant. It is noindex, excluded from the sitemap, and its getStaticPaths emits no paths in a production build — so it costs no pages.

It does cost CSS, because Tailwind still scans its markup. Deleting src/components/Sections/UiCatalog/ and src/pages/examples/ takes the shared stylesheet from 76,413 to 56,695 bytes and drops 70 unused @keyframes. Keep it while you are still picking primitives.

The thirteen planned routes

The header and footer draw a complete clinic information architecture, and thirteen of those destinations are pages only you can write:

/locations/ and its four clinic pages · /insurance/ · /pricing/
/patient-portal/ · /newsroom/ · /community/ · /accessibility/
/notice-of-privacy-practices/ · /services/urgent-care/

They are declared as PLANNED_ROUTES in src/config/navData.json.ts, and pnpm test asserts that the dead links the chrome draws are exactly that list. So a typo fails the build, and every page you build must be struck off.

Build them or remove their links from navData.json.ts — either way the check keeps you honest. Nothing else on the site 404s.

A signpost is not a listing

Two page groups take opposite decisions about incomplete destinations, and the distinction is deliberate.

The careers board publishes all fourteen roles rather than hiding six behind an off-site “see all” button, because a directory card is the thing it names — a listing that withholds its own contents cannot reach them.

The services index draws all eighteen specialties, nine of which point at routes nothing has built, because a specialty card on an index is a signpost. It honestly says the practice offers cardiology; it does not claim to be the complete record of cardiology.

If you add a route, the question to ask is which of those two it is.

Adding a route

Create the file in src/pages/, wrap the content in BaseLayout with title and description, and compose sections. If the page has a visible breadcrumb, pass the same trail to getTrailSchema so the markup and the schema describe one trail rather than two.

If the new route is one of the thirteen planned ones, remove it from PLANNED_ROUTES in the same commit — the check will fail otherwise, which is the point.

NEXT STEPColors