How Tovu works
Tovu is a content platform: a database of structured content, rendered through a theme you choose independently. This page describes each content entity Tovu stores and how it moves from the database to a rendered page. Tovu runs locally today — there is no hosted plan or signup — and every entity described below lives in one SQLite database (content.db) inside your site's own folder.
Posts
A Post is a row in the posts table with kind: "post". It is workspace-scoped, has a unique slug, a status of draft or published, and a version number incremented on every edit.
How posts work
A Post's body is a TipTap rich-text document, stored as JSON in the body_json column (body_format: "doc"). A Post can never carry bespoke HTML — that restriction is enforced at the single write chokepoint (resolveBodyFields in apps/website/src/features/post/post.ts), not just by convention. Rendering a Post means walking this document tree and mapping each node to markup; the theme supplies the surrounding page (navigation, footer, typography), never the content itself.
{
"type": "doc",
"content": [
{
"type": "paragraph",
"content": [{ "type": "text", "text": "A theme decides how your site looks." }]
}
]
}
Pages
A Page is a row in the same posts table, discriminated by kind: "page". Posts and Pages share one table and one update contract; kind only changes which admin list a row surfaces in and how it was created.
How pages work
A Page can hold either of two body formats. The default is the same TipTap "doc" format a Post uses. The other, Page-only format is "html" (body_format: "html", content in body_html) — bespoke HTML you write directly, rendered close to verbatim by the active theme. A database constraint enforces that exactly one of body_json/body_html is populated, matching body_format. Converting a Page from "doc" to "html" is one-way: writing HTML for the first time drops whatever TipTap content was there. This page you are reading is itself an "html"-format Page.
An HTML Page's dynamic content — a widget, an embedded image, another entry's content — is not raw markup. It is a marker your theme's renderer resolves at request time:
<div data-embed-config='{"type":"content"}'></div>
That exact marker is what every static-tier theme page uses to mark where a Post's or Page's own content is injected into the theme's surrounding template.
Menus
A Menu is a row in the menus table: a title, a unique slug, and a tree of items in a doc_json column. The navigation menu you used to reach this page, and the sidebar menu next to this text, are both ordinary Menu rows.
How menus work
Each item has a stable id, a label, a target, and an optional list of children for one level of nesting. A target can point at a real entry (entryRef), a taxonomy term (termRef), an arbitrary URL (url), or a named platform route (route). A static-tier theme's own markup references a stored menu by slug through the same marker convention Pages use:
<nav data-embed-config='{"type":"menu","id":"docs-how-tovu-works-sidebar","variant":"tree"}'></nav>
The sidebar menu for this exact page is a real row at slug docs-how-tovu-works-sidebar, trimmed here to its first entry:
{
"type": "menu",
"version": 1,
"items": [
{
"id": "tovu-toc-posts",
"label": "Posts",
"target": { "kind": "url", "href": "#posts" },
"children": [
{ "id": "tovu-toc-how-posts-work", "label": "How posts work", "target": { "kind": "url", "href": "#how-posts-work" } }
]
}
]
}
Media
A Media row (table media) is an uploaded asset: title, alt, caption, credit, and a sourceSha256 content hash, workspace-scoped and versioned like every other entity here.
How media works
Inside a Post's TipTap document, an image is its own node type, with optional per-asset width, height, and CSS class overrides. Inside an HTML Page, a Media asset is placed with the same marker convention as everything else:
<div data-embed-config='{"type":"media","id":"<media-id>"}'></div>
Widgets
A Widget is a reusable, embeddable content block — the mechanism a Page or a declarative-tier theme template uses to place anything that is not plain text: a form, a product listing, a piece of structured data.
How embeds work
Every embeddable type in an HTML Page's body — widget, media, post, content — shares one marker vocabulary and one resolver registry keyed by the marker's type (apps/website/src/features/widgets/resolver-service.ts). A theme's own structural markers (partial, menu) use the identical marker syntax but are resolved by the theme layer instead, never by this registry. Tovu has no separate embed type for a form — a form is embedded as a widget, the same as any other widget instance:
<div data-embed-config='{"type":"widget","id":"<contact-form widget entry id>"}'></div>
Themes
A theme is not a database row — it is a folder of files, copied into your site when installed, that decides how every entity above is arranged and styled. Tovu themes come in tiers, from pure data to plain, fully editable HTML/CSS/JS. This install has 8 static-tier themes, 2 templated-tier themes, and 1 declarative-tier reference theme.
Tiers, at a glance
- Declarative — a JSON manifest, design tokens, and JSON block trees for each template page. No code runs.
- Templated — the same data plus real arranging logic, in a sandboxed language (LiquidJS or Handlebars). Still no JavaScript.
- Static — plain HTML, CSS, and JS files, one per route, every byte editable. This site's active theme is static-tier.
- Code — reserved for trusted, signed plugin JavaScript. Designed, not built yet.
Full detail — the manifest format, per-tier template formats, and how plugins extend a theme — is in How Themes Work.