# Theme development
This guide is for the **agency developer**. You own React layout, section components, and `defineSection` contracts. Site owners must not edit `theme/**` unless you explicitly ask them to.
## Theme entry
`theme/index.tsx` must export:
| Export | Required | Purpose |
|---|---|---|
| `Layout` | yes | Wraps every page (nav, footer, shell) |
| `sections` | preferred | Array of `defineSection` contracts |
| `stylesheet` | optional | CSS file relative to the theme dir (copied into `dist/`) |
```ts
import { Layout } from "./Layout.js";
import { cta } from "./sections/cta.js";
import { faq } from "./sections/faq.js";
import { features } from "./sections/features.js";
import { hero } from "./sections/hero.js";
export { Layout };
/** Section contracts — source of truth for validation + generated agent docs */
export const sections = [hero, features, faq, cta];
export const stylesheet = "styles.css";
```
Legacy themes may export a bare `registry` map instead of `sections`. Prefer `sections` via `defineSection` so sync can generate rich agent docs.
## Layout
`Layout` receives site data, the current page, and rendered section children:
```tsx
import type { ReactNode } from "react";
import type { PageData, SiteData } from "aftercare";
type LayoutProps = {
site: SiteData;
page: PageData;
children: ReactNode;
};
export function Layout({ site, page, children }: LayoutProps) {
return (
{/* brand + site.nav */}{children}
);
}
```
Build-time rendering uses React SSR (`renderToStaticMarkup`). Keep components free of browser-only APIs; output is static HTML. For interactivity, use co-located `*.client.js` or site plugins — see [Plugins](./plugins.md).
## Client scripts
Co-locate next to a component:
```
theme/components/Gallery.tsx
theme/components/Gallery.client.js
theme/components/Gallery.client.css
```
Build discovers these files, copies them to `dist/assets/aftercare/`, and injects them on every page. Bind behavior with `data-*` attributes in the React markup (e.g. `data-aftercare-gallery`).
Site-level features (search, cart, forms, analytics) belong in `aftercare.config.ts` plugins, not as theme clients.
## defineSection
```ts
import { z } from "zod";
import { defineSection } from "aftercare";
import { Hero } from "../components/Hero.js";
export const hero = defineSection({
type: "hero",
description: "Opening band with headline and optional CTA button",
guidance: "Use once near the top of a page. Keep heading under ~12 words.",
props: z.object({
heading: z.string().min(1),
subheading: z.string().optional(),
ctaLabel: z.string().optional(),
ctaHref: z.string().optional(),
}),
component: Hero,
example: {
type: "hero",
heading: "Ship the site. Skip the retainer.",
subheading: "Locked themes. Content updates via an LLM.",
ctaLabel: "See the work",
ctaHref: "/work",
},
});
```
### Checklist
| Field | Notes |
|---|---|
| `type` | Stable string. Content files depend on it—renaming breaks existing pages. |
| `description` | One line; shown in `AGENTS.md` and the catalog. |
| `props` | Zod schema for section props **excluding** `type`. |
| `component` | React component that accepts those props. |
| `example` | Minimal valid section; `example.type` must equal `type`. |
| `guidance` | Optional longer instructions for site-owner agents. |
## Content-driven images
Declare image props with the framework helper so validation and `doctor` can check assets:
```ts
import { z } from "zod";
import { defineSection, image, imageProps } from "aftercare";
props: z.object({
heading: z.string(),
image: image(), // requires non-empty alt by default
})
```
In the component:
```tsx
import { imageProps, type ContentImage } from "aftercare";
export function Portrait({ image }: { image: ContentImage }) {
return ;
}
```
Content references paths like `/images/team.webp`. The file lives at `public/images/team.webp`. Absolute `http(s)` URLs are allowed and are not checked on disk.
`image({ altRequired: false })` relaxes alt to an optional/default empty string—prefer real alt text.
## Developer loop
Keep watch running while you build:
```powershell
npx aftercare watch
```
On changes under `theme/`, `content/`, or `public/`, watch:
1. Syncs the agent surface
2. Validates contracts and content
3. Rebuilds static HTML (pass `--no-build` to skip the HTML step)
One-shot sync:
```powershell
npx aftercare sync
```
### What sync writes
| Path | Purpose |
|---|---|
| `AGENTS.md` | Site-owner agent guide |
| `agent/catalog.toon` | Compact section catalog |
| `agent/schemas/*.schema.json` | Per-section JSON Schema |
| `agent/README.md` | Explains the `agent/` folder |
| `.cursor/skills/aftercare-content/` | Owner skill + validate/build/doctor scripts |
| `.cursor/skills/aftercare-theme/` | Developer skill |
**Do not hand-edit generated agent files.** Change contracts, then re-run sync/watch.
## Styling
Point `stylesheet` at a CSS file in the theme directory. Build copies it to `dist/` and links it from every page. Put fonts, images, and other static files in `public/`.
## Handing off
1. Finish sections and sample content.
2. Run `npx aftercare doctor` and fix errors.
3. Run `npx aftercare sync` (or rely on watch) so `AGENTS.md` is current.
4. Tell the site owner (or their agent) to edit only `content/**` and follow `AGENTS.md`.
## Next
- [Content](./content.md) — what owners edit
- [Plugins & client scripts](./plugins.md)
- [CLI reference](./cli.md)
- [Deploy](./deploy.md)