Introduction to building a TypeDock theme — directory layout, the shipping checklist, and pointers to the per-topic references.
If you're porting a WordPress or Hugo theme, the quickest mental model is:
theme.jsonreplacesfunctions.phpfor declaring what the theme exposes to the admin UI (settings, slots, menu locations). It does not contain behaviour.- Template files are Latte (
.latte), which is strictly server-rendered and auto-escapes everything by default. - Themes must not talk to the database or run PHP logic. They render.
Dynamic template output goes through
{component(...)}or{slot(...)}. Page and post bodies use the Tiptap Component Block instead of Latte tags.
Where to look next
| Topic | Read |
|---|---|
theme.json schema (metadata, settings, slots, menus, fetch) |
theme-json-reference.md |
Latte globals, $page / $post shape, image handling, partials |
theme-template-reference.md |
| The bare-component-themed-chrome principle + class tables | theme-components.md |
| CSS variables, asset switching, custom-CSS escape hatch | theme-settings.md |
Latte 3 syntax gotchas ({literal}, <script>, |escapeUrl, …) |
latte-quickref.md |
1. Directory layout
A theme lives in themes/<slug>/ and has this shape:
themes/my-theme/
theme.json # Declaration: metadata, settings schema, slots
layouts/
base.latte # The root HTML document
single.latte # Blog post
page.latte # Static page
archive.latte # Category/tag/blog index
source-list.latte # External Source list/archive, e.g. /docs
source-detail.latte # External Source detail, e.g. /docs/install
search.latte # Search results
home.latte # Homepage when home_mode = page
author.latte # Author archive
403.latte / 404.latte / 500.latte
partials/
header.latte
footer.latte
breadcrumb.latte
pagination.latte
... # Any supporting fragments
components/ # Optional — per-theme overrides of shared components
search-form.latte
latest-posts.latte
...
assets/
css/style.css
js/main.js
screenshot.svg # Preview image shown on /admin/themes
At runtime TypeDock publishes themes/my-theme/assets/ into
public/themes/my-theme/assets/ so the web server can serve the files
directly, without PHP. You reference those files from templates as
{$theme->url}/assets/css/style.css (see §2).
The components/ directory is optional. When present, its files override
the built-in component templates for your theme only: drop a
components/search-form.latte and you restyle the search widget without
touching any other theme.
2. Assets
2.1 Publishing
public/themes/<slug>/assets/ is populated by AssetPublisher on theme
activation and whenever php cli/assets-publish.php is run. The source
lives at themes/<slug>/assets/, and templates reference the published
URL via {$theme->url}/assets/....
During development you can skip the CLI and just mirror your source dir:
php cli/assets-publish.php # publishes all themes + plugins
2.2 The screenshot.svg convention
Drop assets/screenshot.svg (PNG / JPG / WEBP also supported) and it
will be shown as the theme's preview on /admin/themes. Recommended
size: 1200×900 @ 2x.
2.3 Third-party assets
Theme-supplied JS is loaded with defer. Keep it framework-free when
possible — TypeDock's admin already ships no JS framework on the
frontend, so adding one just for a menu toggle goes against the grain.
3. Demo content for development
You don't need to author posts by hand to start theming. After installing TypeDock, run:
php cli/seed.php
…to drop in a baseline set of categories, tags, posts, pages, and menus. The seed is idempotent — re-running skips rows that already exist, and it never touches operator-authored content. You can also combine it with the installer in one shot:
php cli/install.php --with-demo
The seed targets every layout your theme ships: home (archive mode), single post, static page, category / tag archives, search, and the author archive. If a layout still looks empty after seeding, the issue is in the template, not the database.
For a disposable preview loop that does not touch your real
config.php or site database, run:
php cli/theme-preview.php my-theme --port 8080
This creates .preview/my-theme/preview.sqlite, runs migrations, seeds
preview content, activates the target theme in that sandbox, publishes
the theme assets, and starts PHP's built-in server. The command prints
URLs for home, single, page, archive, category, tag, search, author,
403, 404, and 500 layouts.
If Playwright is already installed in the project, add --screenshot
to save full-page PNGs under .preview/my-theme/screenshots/.
4. A checklist for shipping a new theme
Before you publish a theme on a marketplace or submit it to the TypeDock theme repository:
-
theme.jsonhasname,version,author,description - At least
base,single,page,archive,search,404layouts render without error against a seeded database (runphp cli/seed.phpand walk every URL) - If the site uses External Sources, ship
source-list.latteandsource-detail.latte(or slug-specific variants such assource-docs.latte) so/docsdoes not fall back to the blog archive -
.sr-onlyand.skip-linkare defined in your CSS - Every slot your theme declares has a sensible
defaultsarray - Every
menus.<location>declared intheme.jsonis consumed somewhere — by a$site->menu('<location>')call or a{component('menu', ['location' => '<location>'])}widget - Location keys describe placement/role (
header,footer,mobile) rather than abstract priority (primary,nav1) -
partials/breadcrumb.latteandpartials/pagination.latteexist and are included from the relevant layouts - Every settings field has a
default - The theme does not read from the database directly — everything
dynamic in templates flows through
{component},{slot}, or atheme.jsonfetchdeclaration - Author-facing instructions for page/post content use the Tiptap
Component Block, not Latte
{component}snippets - Theme CSS defines semantic tokens used by component chrome, such
as
--color-accent,--color-on-accent,--color-border, and--color-surface - Common plugin component classes your users are likely to enable
are styled or intentionally left bare, especially
.td-formand.td-social-* - Components intended for External Source list views declare
source_list.compatibleand their mappable inputs intheme.json - Body/content CSS covers Markdown and Tiptap output: headings,
lists, blockquotes, links, images, inline
code, fenced code blocks (pre > code), and GFM tables - Switching between
font-style--sans/--serifetc. does not leave strayvar(--font-serif)references unset -
screenshot.svg(or .png/.jpg/.webp) is present underassets/ -
advanced.custom_css(or equivalent) is exposed so users can override per-locale needs without forking - All
<img>elements pair with$post->thumbnailAlt(or a hard-coded alt attribute, including empty string for purely decorative images)
4.1 External Source layouts
External Sources are read-only routed sections managed in the admin UI: jobs, docs, changelogs, issue boards, product catalogs, and similar content that lives outside TypeDock. They are not blog posts, so do not make them inherit blog copy by accident.
Ship generic templates when your theme supports External Sources:
layouts/source-list.latte
layouts/source-detail.latte
You can also specialize a single source by slug:
layouts/source-docs.latte
layouts/source-docs-single.latte
List templates receive $source, $source_meta, $items, $posts
(an alias of $items for archive fallback compatibility), and
$pagination. Detail templates receive $source, $source_meta,
$resource, and a synthetic $page object whose renderedBody is the
source detail template output.
Use $source->name for the archive heading and $source->description
for the tagline / meta description. $source_meta describes the adapter
(GitHub Markdown Docs, Contentful, etc.); it is useful for small
kickers, not for user-facing section copy.
{* layouts/source-list.latte *}
{layout 'base.latte'}
{block title}{$source->name} - {$site->name}{/block}
{block description}{$source->description ?: ($source_meta->description ?? '')}{/block}
{block content}
<section class="listing-shell">
{include '../partials/breadcrumb.latte'}
<header class="content-header">
<p class="section-kicker">{$source_meta->label}</p>
<h1>{$source->name}</h1>
{if $source->description}<p>{$source->description}</p>{/if}
</header>
{foreach $items as $item}
<article>
<h2><a href="{$item->url}">{$item->title}</a></h2>
{if $item->excerpt}<p>{$item->excerpt}</p>{/if}
</article>
{/foreach}
{include '../partials/pagination.latte'}
</section>
{/block}
For detail pages:
{* layouts/source-detail.latte *}
{layout 'base.latte'}
{block title}{$page->title} - {$site->name}{/block}
{block description}{$page->excerpt ?: ($source->description ?? '')}{/block}
{block content}
<article class="content-shell">
{include '../partials/breadcrumb.latte'}
<header class="content-header">
<p class="section-kicker">{$source->name}</p>
<h1>{$page->title}</h1>
</header>
<div class="entry-content">
{$page->renderedBody|noescape}
</div>
</article>
{/block}
When the source is GitHub Markdown Docs, TypeDock renders GitHub-Flavored
Markdown. Relative links ending in .md are normalized to the routed
External Source URL, and direct .md requests redirect to the extensionless
URL. Your theme still owns the visual styling for generated HTML.
4.2 Content body CSS
Theme CSS should style generated body HTML from both Tiptap pages/posts and External Source Markdown. A practical baseline:
.entry-content {
font-size: 1rem;
line-height: 1.75;
}
.entry-content a {
color: var(--color-accent);
text-decoration: underline;
}
.entry-content :not(pre) > code {
padding: 0.12em 0.34em;
border-radius: 5px;
background: var(--color-surface);
font-size: 0.9em;
}
.entry-content pre {
margin: 1.75rem 0;
padding: 1rem;
overflow-x: auto;
border-radius: 8px;
background: #0b1110;
color: #e7fff8;
}
.entry-content table {
display: block;
width: 100%;
overflow-x: auto;
border-collapse: collapse;
}
.entry-content th,
.entry-content td {
padding: 0.625rem 0.75rem;
border: 1px solid var(--color-border);
}
Also cover ul, ol, blockquote, hr, img, figure, and heading
spacing. External Markdown can contain GFM tables and fenced code blocks;
without these rules docs pages look broken even when the data is correct.
4.3 Core component CSS class table
Core components render bare semantic markup with stable classes. Themes own the visual chrome around those classes. The table below is the public contract for bundled components most themes target.
| Component | Root class | Stable internal classes | Params that change structure |
|---|---|---|---|
search_form |
.search-form |
.sr-only, .search-submit |
placeholder changes the input placeholder only. |
latest_posts |
.widget.widget-latest-posts |
.widget-title, .post-list, .post-list-item, .post-list-item-thumb, .post-list-item-body |
title renders <h3 class="widget-title"> when non-empty; blank title removes the heading. count changes item count only. Thumbnail markup appears only when the post has $post->thumbnail. |
category_list |
.widget.widget-category-list |
.widget-title, .category-list, .count |
title renders <h3 class="widget-title"> when non-empty; blank title removes the heading. .count appears only for categories with posts. |
tag_cloud |
.widget.widget-tag-cloud |
.widget-title, .tag-cloud, .tag-cloud-item |
title renders <h3 class="widget-title"> when non-empty; blank title removes the heading. limit changes item count only. |
related_posts |
.related-posts |
.related-posts-title, .widget-title, .related-posts-grid, .related-post-card, .related-post-thumb |
title renders <h3 class="related-posts-title widget-title"> when non-empty; blank title removes the heading. count changes item count only. Thumbnail markup appears only when the post has $post->thumbnail. Requires post context. |
author_profile |
.author-profile |
.author-avatar, .author-info, .author-name, .author-bio, .author-links |
No params. Avatar, bio, website, and social links render only when the author profile has those values. Requires post or page context. |
menu |
.menu-list |
.menu-item, .has-children, .sub-menu |
location selects the theme-declared menu location. .has-children and .sub-menu appear only for nested items. Menu item custom classes from admin are appended to .menu-item. |
link_list |
.link-list, plus .link-list--horizontal or .link-list--vertical |
.link-list__item |
links controls rendered anchors. layout changes the root modifier class. Empty links render nothing. |
The complete component design contract, including plugin component classes and the bare-component-themed-chrome principle, lives in theme-components.md.
5. Further reading
themes/default/— the minimal starting pointthemes/kinari/— polished single-author / journal themethemes/northline/— magazine theme, demonstratestheme.jsonfetchthemes/kawara/— magazine theme, demonstrates route-data + slot styling withoutfetch
Source: https://github.com/typedock/core/blob/main/docs/theme-development.md