Skip to content
AstroCraft Docs
On this theme

Routing

pnpm build emits 109 HTML files from 25 route files. Everything is prerendered — there is no adapter and no on-demand route — and trailingSlash: "always" means every page is a directory: /used-cars/index.html.

Route File Pages
/ pages/index.astro 1 — ten sections from homeData
/used-cars/, /new-cars/, /electric-cars/ one file each 3 — the browse hubs
/cars/<ref>/ pages/cars/[ref].astro 69 — one per record, across three datasets
/reviews/ pages/reviews.astro 1 — the verdict shelf
/blog/, /blog/<slug>/ pages/blog/index.astro, [slug].astro 11 — index + 10 posts
/collections/best-used-automatics-under-30k/ one file 1
/specialists/classic-restoration/, …/london/ two files 2
/search/, /sign-in/, /saved/, /value-my-car/, /submit/, /advertise/ one file each 6 — the application shells
twelve info pages pages/[info].astro 12 — keys of infoData
/privacy/, /terms/ two files 2 — from legalData
/404 pages/404.astro 1
/examples/ui pages/examples/[catalog].astro 0 in production, 1 in astro dev

Two of those rows do the heavy lifting. [ref].astro merges carRecords, newCarRecords and electricCarRecords in one getStaticPaths and keys on the lowercased ref, which is why refs have to be unique across all three datasets. [info].astro enumerates infoData’s keys, so an information page is an object in config rather than a file in pages/.

The dev-only catalog

/examples/ui is gated in getStaticPaths rather than by a runtime guard:

export function getStaticPaths() {
  return import.meta.env.PROD ? [] : [{ params: { catalog: "ui" } }];
}

No paths in a production build means no HTML ships, with no adapter needed to enforce it. It is also noindex and excluded from the sitemap, belt and braces. Its only remaining cost is the CSS Tailwind compiles from its markup, which Deployment measures.

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 — 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 allows everything; there is nothing in a static marketplace worth hiding from a crawler.

llms.txt is a curated content map in the llmstxt.org shape: the core pages, the index hubs, then the blog and its feed. It is hand-shaped on purpose and says so — which pages matter is an editorial judgement, not a fact about the file tree. Its ceiling is stated in a ponytail: note: a new route family means adding a line.

rss.xml is hand-rolled from the blog collection. sitemap-index.xml comes from @astrojs/sitemap, with a filter that drops the four non-indexable routes.

Built but not indexable

Three pages ship <meta name="robots" content="noindex, nofollow"> and are excluded from the sitemap: /sign-in/, /saved/ and the 404.

That is deliberate rather than defensive. Both account pages are static demonstrations of screens a real deployment would put behind authentication — /sign-in/ stores an email in localStorage and hands off to /saved/, which reads the browser’s own favourites. There is no account system, so indexing either one would put a fake sign-in page in a search result. The sitemap filter and the meta tag agree, which matters: a sitemap entry for a noindex page is a contradiction a crawler reports as an error.

sitemap({
  filter: (page) =>
    !page.includes("/examples/") &&
    !page.includes("/404/") &&
    !page.includes("/sign-in/") &&
    !page.includes("/saved/"),
}),

106 URLs survive that filter, which is 109 pages minus those three.

Adding a page

Create the file in src/pages/, own BaseLayout and its SEO props, and compose sections:

---
import BaseLayout from "@layouts/BaseLayout.astro";
import SiteHeader from "@components/Sections/Global/SiteHeader.astro";
import SiteFooter from "@components/Sections/Global/SiteFooter.astro";
import MySection from "@components/Sections/MyPage/MySection.astro";
import myData from "@config/myData.json";
---

<BaseLayout title={`…`} description={myData.description}>
  <SiteHeader slot="header" />
  <MySection data={myData.section} />
  <SiteFooter slot="footer" />
</BaseLayout>

The header and footer are named slots, not automatic — BaseLayout is chrome-free, so a page that does not fill them renders without navigation. The 404 does that on purpose: it is a full-height centred panel with one link home.

Fourteen pages currently do it by omission. [info].astro, privacy.astro and terms.astro compose only their article section, so the twelve information pages plus Terms and Privacy build with no site header, no footer and no internal links at all — they are in the sitemap and linked from every other page’s footer, but they are exits rather than stops. If you are shipping this site, adding the two slotted sections to those three route files is the smallest fix:

<SiteHeader slot="header" />
<LegalArticle page={page} />
<SiteFooter slot="footer" />

Three things follow a new route family: content in a src/config/ module rather than in the markup, a line in llms.txt if it deserves one, and a look at whether the sitemap filter should exclude it. If it is a page the footer links to, footerLinks.test.ts will hold you to actually building it.

NEXT STEPColors & Theming