# 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)