Content collections

Content is data. A collection is a named group of entries, each validated against an optional Pydantic schema, stored in a collection-keyed immutable store. The entry digest drives incremental rebuilds.

Defining collections

Collections are declared in content.config.py:

  
content.config.py python
from pydantic import BaseModelfrom epresso.content import define_collection, referenceclass Post(BaseModel): title: str date: str tags: list[str] = [] author: reference("authors") # content reference draft: bool = Falseposts = define_collection("posts", glob="**/*.md", base="./content/posts", schema=Post)authors = define_collection("authors", loader=fetch_authors) # remote/derived

define_collection requires a glob pattern or a loader function. When installed, the loader's base is resolved relative to the site root and entries are loaded and validated against the schema.

Loaders

  • Glob loaderglob="**/*.md", base="./content/posts" reads files from disk. Optionally generate_id=callable to derive entry ids from paths.
  • Python loaderloader=fetch_authors is any callable; epresso wraps it as a PythonLoader. Use it for remote or derived data.

Content store

The store is a Map<collection, Map<id, entry>>. An entry is {id, data, filePath, body, digest, rendered}:

  • entry.data — validated schema data.
  • entry.slug / entry.id — entry id without extension.
  • entry.headings — extracted headings (from rendered.metadata).
  • entry.digest — stable digest over body + data; the incremental key.
  
python
store.get_collection("posts") # -> list[Entry] (deterministically sorted by id)store.get_entry("posts", "hello") # -> Entry | Nonestore.collection("posts") # -> Collection (raw)store.names() # -> sorted collection names

Accessing data in templates and routes

The curated globals mirror the store:

  
jinja
{% for post in get_collection("posts") %} <a href="/blog/{{ post.id }}/">{{ post.data.title }}</a>{% endfor %}

get_entry(collection, id) fetches a single entry. These helpers also record the dependency edge for incremental builds — a change to an entry re-renders only the pages that consume it.

Content references

Use reference("collection") as a field type to store a referenced entry id as a string, then resolve at render time with get_entry(...) / get_entries(...):

  
python
class Post(BaseModel): author: reference("authors")

Drafts & scheduled content

In production builds, draft: true and future-dated entries are automatically excluded from output.

Private files (underscore prefix)

Files and directories whose name starts with _ (e.g. _draft.md, _partials/) are treated as private — the Jekyll convention. Most loaders exclude them from builds (this applies to content collections, pages/ routes, and public/ passthrough).

The docs loader instead keeps _-prefixed files but tags them private, and honors draft: true. In the dev server a coloured status pill shows in the toolbar (bottom-right) so you know the current page is private or draft.

Controlling visibility per environment ([content])

Which of these entries actually show up is configurable. By default epresso shows draft and private entries in development / preview and hides them in production builds. Set [content] in site.toml to change that:

  
toml
# site.toml — the production base (epresso build / preview / check)[content]show_drafts = false # omit to keep the default (shown in dev, hidden in prod)show_private = false

The keys are optional: when left out, the historic default applies (shown in development/preview, hidden in production). An explicit value overrides it for the current environment. Because per-environment files are merged on top of site.toml, you can split dev and prod behaviour across files:

  
toml
# site.development.toml — merged on top for `epresso dev`[content]show_drafts = trueshow_private = true
  
toml
# site.production.toml — merged on top for `epresso build`/`preview`/`check`[content]show_drafts = falseshow_private = false

show_drafts controls draft: true entries; show_private controls _-prefixed (private) ones. Both apply to the docs collection (and to any collection whose schema carries a matching draft/private field). Future-scheduled dates are still excluded in production only.

Data collections

Collections can be backed by JSON / YAML / TOML files as well as Markdown — the glob loader reads whatever files match, and schema validates them. A markdown post with YAML front matter:

  
markdown
---title: Hellodate: 2026-01-01tags: [python]---# Hello

Markdown is rendered to HTML (see Markdown; the entry's rendered.html and rendered.metadata (headings, image paths) are available.