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 sevenSections/Home/*blocks (Hero, Stats, LatestPosts, TechStack, FeaturedProjects, About, Contact)./about/—pages/about.astro, fiveSections/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 itslegalDatarecord throughSections/Legal/LegalArticle./404— the not-found page,noindex, renderingSections/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 aSitemap:line resolved againstsite./llms.txt— a curated markdown content map for AI crawlers./rss.xml— the blog feed./sitemap-index.xml— from@astrojs/sitemap, filtered to drop thenoindexcatalog 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
- Create the route. Add
src/pages/services.astro, importBaseLayout, and give it a realtitleanddescription. That alone is a working page at/services/. - Build its sections. For anything more than a paragraph, create
src/components/Sections/Services/<Name>.astrofiles and compose them in the route. Keep markup in the sections, not the route. - 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. - 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.