Skip to content

Working in the code

Everything in your project is yours to change. This page shows where things are, and the few rules the templates follow so the site stays at Lighthouse 100, readable in both themes, and editable in the CMS. The same rules are written for coding assistants in AGENTS.md, in the root of your project.

Terminal window
npm run dev # the site and its content editor (/keystatic)
npm run build # the production build; stops on content that doesn't fit the model or a brand color below WCAG AA
npm run check # type check (astro check)
npm run lint # lint (Biome)

Templates with a map also have npm run map (see your template’s page). In the EmDash variant: npm run deploy and npm run emdash:schema.

Handy as the example; every template has the same layout, with its own components.

Path What’s in it
src/pages/ Routes: each a few lines that render a view. src/pages/[locale]/ repeats them for other languages. src/pages/api/ holds the server routes (forms, sign-ups, checkout, chat).
src/views/ The pages themselves (Home, Services, Service, Prices, Contact…)
src/layouts/Base.astro Every page’s shell: head, header, footer, phone tab bar, chat, search
src/components/ Components; each documents its props in a comment
src/components/sections/ The home page’s sections
src/components/Sections.astro Picks the component for each section type
src/content/model.ts The content model: every field of every page and collection
src/content/cms.ts Which pages and collections exist and where their files are
src/content/ The content itself, one folder per language (singletons/en/, services/en/…)
src/content/singletons/brand.json Colors, fonts, corners
src/lib/content.ts The one way pages read content
src/lib/brand.ts Turns the brand into colors for both themes, checked for contrast
src/i18n/locales.json The site’s languages
src/styles/global.css The Tailwind theme and the template’s look
src/emails/ Email templates
src/assets/photos/ Photos
astro.config.mjs Your site’s address, fonts, environment variables
.env.example Every setting for email, payments, analytics, chat, search and the CMS
.wptom/ What wptom update needs; don’t edit it

Pages and components never import astro:content. Content can come from files, Sanity or EmDash, and src/lib/content.ts hides the difference:

  • Astro.locals.content holds the site settings (site), interface texts (ui), the home page (home), the single pages (pages.prices, pages.contact…) and the page’s locale. It’s loaded for every page by src/middleware.ts, in the page’s language.
  • For collections: listEntries('services', locale) and findEntry('journal', slug, locale) (or the helpers in src/lib/collections.ts, such as publishedServices(locale), which leave out drafts and sort). <Body entry={entry} /> renders an entry’s Markdown.
---
import { listEntries } from '../lib/content';
const { site, ui, locale } = Astro.locals.content;
const services = await listEntries('services', locale);
---

Stick to this and your code keeps working if you move the content to Sanity later.

No words written into components: button labels, messages and headings are fields, so they can be edited in the CMS and translated. Short labels shared across the site live in Interface texts (ui). Scripts that run in the browser get their texts from a data-messages attribute the component fills in.

Texts can contain {placeholders}, filled with fill() from src/lib/content.ts: fill(ui.journal.minutes, { n: 4 }) turns “{n} min read” into “4 min read”.

A new field goes into src/content/model.ts (with .meta({ title }) for the editor), then into the content in every language, then into the page. Groups of optional fields are objects with empty defaults, not .optional() objects, so every CMS stores them the same way. Details: The content model.

Internal links in components use localize(href, locale) from src/lib/i18n.ts, which adds the language prefix where needed. Never write /de/ into a link.

Below 760 px wide, navigation lives in the bottom tab bar and its Menu. Keep it that way when you change the header.

The templates use Tailwind CSS 4. The theme is at the top of src/styles/global.css (@theme), where each color is a variable from the brand:

Class Use
bg-bg, bg-panel, bg-sunk The page, cards and panels, sunken areas
text-text, text-muted Text and secondary text
text-link Text in the accent’s hue (checked for contrast; use it rather than text-accent for words)
bg-button + text-on-button Buttons
bg-second, bg-third + text-on-fill Colored fills and the text on them
border-border, border-rule Strong and quiet lines
font-sans, font-display The text and heading fonts
rounded-brand, rounded-button The corners from the brand

Each template has its own set (Handy also has ink, paper, night…): AGENTS.md lists them under “Colors only from the theme”.

Two rules keep both themes readable:

  • Never a hex color in a component. Use the theme’s classes, or var(--brand-…) in CSS. These change with the theme and were checked for contrast; a fixed color wasn’t.
  • Check both themes with the toggle after any visual change.

Template-wide classes (.btn, .panel, .eyebrow, .label and the template’s own) are defined in global.css. Reuse them rather than rebuilding a button from utilities.

Every template scores 100 in Lighthouse’s four categories on phone and desktop. What keeps it there:

Photos go through the <Photo> component (src/components/Photo.astro), never a bare <img>. It serves AVIF and WebP in the sizes each screen needs, with the width and height set so nothing jumps, and it works the same for local photos, Sanity and EmDash:

<Photo src={entry.data.image} alt={entry.data.imageAlt} widths={[360, 540, 720]} sizes="(min-width: 64rem) 22rem, 100vw" />
  • The first large image on a page gets loading="eager" fetchpriority="high"; every other image stays lazy (the default).
  • alpha keeps transparency for cut-outs.
  • The default quality is in src/data/images.ts, with the widths and sizes the template’s images use. Reuse those.
  • Upload large originals (2000 px wide or more); the build makes the smaller versions.

Fonts load only through the Astro Fonts API, set in astro.config.mjs from brand.json (Fonts). Don’t add a <link> to a font service or @font-face rules: they’d slow the first paint and send visitors’ requests to a third party.

Pages ship no JavaScript unless a feature needs it, and then a small script that starts after the page has loaded or when the feature is used (the booking calendar loads on click, and the search index downloads the first time search is opened). Prefer CSS for effects: the templates’ scroll animations are CSS.

Sections that aren’t on the first screen get the below-fold class, with a rough height:

<section class="below-fold [contain-intrinsic-size:auto_600px]">…</section>

The browser then skips their work until they’re scrolled near.

  • Every animation stops with prefers-reduced-motion: reduce.
  • One h1 per page, labels on every control, alt text on every image.
  • Every page passes a title and a description to <Base>.

To check, measure the built site, not npm run dev (the dev server isn’t optimized): open a deployment of your branch (on Vercel, pushing a branch other than main gives it a preview address) and run Lighthouse in Chrome’s DevTools, in a private window so extensions don’t lower the score.

npx wptom update brings in a new template version and merges it with your edits, file by file: files you didn’t touch are replaced, files you changed are merged line by line, and where you both changed the same lines you get conflict markers to resolve. Commit before you update. Details: wptom update and Keep it up to date.

What helps:

  • Prefer adding over rewriting: a new component, view or section merges cleanly; a rewritten template file means more conflicts later.
  • Keep content changes in the content, not in components.
  • Don’t edit .wptom/.

The project is set up for coding assistants:

  • AGENTS.md describes the project: commands, where things are, how content is read, the look and the rules on this page. Claude Code reads it through CLAUDE.md, Cursor through .cursor/rules/. Other assistants: point them at it.
  • npx -y wptom mcp is an MCP server your assistant can ask about your template, its version, its components and props, and these docs. It only reads, never changes files. Setup for each editor: wptom mcp.

Ask for changes in plain words (“add a testimonials section to the home page”) and review the result in both themes and on a phone width before you commit.