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 and its SEO, composes sections, and holds no markup of its own. Everything visible is in src/components/Sections/.

The route table

Every route below is prerendered to static HTML except /contact/.

  • /src/pages/index.astro. Composes hero, about, clients, work grid, services, process, FAQ and CTA. The only route that uses siteData.title and siteData.description verbatim.
  • /about/ — hero, career experience, testimonial, CTA.
  • /work/ — hero, project list, CTA. The project list reads the collection itself.
  • /work/<slug>/ — hero, chapters, testimonial, CTA. Emits a CreativeWork JSON-LD node.
  • /blog/ — hero, featured post, article grid, CTA.
  • /blog/<slug>/ — hero, article body with contents, related posts, CTA. Emits BlogPosting and the article Open Graph block.
  • /contact/ — hero and form. prerender = false. No CTA — a closing invitation that links to itself is a loop.
  • /terms/ and /privacy/ — one shared LegalArticle section, driven by legalData.
  • /404 — the not-found page. Sets noindex.
  • /examples/ui/ — the primitive catalog. Development only, noindex, emits no pages in a production build.
  • /rss.xml, /robots.txt, /llms.txt — endpoints, prerendered to files.

trailingSlash is "always" and the build format is directory, so every page URL ends in a slash and canonical, Open Graph and sitemap entries all agree on that shape.

What a route owns

Route concerns only:

  • BaseLayout with a unique title and description. These are the two tags that still move rankings; never reuse the site defaults on a subpage.
  • noindex, when the page should not be indexed.
  • schema and article, when the page earns structured data.
  • Any data lookup the SEO tags need.
  • Any list-splitting two sections would otherwise disagree about — the blog index’s featured-versus-grid split is the example.
  • export const prerender = false;, if the page needs a request.

No route in the theme contains a bare HTML element except blog/[slug].astro, which wraps its two sections in an <article>.

Layouts

BaseLayout is the shell. It takes the same six props as BaseHead and forwards all of them verbatim:

interface Props extends SeoProps {
  title: string;
  description: string;
  image?: ImageMetadata;
  noindex?: boolean;
}

where SeoProps adds schema?: JsonLdNode[] and article?: ArticleMeta. That pass-through is the entire “how does a page set its meta tags” story — nothing else in the codebase mounts BaseHead.

What it renders, in order: the <html> element with lang from siteSettings, the head, a skip link, a header slot, <main id="main" tabindex="-1"> holding the default slot, a footer slot, and the page-transition curtain.

The header and footer are slot fallbacks, not fixed markup:

<slot name="header"><Header /></slot>
<main id="main" tabindex="-1"><slot /></main>
<slot name="footer"><Footer /></slot>

So every route gets site chrome for free, and any route can replace it by passing slot="header" or slot="footer". None currently does.

main carries tabindex="-1" so the skip link moves real focus rather than only scrolling.

Adding a page

Say you want /services/.

1. Create the route. src/pages/services.astro — or src/pages/services/index.astro if it will have children later, which is why blog/ and work/ are folders.

---
import Hero from "@components/Sections/Services/Hero.astro";
import Pricing from "@components/Sections/Services/Pricing.astro";
import Cta from "@components/Sections/Global/Cta.astro";
import siteData from "@config/siteData.json";
import BaseLayout from "@layouts/BaseLayout.astro";

const title = `Services — ${siteData.name}`;
const description = "What I do, how engagements work, and what they cost.";
---

<BaseLayout title={title} description={description}>
  <Hero />
  <Pricing />
  <Cta />
</BaseLayout>

2. Create the sections. src/components/Sections/Services/Hero.astro and Pricing.astro. Never import BaseLayout from a section.

3. Add the copy to pageCopy.json.ts if the page’s framing text should be editable, and type the new key in src/config/types/configDataTypes.ts.

4. Add the link to navData.json.ts{ href: "/services/", label: "Services" } in main for the header and mobile drawer, or in footer for the utility row. The trailing slash is required.

5. Optionally add it to src/pages/llms.txt.ts. That core-page list is hand-curated on purpose, so a genuinely important page has to be added deliberately.

Nothing needs doing for the sitemap — it enumerates automatically. The exception is a noindex page, which must also be excluded from the sitemap filter in astro.config.mjs, or the two will contradict each other.

Writing a section

Six habits keep a new section consistent with the shipped ones.

  • Pick one data source. Typed props from the route, or a direct @config/* import — never both for the same value.
  • Build from primitives and cards in @components/ui/* and @components/Cards/*. Tokens only, no raw Tailwind colours.
  • Use .page-frame for the shared gutters and py-24 md:py-32 for the standard vertical rhythm.
  • Give the <section> an accessible name. aria-labelledby pointing at the heading, or an sr-only <h2> if the design has no visible one.
  • Take motion delays from the shared scale. <Reveal delay={200}> — off-scale values are type errors, which is the point.
  • Store a per-item delay on the item, not in a parallel array indexed alongside it.

And promote the section to Sections/Global/ the moment a second page uses it. That is what Cta.astro and Testimonial.astro did.

The noindex pattern

404.astro is the model. The route owns the copy, and the same two constants feed both the meta tags and the section’s props, so visible text and SEO tags cannot drift:

const title = "Page not found";
const description = "Sorry, the page you're looking for doesn't exist or has moved.";
---
<BaseLayout title={`${title} — ${siteData.name}`} description={description} noindex={true}>
  <NotFound title={title} description={description} />
</BaseLayout>

noindex emits <meta name="robots" content="noindex, nofollow" />. Two routes set it, and both are excluded from the sitemap filter.

The dev-only catalog gate

/examples/ui/ exists under astro dev and nowhere else, using a pattern worth stealing for any development-only route:

export const getStaticPaths = (() => {
  return import.meta.env.PROD ? [] : [{ params: { catalog: "ui" } }];
}) satisfies GetStaticPaths;

No paths in a production build means no HTML ships, with no adapter or middleware involved.

Site chrome

The header and footer live in Sections/Global/ and are worth knowing about because you will probably modify them.

The header is fixed and hides on scroll down, revealing on scroll up. The rule is a pure function with its own test:

export const SCROLL_THRESHOLD = 8;   // swallows trackpad jitter
export const REVEAL_ABOVE = 80;      // the top of the page never opens hidden

export function nextHeaderState(state: HeaderScrollState, y: number): HeaderScrollState {
  const top = Math.max(0, y);        // clamp iOS rubber-band overscroll
  const delta = top - state.lastY;
  if (Math.abs(delta) < SCROLL_THRESHOLD) return state;
  return { hidden: delta > 0 && top > REVEAL_ABOVE, lastY: top };
}

The current page is marked with startsWith, not equality, so /blog/a-post/ still highlights Blog. Below md the nav becomes a Sheet drawer — a native modal <dialog>, so the focus trap, inertness and Escape handling are free. The contact pill stays in the bar at every width, which is why the drawer holds only the three nav links.

The footer carries the email, the tagline, the social list, an oversized brand mark, and a live local clock. The clock’s time zone comes from siteData.author.timeZone and updates on a single 30-second interval that sweeps every [data-local-time] element — one timer for the page, not one per element, so view transitions cannot stack them.

Every link in both is a NavLink with the glitch variant, which is where the theme’s hover vocabulary is defined — once, in src/components/ui/nav/NavLink.astro.

NEXT STEPColors & Theming