The schema TypeDock reads from a theme's root theme.json. The file
serves three jobs:
- show the theme on
/admin/themes, - generate the
/admin/theme-settingsform, - seed default slot placements on activation.
For introductory orientation see theme-development.md. For how settings turn into CSS variables, see theme-settings.md.
1. Metadata
{
"name": "My Theme",
"version": "1.0.0",
"description": "What the theme is for, who it's aimed at.",
"author": "Your name"
}
2. settings — user-tunable options
The settings block declares field groups that appear as tabs in
/admin/theme-settings. TypeDock handles persistence, form rendering, and
coercion; the theme only decides what is adjustable.
{
"settings": {
"colors": {
"label": "Colors",
"fields": {
"accent": { "type": "color", "label": "Accent", "default": "#8c2a3a" },
"header_bg": { "type": "color", "label": "Header background", "default": "#1f1d1b" }
}
},
"typography": {
"label": "Typography",
"fields": {
"font_family": {
"type": "select",
"label": "Font style",
"options": { "sans": "Sans-serif", "serif": "Serif", "mono": "Monospace" },
"default": "sans"
}
}
}
}
}
Supported field types:
| type | Admin UI | Stored as |
|---|---|---|
color |
Native color picker + hex text input | string ("#rrggbb") |
text |
Single-line text | string |
url |
URL input | string |
number |
Number input | integer |
boolean |
Checkbox | true / false |
select |
Dropdown (requires options) |
selected key |
textarea |
Multi-line text (raw, not CSS-var-safe) | string |
image |
URL field (media picker planned) | string URL or media id |
Every field should declare a default. If omitted, the field is
treated as optional.
For the runtime side — how scalar settings turn into --td-* CSS
custom properties and the recommended --color-* alias pattern — see
theme-settings.md.
3. slots — where content plugs in
Slots are named regions your templates render components into. Each slot has:
- a
labelshown in/admin/slots, - a
contextlist (["post"],["page", "archive"], or["all"]) so incompatible components are filtered out of the picker, - an optional
defaultsarray that's copied intoslot_placementson theme activation.
{
"slots": {
"header_right": {
"label": "Header — right",
"context": ["all"],
"defaults": [{ "component": "search_form" }]
},
"sidebar": {
"label": "Sidebar",
"context": ["post", "page", "archive"],
"defaults": [
{ "component": "search_form" },
{ "component": "latest_posts", "params": { "count": 5 } },
{ "component": "category_list" }
]
},
"after_content": {
"label": "After content",
"context": ["post"],
"defaults": [
{ "component": "related_posts", "params": { "count": 6 } }
]
}
}
}
A slot with no defaults renders nothing until the site operator places
components into it via /admin/slots.
4. menus — navigation locations
Declare every navigation region your theme renders. Each entry becomes
a card on /admin/menus, and TypeDock auto-provisions the backing menu
row the first time the operator opens it — the admin never has to
create a "menu" entity by hand or guess a location key.
{
"menus": {
"header": {
"label": "Header Navigation",
"description": "Main navigation shown in the header on every page."
},
"footer": {
"label": "Footer Links",
"description": "Legal / contact links rendered in the footer."
},
"mobile": {
"label": "Mobile Drawer",
"description": "Navigation surfaced when the mobile toggle opens."
}
}
}
Each location takes:
| Key | Required | Purpose |
|---|---|---|
label |
yes | Human-readable title shown on /admin/menus. |
description |
no | One-line hint surfaced next to the label. |
4.1 Naming
Name locations by placement or role (header, footer, mobile,
utility, legal). Avoid abstract names like primary or nav1 —
the key is what the operator sees when editing, so make it describe
where the nav appears.
4.2 Rendering: $site->menu() is the primary pattern
Themes render navigation by consuming the data array directly. Call
$site->menu('<location>') and write the HTML yourself:
{* partials/header.latte *}
<nav class="site-nav" aria-label="Primary">
<ul class="menu-list">
{foreach $site->menu('header') as $item}
<li class="menu-item {!empty($item->children) ? 'has-children' : ''}">
<a href="{$item->url}">{$item->label}</a>
{if $item->children}
<ul class="sub-menu">
{foreach $item->children as $child}
<li class="menu-item"><a href="{$child->url}">{$child->label}</a></li>
{/foreach}
</ul>
{/if}
</li>
{/foreach}
</ul>
</nav>
Shape of a MenuItem:
| Field | Type | Notes |
|---|---|---|
label |
string | Human text shown in the link |
url |
string | Resolved at render time for Page/Post/Category targets, so slug changes don't break nav |
targetType |
string | custom / page / post / category |
cssClass |
?string | Optional per-item class (admin-set). Camel-cased on the MenuItem object — note the difference from {component('menu', …)}, which still hands back raw rows with the css_class array key. |
children |
MenuItem[] | One level of nesting is supported |
Menu items nest one level deep (parent_id). The admin UI enforces
this limit so your CSS only has to style two levels.
4.3 The menu component is a drop-in fallback
{component('menu', ['location' => 'header'])} still works and is
registered as a slot-placeable widget. Use it when you want to offer a
navigation as sidebar furniture or a similar widget slot. For the
primary header / footer / mobile navs that every theme has, prefer
$site->menu() — it gives you full control over markup without a
per-component template override.
4.4 Locale
Locale is stored internally but not surfaced in the admin UI — assume a
single-language site for now. When TypeDock grows first-class
multilingual support, the menus contract will not change; the admin
gains a language switcher on top.
5. templates — declarative data fetch
Each top-level layout (home, single, archive, search, page,
author) can declare a fetch block that loads data into the
template's $fetch global without writing PHP.
{
"templates": {
"home": {
"file": "layouts/home.latte",
"fetch": {
"home_posts": { "source": "posts", "params": { "limit": 12, "post_type": "post" }, "sort": "-published_at" },
"home_categories": { "source": "categories", "params": { "show_empty": true }, "sort": "sort_order" }
}
},
"single": {
"file": "layouts/single.latte",
"fetch": {
"top_posts": { "source": "posts", "params": { "limit": 4 }, "sort": "-published_at" },
"related": { "source": "related_posts", "params": { "limit": 3 } },
"tags": { "source": "tags", "params": { "limit": 12, "order_by": "count" } }
}
}
}
}
Available source values: posts, related_posts, categories,
tags, menu, site_options. External Sources created in the admin
may also be exposed as source values by their slug. The shape returned
by each becomes the value of $fetch->{key} in the template — see
theme-template-reference.md §1.1.
params may interpolate {{context.category}} / {{context.tag}} /
{{context.post_id}} etc. for archive / single contexts.
6. Source-compatible list components
External Sources can render archive/list pages by mapping API fields to
a theme component. To make a component available in the Source list view
picker, declare source_list.compatible: true and the inputs the Source
mapping form can fill.
{
"components": {
"job-card": {
"label": "Job Card",
"template": "components/job-card.latte",
"source_list": {
"compatible": true,
"inputs": {
"title": { "type": "text", "required": true },
"url": { "type": "url", "required": true },
"excerpt": { "type": "text", "required": false },
"thumbnail": { "type": "image", "required": false },
"thumbnail_alt": { "type": "text", "required": false },
"date": { "type": "date", "required": false },
"category": { "type": "text", "required": false },
"tags": { "type": "list", "required": false, "item_type": "text" }
}
}
}
}
}
Source-compatible components should stay bare: emit semantic markup and
stable classes, but leave card borders, spacing, background, and
placement-specific chrome to the theme CSS. title and url should be
required for most list cards; everything else should degrade cleanly
when omitted.
Supported input types for Source mapping are text, date, number,
url, image, and list.
7. Context-type awareness
Slots can declare the page contexts they render on, via the context
array. Components can declare the contexts they support, via their
PHP definition. TypeDock intersects the two:
- A component placed in a slot whose context it doesn't support is flagged in the admin UI and skipped at render time.
- The admin "add component" dropdown hides incompatible options.
As a theme author you don't have to worry about this — just set
"context": ["post", "page"] on slots that only apply to single
content and let the rest default to ["all"].
Source: https://github.com/typedock/core/blob/main/docs/theme-json-reference.md