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.
Commands
Section titled “Commands”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 AAnpm 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.
Where things are
Section titled “Where things are”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 |
The rules, in plain words
Section titled “The rules, in plain words”Read content through src/lib/content.ts
Section titled “Read content through src/lib/content.ts”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.contentholds the site settings (site), interface texts (ui), the home page (home), the single pages (pages.prices,pages.contact…) and the page’slocale. It’s loaded for every page bysrc/middleware.ts, in the page’s language.- For collections:
listEntries('services', locale)andfindEntry('journal', slug, locale)(or the helpers insrc/lib/collections.ts, such aspublishedServices(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.
All visible text comes from the content
Section titled “All visible text comes from the content”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”.
Add a field in the model first
Section titled “Add a field in the model first”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.
Links go through localize()
Section titled “Links go through localize()”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.
Phones have no top header
Section titled “Phones have no top header”Below 760 px wide, navigation lives in the bottom tab bar and its Menu. Keep it that way when you change the header.
Styles
Section titled “Styles”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.
Keeping Lighthouse 100
Section titled “Keeping Lighthouse 100”Every template scores 100 in Lighthouse’s four categories on phone and desktop. What keeps it there:
Images
Section titled “Images”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). alphakeeps 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.
JavaScript
Section titled “JavaScript”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.
Below the fold
Section titled “Below the fold”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.
Motion and accessibility
Section titled “Motion and accessibility”- Every animation stops with
prefers-reduced-motion: reduce. - One
h1per page, labels on every control,alttext 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.
Updates keep your changes
Section titled “Updates keep your changes”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/.
Working with an AI assistant
Section titled “Working with an AI assistant”The project is set up for coding assistants:
AGENTS.mddescribes the project: commands, where things are, how content is read, the look and the rules on this page. Claude Code reads it throughCLAUDE.md, Cursor through.cursor/rules/. Other assistants: point them at it.npx -y wptom mcpis 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.