Documents

TypeDock Template Reference

Everything a theme template can read at render time, plus the idiomatic Latte patterns themes use to assemble pages.

For an introduction to themes (directory layout, theme.json, publishing, the shipping checklist) start with theme-development.md.


1. The Latte globals available to every template

This is the complete list of variables and properties TypeDock hands to your templates. Anything not in these tables does not exist on the view model — referencing it (e.g. $page->categories[0] on the wrong context) will fall through to null at render time, which Latte 3 surfaces as a warning. AI-generated themes are particularly prone to inventing fields that "ought to be there"; treat this section as canonical.

1.1 Always-available globals

Variable Type Purpose
$site SiteService $site->name, $site->url, $site->option('site.description'), $site->postUrl(), $site->postsArchiveLabel
$site->menu() fn(string): MenuItem[] $site->menu('header') returns the tree of menu items for the given location. URLs are resolved for Page/Post/Category targets. See menus in theme-json-reference.md.
$theme ThemeContext $theme->url (public assets root), $theme->name, $theme->setting('group.field', default)
$themeStyle ThemeStyleRenderer $themeStyle->renderCssVariables() — emit --td-* declarations
$cms feature-detection helper $cms->hasModule('Collection'), $cms->hasModule('Backup')
$body_class string Route-provided body class
$currentUrl string Current request URI (path + query). Useful for <link rel="canonical"> fallbacks and "active nav" checks.
$fetch object Per-template fetch results from theme.json templates.<name>.fetch. Each declared key becomes a property whose value is the data source's return type — PostView[] for source: posts / related_posts, an array of {id, name, slug, post_count} for categories / tags. Absent keys are null (not undefined).

1.2 Per-context globals

Variable When present Notes
$breadcrumbs Single, page, archive (category/tag/author), search Empty array on 404/500/home. Each item has label (string), url (string), isCurrent (bool). Render via {include 'partials/breadcrumb.latte'}.
$pagination archive, search, author archive, home (archive mode), External Source list PaginationData: current, totalPages, perPage, totalItems, plus hasPrev(), hasNext(), url(int $page), range(int $window). range() returns a contiguous list of page numbers — no null gaps.
$seo single, page, home SeoService result object: title, description, canonical, robots, ogTitle, ogDescription, ogImageUrl, ogType, twitterCard, schemaType, jsonLd (HTML string, emit with |noescape).
$page single (single.latte), page (page.latte), External Source detail (source-detail.latte), home in home.latte when home_mode = page PageView object for posts/pages (§2). External Source detail receives a page-like object whose renderedBody is the source detail template output (§4).
$posts archive, author archive, home (archive mode) PostView[] (list) — see §3.
$items External Source list (source-list.latte) External Source item objects — see §4. $posts is also set to the same array for archive fallback compatibility. Prefer $items in source templates.
$source External Source list/detail Source configuration object — see §4.
$source_meta External Source list/detail Adapter metadata object — see §4.
$resource External Source detail The raw projected source item object — see §4.
$results search PostView[] (list). Same shape as $posts.
$category category archive Array {id, name, slug, description?, parent_id?}.
$tag tag archive Array {id, name, slug}.
$author author archive Array {id, name, display_name, slug, bio?, website_url?, avatar_url?, social_links}.
$query search string — the raw query the visitor typed.

2. $page — the single-page view model

Available on single.latte, page.latte, and home.latte when home mode is page. PageView is a strict superset of PostView (§3).

Property Type Notes
$page->id string (UUID)
$page->slug string
$page->title string
$page->url string Full URL with origin.
$page->excerpt string Auto-derived from body if not authored.
$page->publishedAt ?string (ISO8601) null for never-published rows.
$page->updatedAt ?string (ISO8601)
$page->postType 'post' | 'page'
$page->status string 'published' for any rendered page.
$page->thumbnail ?string (URL) The page's image (per-page og_image, falling back to the site-wide default). Use for cards / list-view contexts.
$page->heroImage ?string (URL) Same value as $page->thumbnail. Use this name in single/hero contexts so the template intent reads correctly — both fields share one underlying media id today.
$page->thumbnailAlt string The image's alt text from the media library (media.alt_text). Empty string when not set or when there's no image.
$page->ogImageUrl ?string (URL) Alias for $page->thumbnail. Matches $seo->ogImageUrl.
$page->renderedBody string (HTML) Pre-rendered Tiptap → HTML. Always emit with |noescape.
$page->author->name ?string display_name if set, else name.
$page->author->slug ?string null for system / external authors.
$page->author->avatar ?string (URL) From users.avatar_media_id (uploaded), then users.avatar_path (URL fallback).
$page->author->bio ?string
$page->author->websiteUrl ?string
$page->category ?{name, slug} Primary category — first by categories.sort_order. null when no category attached. Convenient for cards / kickers.
$page->categories array of {id, name, slug} All categories attached to this page. Empty array when none.
$page->tags array of {id, name, slug} All tags. Empty array when none.

Properties that intentionally do not exist on $page: readingTime (TypeDock does not estimate read time — compute it in your template if you need it), commentCount (no native comments yet), and any author social links beyond websiteUrl.


3. $posts / $results — the list view model (PostView)

The shape themes consume on every list-driven layout (archive.latte, author.latte, search.latte, home.latte in archive mode), as well as inside core component templates (latest_posts, related_posts), and as the elements of any theme.json fetch declaration whose source is posts or related_posts.

Property Type Notes
$post->id string (UUID)
$post->slug string
$post->title string
$post->url string Full URL. Use this — never reconstruct via $site->postUrl($post->slug) or post_path().
$post->excerpt string Authored excerpt, or auto-derived.
$post->publishedAt ?string (ISO8601)
$post->updatedAt ?string (ISO8601)
$post->postType 'post' | 'page'
$post->thumbnail ?string (URL) The post's image (per-row og_image, falling back to the site-wide default).
$post->heroImage ?string (URL) Same value as $post->thumbnail — kept as a separate name so list-view templates can read thumbnail and feature/hero templates can read heroImage.
$post->thumbnailAlt string Alt text from media.alt_text. Empty string when no image / no alt set. Always pair with the image: <img src="{$post->thumbnail}" alt="{$post->thumbnailAlt}">.
$post->author->name ?string
$post->author->slug ?string
$post->category ?{name, slug} Primary category for "category overlay" labels. null when the post has no category.

Properties that intentionally do not exist on $post in lists: renderedBody (lists never need full body — query the single page if you do), categories / tags (lists carry only the primary category to keep the view model cheap; query a fetch source if you need the full list per card), author->avatar / bio (those are loaded for $page only).

3.1 Old array-style access is gone

Earlier prototypes exposed posts as PHP arrays ($post['title'], $post['og_image_url'], $post['author_name']). All themes now consume the object shape above. Translation table for porting an older theme:

Old (array) New (object)
$post['id'] $post->id
$post['title'] $post->title
$post['slug'] $post->slug
$post['og_image_url'] $post->thumbnail
$post['published_at'] $post->publishedAt
$post['updated_at'] $post->updatedAt
$post['excerpt'] $post->excerpt
$post['author_name'] $post->author->name
$post['category_name'] (never existed) $post->category?->name
$site->postUrl($post['slug']) $post->url
post_path($post['slug']) $post->url

4. External Source view models

External Sources render read-only data from an adapter such as Contentful, GitHub Issues, Generic JSON, WordPress REST, or GitHub Markdown Docs. They use their own template candidates before falling back to normal blog templates.

List route candidates, in order:

  1. source-{slug}.latte
  2. layouts/source-{slug}.latte
  3. source-list.latte
  4. layouts/source-list.latte
  5. archive.latte
  6. layouts/archive.latte

Detail route candidates, in order:

  1. source-{slug}-single.latte
  2. layouts/source-{slug}-single.latte
  3. source-detail.latte
  4. layouts/source-detail.latte
  5. single.latte
  6. layouts/single.latte

4.1 $source

Available on source-list.latte and source-detail.latte.

Property Type Notes
$source->id string UUID.
$source->slug string Public route prefix, e.g. docs owns /docs.
$source->name string Operator-authored section name. Use this as the H1 on list pages.
$source->description string Operator-authored section tagline / description. Use for archive copy and meta description. Empty string when not authored.
$source->provider string Adapter id such as contentful, github_issues, github_docs, wordpress_rest, or generic_json.
$source->status string active or draft; public templates only receive active sources.
$source->cache_ttl_seconds int Mostly for diagnostics.

4.2 $source_meta

Available on External Source list/detail templates. This describes the adapter, not the individual section.

Property Type Notes
$source_meta->provider string Same adapter id as $source->provider.
$source_meta->label string Human label such as GitHub Markdown Docs. Useful for a small kicker.
$source_meta->description string Adapter description, with $source->description already preferred when authored. Use as a fallback only.

4.3 $items and $resource

External Source list templates receive $items. Detail templates receive $resource plus a page-like $page object for compatibility with single-page markup.

Property Type Notes
$item->id / $resource->id string Adapter-provided id or stable hash.
$item->slug string Public item slug. May contain / for nested GitHub docs.
$item->url string Public URL. Use this; do not reconstruct from the slug.
$item->title string Mapped title.
$item->excerpt string Mapped excerpt or adapter-derived first paragraph.
$item->thumbnail string URL or empty string.
$item->thumbnailAlt string Currently title fallback for external items.
$item->publishedAt / $item->date string Mapped date or adapter update time.
$item->category string Mapped category/directory/state.
$item->tags string[] Mapped tags.
$item->content mixed Mapped content field. Usually Markdown string, Contentful rich text array, or plain text.
$item->fields object/array-like payload Normalized adapter fields. Prefer mapped top-level fields for templates.
$item->raw mixed Raw normalized adapter item. Use only for adapter-specific escape hatches, such as a source URL.

On detail pages, $page exposes:

Property Type Notes
$page->title string Same as $resource->title.
$page->excerpt string Same as $resource->excerpt.
$page->renderedBody string (HTML) External Source detail template output. Emit with |noescape.
$page->publishedAt string Same as $resource->publishedAt.
$page->thumbnail / $page->heroImage string External image URL or empty string.
$page->tags string[] External tags. These are strings, not {name, slug} objects.
$page->source object Same source object as $source.
$page->resource object Same item object as $resource.

GitHub Markdown Docs sources render GitHub-Flavored Markdown in $page->renderedBody. Relative links ending in .md are rewritten to the routed External Source URL (theme-template-reference.md becomes /docs/theme-template-reference), and direct .md requests redirect to the extensionless URL. Themes should still style the generated HTML: inline code, pre > code, table, blockquote, ul, ol, images, and headings.


5. Image handling

There is one image per page in TypeDock today: the og_image set on the SEO panel. The view model exposes it under three names so templates can read whichever fits the context:

Name When to use it
$post->thumbnail Cards / list rows / sidebar widgets
$post->heroImage Hero / featured / above-the-fold contexts on single.latte or home.latte
$post->ogImageUrl (single only) Anything tied to social sharing meta — same value as $seo->ogImageUrl

All three resolve from the same media id, so set the SEO image once and every consumer picks it up. $post->thumbnailAlt is the alt text from media.alt_text (empty string when not authored). Always pair them:

{if $post->thumbnail}
    <img src="{$post->thumbnail}" alt="{$post->thumbnailAlt}" loading="lazy">
{/if}

Fallback strategy. When a post has no image, $post->thumbnail is null. Themes decide what to do:

{* Option 1 — graceful: hide the image entirely. *}
{if $post->thumbnail}
    <img src="{$post->thumbnail}" alt="{$post->thumbnailAlt}" loading="lazy">
{/if}

{* Option 2 — slot a placeholder so cards stay the same shape. *}
<img src="{$post->thumbnail ?: $theme->url . '/assets/img/post-placeholder.svg'}"
     alt="{$post->thumbnailAlt}" loading="lazy">

{* Option 3 — for demos / dogfood / preview seeds only. *}
<img src="{$post->thumbnail ?: 'https://picsum.photos/seed/' . $post->slug . '/520/360'}"
     alt="" loading="lazy">

Option 1 is the production default. Option 3 is fine in bundled demo themes (the Northline / Kawara theme files use it) but be explicit that it's a demo crutch in your README so users know to author real images.

What about srcset / multiple sizes? Not yet. The contract today is a single URL. When TypeDock adds responsive images, it will extend $post->thumbnail to also expose sibling fields (thumbnailSrcset, thumbnailSizes) — never replace the existing fields. Treat the URL form as stable.


6. base.latte — the root document

Every concrete layout extends base.latte via {layout 'base.latte'}. A minimal but production-ready base.latte:

<!DOCTYPE html>
<html lang="{$site->option('site.locale') ?? 'en'}">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">

    {* SEO block when present, or plain title fallback *}
    {if isset($seo)}
        <title>{$seo->title} - {$site->name}</title>
        {if $seo->description}<meta name="description" content="{$seo->description}">{/if}
        {$seo->jsonLd|noescape}
    {else}
        <title>{block title}{$site->name}{/block}</title>
    {/if}

    {* Theme-settings CSS variables — MUST come before your stylesheet so
       var(--td-*) references resolve to the saved values. *}
    {var $cssVars = $themeStyle->renderCssVariables()}
    {if $cssVars}<style>{$cssVars|noescape}</style>{/if}

    <link rel="stylesheet" href="{$theme->url}/assets/css/style.css">

    {* User-supplied custom CSS — always loaded last so it wins. *}
    {var $customCss = $theme->setting('advanced.custom_css', '')}
    {if $customCss}<style>{$customCss|noescape}</style>{/if}
</head>

{* Read settings once, project them onto body classes so CSS can branch. *}
{var $fontStyle = $theme->setting('typography.font_family', 'sans')}
{var $sidebar   = $theme->setting('layout.sidebar', 'right')}
<body class="{$body_class ?? ''} font-style--{$fontStyle} sidebar--{$sidebar}">
    {include 'partials/header.latte'}
    <main>{block content}{/block}</main>
    {include 'partials/footer.latte'}
</body>
</html>

7. Slots and components

Render configurable regions with {slot('name')}. Render a single named component with {component('type', [params])}:

<aside class="sidebar">
    {=slot('sidebar')|noescape}
</aside>

<aside class="sidebar-nav">
    {=component('menu', ['location' => 'header'])|noescape}
</aside>

The location key passed to {component('menu', ...)} must match a key declared under the menus block of theme.json. That declaration is how the admin discovers which navigation regions exist — referencing an undeclared key will render an empty menu. See theme-json-reference.md for why you usually reach for $site->menu('location') instead.

|noescape is required because both helpers return pre-rendered, already-escaped HTML.

Important: {component(...)} and {slot(...)} are theme template syntax only. Page and post bodies are stored as Tiptap JSON, so site operators should insert components with the editor's slash menu Component Block. Do not tell users to paste Latte tags into editor content.


8. Partials — the idiomatic pattern

Breadcrumbs, pagination, and navigation are data arrays (§1) that TypeDock hands to every template. The convention is to isolate their HTML in partials/ and pull them in wherever needed:

partials/
  header.latte       # consumes $site->menu('header')
  footer.latte       # consumes $site->menu('footer')
  breadcrumb.latte   # consumes $breadcrumbs
  pagination.latte   # consumes $pagination

Then each layout stays focused on its page shape:

{* layouts/single.latte *}
{layout 'base.latte'}
{block content}
    {include 'partials/breadcrumb.latte'}
    <article>
        <h1>{$page->title}</h1>
        {$page->renderedBody|noescape}
    </article>
{/block}
{* layouts/archive.latte *}
{layout 'base.latte'}
{block content}
    {include 'partials/breadcrumb.latte'}
    {foreach $posts as $post}
        <article><a href="{$post->url}">{$post->title}</a></article>
    {/foreach}
    {include 'partials/pagination.latte'}
{/block}

This keeps the HTML for shared UI concerns in one place and makes layouts easy to skim. Use it over inlining the <nav> and <ol> on every layout.


9. Reading a theme setting

<body class="... font-style--{$theme->setting('typography.font_family', 'sans')}">

Always pass a default as the second argument. Saved values are merged with the schema's defaults, but defending against a missing key is cheaper than debugging a null in the class list later.

For how scalar settings turn into CSS custom properties (and the recommended --td-* → semantic-token alias pattern), see theme-settings.md.

Source: https://github.com/typedock/core/blob/main/docs/theme-template-reference.md