Two levels of ready-made design ship with TruCMS: the template library (sections and full pages an editor inserts from the builder) and website templates (complete demo sites with pages, menus, theme options and sample data). A plugin can add layouts to the first; the second is extended with seeders or .trucms.zip packages.
| Term | Meaning | Storage |
|---|---|---|
| Template source | A catalog of layouts: the active theme (core) or one module | Files: catalog.php + sections/ |
Section (kind section) | An HTML fragment an editor appends to a page (heroes, feature rows, FAQs…) | page_templates row |
Page (kind page) | A composition of sections ("Home", "Pricing") | page_templates row |
| Template asset | CSS for a layout (shared ui-* kit and per-layout files) | template_assets row, key {Source}:{section} |
| Website template / niche | A whole site; a NicheSeeder subclass or an imported package | website_templates row |
Modules/Playground/
resources/page-templates/
catalog.php what the library lists
sections/playground/note-strip.html the markup
sections/playground/note-strip.css optional layout CSS (stored in the database)
database/seeders/PlaygroundPageTemplatesSeeder.phpresources/page-templates/catalog.php
return [
'sites' => [], // optional demo sites that group pages (see Booking)
'heroes' => [], // sections shown in the "Hero" category: name, thumbnail, premium?, new?
'sections' => [
'playground/note-strip' => [ // = path of sections/playground/note-strip.html
'name' => 'Note strip',
'group' => 'Playground', // category in the library
'thumbnail' => 'placeholder.webp', // a photo from the theme's template images until a screenshot exists
],
],
'library_pages' => [ // full-page layouts built from sections
'playground-notes-page' => [
'name' => 'Playground notes page',
'category' => 'Playground Pages',
'thumbnail' => 'placeholder.webp',
'description' => 'A page that shows one note.',
'sections' => ['playground/note-strip'],
// 'bound' => 'note', // wrap the page in a data-bound element so one record is picked for every section
],
],
];sections/playground/note-strip.html
<section class="pg-note-strip py-5">
<div class="container">
<h2 class="mb-4">Latest note</h2>
<x-shortcode-playground-note note=""></x-shortcode-playground-note>
</div>
</section>sections/playground/note-strip.css
.pg-note-strip { background: var(--bs-light); }database/seeders/PlaygroundPageTemplatesSeeder.php
namespace Modules\Playground\Database\Seeders;
use Database\Seeders\Support\TemplatePublisher;
use Database\Seeders\Support\TemplateSources;
use Database\Seeders\Support\ThemeTemplateCatalog;
use Illuminate\Database\Seeder;
class PlaygroundPageTemplatesSeeder extends Seeder
{
public function run(): void
{
(new TemplatePublisher)->publish(new ThemeTemplateCatalog(
config('dotartisan.front_theme', 'echo'),
TemplateSources::module('Playground'),
));
}
}Call it from your module's DatabaseSeeder (Booking and TruCommerce do) and run it after installing or changing layouts:
php artisan db:seed --class="Modules\Playground\Database\Seeders\PlaygroundPageTemplatesSeeder"What happens: running the seeder creates one section and one page row (deterministic UUIDv5 keys, so a second run updates instead of duplicating); the layout CSS appears as a template_assets row Playground:playground/note-strip; the editor's listing endpoint (admin.page-builder.load) returns the section by name; inserting it (admin.page-builder.templates.show) returns the rendered HTML and the CSS the layout needs. Removing a CSS file from the source deletes its row on the next publish.
Layouts only exist where the plugin is installed
Seed from the module's own seeder so the library never lists layouts whose shortcodes are missing. TemplateSources::modules() only scans enabled modules that contain a catalog.php.
var(--bs-*) values are fine. The theme options (and therefore the whole website) must be able to re-colour any layout..css file (database-stored), never into Echo's Sass. Prefer Bootstrap utilities and the shared ui-* classes first; do not restate what a utility does.<x-shortcode-*></x-shortcode-*> tags with explicit closing tags. Never write {{ }} or @php.lm- class prefix, a single self-contained CSS file with its own @keyframes, and a prefers-reduced-motion fallback; @property and other at-rules are dropped by the purger.Database\Seeders\Support\ShapeDividers), never custom CSS.<footer> inside a layout: the theme styles it as the site footer.assets/images/templates (listed in sources.json).SECTION_CATEGORIES / PAGE_CATEGORIES in ThemeTemplateCatalog map older group names).The full rule set is in .ai/rules/page-templates.md, components.md and bindings.md.
TemplateAssets reads the class names used in the page, CssPurger keeps only the matching rules from the stored kit and layout CSS, and <x-template-styles> prints them into the template_styles stack after the theme stylesheet (so layout rules beat Bootstrap defaults).resources/themes/echo/assets/sass/components/_page-sections.scss by npm run templates:kit into page-templates/kit.css and stored by PageTemplatesSeeder.A layout card shows /modules/{kebab-name}/images/page-templates/previews/{key}.webp when the file exists in the module's resources/assets/images/page-templates/previews (copied to public/modules/{kebab}/images by the Vite static copy for the bundled modules; copy/publish them yourself for a plugin), otherwise the catalog's stock thumbnail. The helper scripts scripts/capture-template-previews.py and capture-niche-previews.py render previews against a scratch database (_previews); they never run against the real database and must not run in parallel.
Use data-bound markers in the section HTML (data-bound="note", data-bind="title"…) and, for a whole page about one record, the catalog's 'bound' => 'source-key'. Bind only when most of the section is source data; leave authored copy (reviews, hours, comparison tables) alone.
database/seeders/Niches/ extending NicheSeeder (usually via ProfiledNicheSeeder::profile(), a data array of brand colours, copy, menus and pages). NicheRegistry discovers every concrete subclass in that folder — no registration. They are published to website_templates by php artisan db:seed --class=WebsiteTemplatesSeeder; raise version() when you republish so sites show "Update available".NichePalette derives every other colour. Presentation (header preset, footer kind, page recipes) lives in NicheStyles; NicheDressingTest fails when an entry is missing.moduleSeeders() (e.g. 'TruCommerce' => [AurelleStoreSeeder::class]); layouts that need a module's shortcodes live in that module (above)..trucms.zipThird parties package a site with Appearance → Website templates → Export (or the template library export) and customers import it through the same screen (permission import website templates). The package (app/Services/WebsiteTemplates/Packages) is JSON plus assets, validated as data only:
PackageContentValidator).PackageAllowList.trucms-asset://{id}; remote files are downloaded only through RemoteAssetFetcher (public IPs, pinned DNS, manual redirects, size/count caps from config/template_packages.php).resources/page-templates; seeder wired into the module's DatabaseSeeder.