Create a theme

Structure a project so epresso new can scaffold others from it.

A theme is not a special file format. It is a normal epresso project shaped for reuse: the same components/, layouts/, styles/, pages/ and site.toml you already write, plus two conventions — everything visual is a component, and site-specific values come from configuration, not from the theme's own files.

Use themes/docs as the reference implementation.

1. Start a project

  
bash
epresso init epresso-theme-mythemecd epresso-theme-mytheme

Name the directory after the theme — a sibling repo named epresso-theme-<name> is exactly what the theme registry looks for when developing locally (see step 5).

2. Adopt the theme conventions

A reusable theme keeps to a shape that makes it predictable to override:

  
tree
epresso-theme-mytheme/ site.toml # defaults + a [theme] table for options layouts/ # page shells (Base.ep, Doc.ep) — expose <slot/> components/ # every visual element, grouped by role ... styles/ global.css # design tokens (:root vars), resets, fonts pages/ index.ep # a starter page, or nothing at all content.config.py # only if the theme owns a collection (e.g. docs) README.md # what the theme provides and which options it reads

Rules of thumb:

  • No hard-coded branding. Names, URLs, menus and accents come from site.toml ([site]) or the theme's own [theme] table, never from a literal in a component.
  • Global styles live in styles/. Only truly document-wide things — tokens, fonts, resets. Everything component-specific goes in that component's scoped <style>.
  • Layouts use <style is:global>. A shell's styles must not be scoped to the shell's own output.
  • Ship a starter, not a site. One or two example pages are fine; real content belongs to the person using the theme.
  • [theme] is your options table. epresso passes it through untouched — read it from a component's frontmatter as site.config.theme.

3. Wire options through [theme]

Declare defaults in the theme's site.toml:

  
toml
[site]name = "My Theme"url = "http://localhost:4321"[build]trailing_slash = "always"[assets]css = ["global.css"][theme]accent = "#087a44"footer_text = "Built with epresso"

Inject a configured value from the layout shell (layouts/Base.ep). A shell is a full HTML document with one global style block and the content slot:

  
epresso
------<!doctype html><html lang="{{ site.config.site.language or 'en' }}"> <head> <meta charset="utf-8"> <title><slot name="title">{{ site.config.site.name }}</slot></title> <link rel="stylesheet" href="{{ asset('global.css') }}"> <style is:global> :root { --accent: {{ site.config.theme.get('accent', '#087a44') }}; } </style> </head> <body> <main><slot/></main> <footer>{{ site.config.theme.get('footer_text', '') }}</footer> </body></html>

A component reads the same table — frontmatter runs with site in scope:

  
epresso
---from pydantic import BaseModelclass Props(BaseModel): title: str subtitle: str = ""---<style> .hero { padding: 4rem 1rem; text-align: center; } .hero h1 { color: var(--accent); }</style><section class="hero"> <h1>{{ props.title }}</h1> {% if props.subtitle %}<p>{{ props.subtitle }}</p>{% endif %} {{ content }}</section>

4. Test it like a project

A theme is buildable on its own:

  
bash
epresso dev . # live reloadepresso build . # → dist/epresso check . # validate config + routesepresso fmt . # canonical .ep formatting

Create a scratch site that consumes the theme to check the parts a theme cannot exercise alone (navigation, missing options, empty collections):

  
bash
epresso new mytheme /tmp/consumercd /tmp/consumer && epresso dev

5. Make epresso new find it

Register the theme in src/epresso/themes.py:

  
python
REGISTRY: dict[str, tuple[str, str]] = { # name -> (repo path or URL, default branch) "docs": ("<repo>/themes/docs", "main"), # ships in this repository "blog": ("...epresso-theme-blog", "main"), # sibling checkout / git URL "mytheme": ("https://github.com/you/epresso-theme-mytheme", "main"),}

The value may be an in-repo path (themes that ship with epresso, like themes/docs), a local path to a sibling checkout while developing, or a published git URL. Then:

  
bash
epresso new mytheme mysite

See Start from a template for the resolution order and the failure mode when a theme is unreachable.

Note: registering a theme currently means editing themes.py. Publishing a theme as an installable package — so consumers add it with their package manager instead — is designed but not implemented. It is covered by a Coming soon tutorial, visible in local previews.

6. Document the theme

Your theme's README.md is its contract. List:

  • the options it reads from [theme] (and their defaults),
  • the collections it expects, if any,
  • the components it exposes for pages to use,
  • anything a consumer must keep (e.g. a specific [assets] css entry).

Next steps