Lesson 3 · Core course

Build the site shell

a local Astro site with a homepage, About page, navigation, and a missing-page message.

Sign in to save progress across devices

On this page

We will build from an empty directory so you know what every file does. Astro also has a setup wizard; use this manual path for the course rather than generating a second starter inside it.

1. Create the project

From the blog-course folder created earlier:

mkdir my-blog
cd my-blog

Open my-blog in your editor. Create the following files. The versions are pinned for this course edition; update them as a deliberate change later.

File: package.json

{
  "name": "my-blog",
  "version": "0.0.1",
  "private": true,
  "type": "module",
  "packageManager": "pnpm@11.19.0",
  "engines": { "node": ">=24 <25" },
  "scripts": {
    "dev": "astro dev",
    "build": "astro build",
    "preview": "astro preview",
    "check": "astro check",
    "astro": "astro"
  },
  "dependencies": {
    "astro": "7.3.3"
  },
  "devDependencies": {
    "@astrojs/check": "0.9.10",
    "typescript": "6.0.3"
  }
}

private: true prevents accidental npm package publication. It does not set GitHub repository visibility.

File: .node-version

24

File: tsconfig.json

{
  "extends": "astro/tsconfigs/strict"
}

File: .gitignore

node_modules/
dist/
.astro/
.wrangler/
.env
.env.*
!.env.example
.dev.vars
.dev.vars.*
.local/
backups/
*.sql.backup
*.log
.DS_Store

Create pnpm-workspace.yaml to approve the build steps required by these tools. This file also applies to a single-package project; it is not asking you to create a multi-project repository. The workerd approval is used when Wrangler is added in Lesson 06.

File: pnpm-workspace.yaml

allowBuilds:
  esbuild: true
  sharp: true
  workerd: true

pnpm blocks unapproved dependency build scripts. These named approvals permit the tools’ native binary setup without allowing every dependency to run installation scripts. Review new packages deliberately rather than enabling all builds. See pnpm build settings.

Install the dependencies:

pnpm install

pnpm creates pnpm-lock.yaml. Keep this file; it records the resolved dependencies so local and hosted builds can use the same versions.

2. Configure the site

Use your intended canonical domain in site. In the examples it is the reserved placeholder https://example.com; replace it with your own before deployment. Local preview still runs on localhost.

File: astro.config.mjs

import { defineConfig } from 'astro/config';

export default defineConfig({
  site: 'https://example.com',
  output: 'static',
  trailingSlash: 'always',
});

File: src/consts.ts

export const SITE_TITLE = 'Field Notes';
export const SITE_DESCRIPTION = 'Essays, questions, and things I am learning.';

3. Create a shared page layout

Create src/layouts/Page.astro. The section between --- lines runs during rendering. The HTML below it becomes the page. <slot /> is where each page’s own content appears. This is an Astro component.

File: src/layouts/Page.astro

---
import '../styles/global.css';
import { SITE_TITLE, SITE_DESCRIPTION } from '../consts';

interface Props {
  title?: string;
  description?: string;
  noindex?: boolean;
}

const { title = SITE_TITLE, description = SITE_DESCRIPTION, noindex = false } = Astro.props;
const pageTitle = title === SITE_TITLE ? title : `${title} · ${SITE_TITLE}`;
const canonical = new URL(Astro.url.pathname, Astro.site);
---

<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <title>{pageTitle}</title>
    <meta name="description" content={description} />
    <link rel="canonical" href={canonical} />
    {noindex && <meta name="robots" content="noindex, nofollow" />}
  </head>
  <body>
    <a class="skip-link" href="#main">Skip to content</a>
    <header class="shell site-header">
      <a class="site-name" href="/">{SITE_TITLE}</a>
      <nav aria-label="Main navigation">
        <a href="/">Home</a>
        <a href="/about/">About</a>
      </nav>
    </header>
    <main id="main" class="shell" tabindex="-1"><slot /></main>
    <footer class="shell site-footer">Written with curiosity.</footer>
  </body>
</html>

File: src/styles/global.css

* { box-sizing: border-box; }
body { margin: 0; background: #f7f5ef; color: #282a26; font: 1.1rem/1.7 system-ui, sans-serif; }
.shell { width: min(100% - 2rem, 46rem); margin-inline: auto; }
.site-header { display: flex; flex-wrap: wrap; justify-content: space-between; align-items: center; gap: 1rem; padding-block: 1.5rem; }
.site-name { font-weight: 700; }
nav { display: flex; flex-wrap: wrap; gap: 1rem; }
main { padding-block: 2rem; min-height: 60vh; }
h1, h2, h3 { line-height: 1.2; }
a { color: #38634d; text-underline-offset: .2em; }
.site-footer { padding-block: 2rem; color: #596157; }
:focus-visible { outline: 3px solid #38634d; outline-offset: 4px; }
.skip-link { position: absolute; left: 1rem; top: -5rem; padding: .5rem; background: white; }
.skip-link:focus { top: 1rem; }

4. Create pages

Astro maps files in src/pages/ to URLs. about.astro becomes /about/; index.astro becomes /. Astro routing also supports generated article routes, which we will add next.

File: src/pages/index.astro

---
import Page from '../layouts/Page.astro';
---
<Page>
  <h1>A place to think out loud.</h1>
  <p>I write about the questions I keep returning to.</p>
  <p><a href="/about/">Meet the person behind the words.</a></p>
</Page>

File: src/pages/about.astro

---
import Page from '../layouts/Page.astro';
---
<Page title="About">
  <h1>About</h1>
  <p>Replace this paragraph with your own introduction.</p>
</Page>

File: src/pages/404.astro

---
import Page from '../layouts/Page.astro';
---
<Page title="Page not found" noindex>
  <h1>That page is missing.</h1>
  <p><a href="/">Return home.</a></p>
</Page>

5. Preview and build

pnpm dev

Open the local URL Astro prints, normally http://localhost:4321. The terminal remains occupied while the server runs. Use a second terminal for other commands, or press Ctrl+C to stop it. Astro versions supporting background mode also allow pnpm dev --background; inspect/stop that server with pnpm astro dev status and pnpm astro dev stop.

Then run:

pnpm build
pnpm check
pnpm preview

The production files are in dist/. Preview serves those built files, so it will not reflect later edits until you build again. Keep both development and preview servers on your computer; development previews will include drafts in the next lesson.

Checkpoint

The homepage and About page open, navigation works, and the build and type checks succeed. You can identify the source files and the generated dist/ output. Change the site title in src/consts.ts and confirm it appears in the browser.