Skip to content
AstroCraft Docs
On this theme

Pages & Routing

Routing is file-based: a file in src/pages/ is a route. 8-BitQuest configures trailingSlash: "always", so every page URL ends in a slash, and every route prerenders to static HTML with a single exception — /contact/, which is server-rendered. Understanding what a route owns, and what it delegates, is most of what you need to add pages of your own.

Every route

Pages

  • /pages/index.astro, composed from seven Sections/Home/* blocks (Hero, Stats, LatestPosts, TechStack, FeaturedProjects, About, Contact).
  • /about/pages/about.astro, five Sections/About/* blocks (Hero, DevProfile, SkillTree, Achievements, Gear).
  • /blog/ and /blog/<slug>/ — the listing and per-post article. See Blog.
  • /projects/ and /projects/<slug>/ — the listing and per-project detail. See Projects.
  • /contact/ — the one server route. See below and Contact Form.
  • /privacy/ and /terms/ — the legal pages, each rendering its legalData record through Sections/Legal/LegalArticle.
  • /404 — the not-found page, noindex, rendering Sections/NotFound/NotFound.
  • /examples/ui — the dev-only primitive catalog. Emits no HTML in a production build.

Endpoints — these emit files rather than pages, and their absolute URLs all derive from site:

  • /robots.txt — a dynamic endpoint allowing everything, with a Sitemap: line resolved against site.
  • /llms.txt — a curated markdown content map for AI crawlers.
  • /rss.xml — the blog feed.
  • /sitemap-index.xml — from @astrojs/sitemap, filtered to drop the noindex catalog and 404, with the SSR /contact/ route added by hand.

What a thin route owns

A route’s job is small and specific: it imports BaseLayout, computes a unique title and description, does any data lookup the SEO tags need, and composes one or more sections. It holds no markup of its own. Optionally it sets noindex, passes a schema array of JSON-LD nodes, or an article block for a post. Everything visible on the page lives in the sections it composes.

src/pages/about.astro is the whole pattern:

---
import Hero from "@components/Sections/About/Hero.astro";
import DevProfile from "@components/Sections/About/DevProfile.astro";
import SkillTree from "@components/Sections/About/SkillTree.astro";
import Achievements from "@components/Sections/About/Achievements.astro";
import Gear from "@components/Sections/About/Gear.astro";
import BaseLayout from "@layouts/BaseLayout.astro";
---

<BaseLayout title="About — 8-BitQuest" description="…">
  <div class="site-container flex flex-col gap-10 py-8 md:gap-12 md:py-12">
    <Hero />
    <DevProfile />
    <SkillTree />
    <Achievements />
    <Gear />
  </div>
</BaseLayout>

The Header and Footer are not in this file — they come from BaseLayout, so every page has them without asking. See Layout & Spacing for the shared container and rhythm.

The dynamic routes

/blog/<slug>/ and /projects/<slug>/ use getStaticPaths to emit one static page per collection entry. Both key the route on entry.id, and the card mappings (toPostCard, toProjectCard) build their links off the same id — so the grid links and the generated routes can never drift apart. The glob loader produces single-segment ids, so a plain [slug] fits:

export const getStaticPaths = (async () => {
  const posts = await getSortedPosts();
  return posts.map((entry) => ({ params: { slug: entry.id }, props: { entry, posts } }));
}) satisfies GetStaticPaths;

The one server route

src/pages/contact.astro is the only page that sets export const prerender = false. It takes the Resend form POST and re-renders with the result, which is why the @astrojs/node adapter is mounted and the build splits into dist/client/ and dist/server/. It still owns BaseLayout + SEO and composes Sections/Contact/* exactly like every other route — only the rendering mode differs. See Contact Form and Deployment.

The dev-only catalog

src/pages/examples/[catalog].astro renders every primitive, icon and motion utility, and it is gated inside getStaticPaths:

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

In a production build getStaticPaths returns an empty array, so no path is emitted and no HTML ships — while astro dev still serves /examples/ui/. The page also sets noindex, so even if it were somehow reachable it would not be indexed, and the sitemap filter drops it.

Adding a page

  1. Create the route. Add src/pages/services.astro, import BaseLayout, and give it a real title and description. That alone is a working page at /services/.
  2. Build its sections. For anything more than a paragraph, create src/components/Sections/Services/<Name>.astro files and compose them in the route. Keep markup in the sections, not the route.
  3. Add it to the nav if it should be reachable from the header — one entry in src/config/navData.json.ts, with its trailing slash. See Configuration.
  4. Decide indexing. A page you do not want in search results takes noindex, which also drops it from the sitemap.

If a section you build is later used by a second page, move it to Sections/Global/ — that is the one promotion rule, and it is literal: two pages, then it moves.

NEXT STEPColors & Theming