Skip to content
AstroCraft Docs
On this theme

Pages & Routing

Routes live in src/pages/ and are deliberately thin. A route owns its layout wrapper, its SEO, and any data lookup those two need. It composes sections and holds no markup of its own. Everything visible lives in src/components/Sections/.

The build emits 28 HTML pages plus four generated text endpoints, all static.

Every route

Route File Notes
/ index.astro Hero, Why, Process, Benefits, Testimonials, Pricing, FAQ, CTA
/features/ features.astro Hero, three alternating feature rows, audience cards, stack, pricing, CTA
/pricing/ pricing.astro Plans, comparison matrix, pricing FAQ, CTA
/about/ about.astro Page hero, two story blocks, values, team, CTA
/contact/ contact.astro Page hero, contact board, contact FAQ — no CTA, by design
/blog/ blog/index.astro Featured card, filter pills, post grid
/blog/<id>/ blog/[id].astro 7 pages — hero, TOC + article, related, CTA
/integrations/ integrations/index.astro Search, featured row, filterable grid
/integrations/<id>/ integrations/[id].astro 9 pages — hero, highlight, setup, CTA
/signin/ signin.astro Chrome-free auth shell, noindex
/signup/ signup.astro Chrome-free auth shell, noindex
/terms/ terms.astro Renders legalData.terms
/privacy/ privacy.astro Renders legalData.privacy
/404/ 404.astro noindex, served for any unmatched route
/examples/ui/ examples/[catalog].astro Dev-only; emits no paths in a production build
/rss.xml rss.xml.ts Hand-built RSS 2.0
/robots.txt robots.txt.ts Dynamic endpoint, prerendered
/llms.txt llms.txt.ts Dynamic endpoint, prerendered
/sitemap-index.xml @astrojs/sitemap Filtered — see below

Trailing slashes are enforced by trailingSlash: "always" in astro.config.mjs, which agrees with the directory build, the canonical tags, the RSS item links and every internal href in navData.

What a thin route owns

Read src/pages/index.astro — it is thirty lines, eleven of them imports:

---
import Hero from "@components/Sections/Home/Hero.astro";
// …the other sections…
import { faqsFor } from "@config/faqData.json";
import siteData from "@config/siteData.json";
import BaseLayout from "@layouts/BaseLayout.astro";
---

<BaseLayout title={siteData.title} description={siteData.description}>
  <Navbar slot="header" />
  <Hero />
  <Why />
  <Process />
  <Benefits />
  <Testimonials />
  <Pricing />
  <Faq items={faqsFor("home")} />
  <Cta />
  <Footer slot="footer" />
</BaseLayout>

A route owns four things and nothing else:

  1. BaseLayout with a real title and description. Never a placeholder — these are the <title>, the meta description, the OG title and the Twitter card.
  2. SEO extras where they apply: noindex, image, schema (page-specific JSON-LD nodes), and article (which flips og:type and emits the article:* meta).
  3. Data lookup the SEO tags need. blog/[id].astro resolves the author because the JSON-LD needs the name; that is a route concern.
  4. Which sections to compose, in what order.

A route holds no markup. If you find yourself writing a <div> in a page file, that markup belongs in a section.

BaseLayout is chrome-free by default. Page chrome is opt-in through two named slots:

<Navbar slot="header" />
<Footer slot="footer" />

The reason is landmark correctness. Rendering them into the slots lands <header> and <footer> as siblings of <main>, giving you a real banner and a real contentinfo landmark. Putting them inside the default slot would nest both inside <main>, which is wrong for assistive technology and passes every visual check.

The two auth pages omit both slots entirely — that is what “chrome-free by design” means for /signin/ and /signup/.

The navbar’s one prop

Navbar takes onDark, defaulting to true. At rest the bar is transparent with white text, because most pages open on a dark hero panel. Pages whose hero is light pass onDark={false} so the resting text is the theme foreground instead — /pricing/, /blog/<id>/ and both integrations routes do.

Past a small scroll threshold the bar condenses into a blurred pill in the page background colour, which is theme-aware in both modes. The scrolled state and the no-JS state are safe on any background, so onDark only affects the at-rest appearance.

Dynamic routes

Both dynamic routes follow the same shape: read the collection through its helper module, map entries to params and props.

export async function getStaticPaths() {
  const posts = await getPublishedPosts();
  return posts.map((post) => ({ params: { id: post.id }, props: { post } }));
}

Passing the entry through props rather than re-fetching it by id inside the page is the pattern to copy — it is one lookup instead of two, and the prop is typed.

Note the param is id, matching the collection entry id, and the file is [id].astro. Astro 5’s collection API dropped slug in favour of id; keeping the param name aligned with the field avoids a translation step nobody needs.

The dev-only catalog route

src/pages/examples/[catalog].astro gates itself with import.meta.env.PROD inside getStaticPaths:

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

No paths in production means no HTML ships. astro dev still serves /examples/ui/. This is the pattern for any route you want available while building but absent from the deployed site — it needs no adapter and no runtime guard.

noindex and the sitemap

Four routes set noindex in their markup: /404/, /examples/ui/, /signin/ and /signup/. The auth pages are noindexed because an auth form has no search value, and their panel copy repeats the homepage hero verbatim — indexing them would hand a search engine a duplicate of the H1 that actually ranks.

The sitemap integration carries a matching filter in astro.config.mjs:

sitemap({
  filter: (page) => !["/examples/", "/404/", "/signin/", "/signup/"].some((p) => page.includes(p)),
});

Keep the two in step. A page that sets noindex but appears in the sitemap is telling a crawler two different things. If you add a noindexed route, add it to the filter in the same commit.

The pages stay crawlable — robots.txt allows everything — so the directive is actually seen rather than hidden behind a Disallow.

Adding a page

  1. Create the route at src/pages/<name>.astro with BaseLayout, a real title and description, and the two chrome slots if the page should have them.
  2. Create its sections under src/components/Sections/<Name>/. If a block you need already exists in Global/, use it — PageHero, Faq, Cta, SectionHeader, IconCards and Pricing are all designed to be reused, and every shipped page except the home page reuses at least one.
  3. Add it to navData.header. The header entries are the site’s route inventory, and the file states the convention: adding a page to the site means adding a line here. Skip it and your page is unreachable except by URL.
  4. Add a sitemap filter entry if the page is noindex.
  5. Run pnpm build. It is the check that catches a broken import, a bad content reference or a route that accidentally depends on request-time behaviour.

If your page needs a section that only differs from an existing one by a flag or two, prefer the flag — Story is rendered twice on /about/ with a reverse prop, and Faq takes a different subset per page. If the layout tree genuinely diverges, write a sibling section instead. That is the same call the Cards folder documents for FeaturedPostCard versus BlogCard.

Making one route server-rendered

The theme has no adapter, so every route is static. Adding export const prerender = false; to a page without installing an adapter fails the build with ActionsWithoutServerOutputError rather than silently degrading — which is the intended guardrail.

The one place this is set up and waiting is src/pages/contact.astro, where the line ships commented out. See Contact Form for the four steps, and note that turning that route on-demand leaves the other 27 static.

NEXT STEPColors & Theming