Theme Basics
Structure layouts, partials, assets, and switch themes per environment.
Directory layout #
themes/
default/
layouts/
default.velvet.php
partials/
blocks/
assets/
css/
js/
images/
- Layouts wrap the entire page. Each
.velvet.phpfile is rendered viaThemeService::renderLayout(). - partials/ and blocks/ are optional folders for reusable fragments (
$theme->partial('hero'),$theme->block('cta')). - assets/ is served through the hardened
/themes/{theme}/assets/{asset*}route defined inroutes/web.php—it sanitizes paths and setsCache-Control,Last-Modified, andETagheaders.
Picking a theme #
- Runtime switch: set
THEME=marketingin.env(overridesconfig/theme.php’sactivekey). - Per-module additions: call
$theme->addPath($module->path('theme'))insideModule::boot()so your module templates participate in the lookup chain. - Programmatic override: resolve
ThemeServicefrom the container and pass a different name in its constructor if you need tenant-based theming.
What ThemeService injects #
Every layout receives:
$page→ currentVelvetCMS\Models\Page(title, slug, timestamps, meta).$content→ rendered HTML body (MarkdownServicealready ran).$site→ array withnameandurlderived fromconfig('app.*').- Helper closures bound to the theme instance:
$asset('css/app.css')→/themes/<active>/assets/css/app.css$url('contact')→/contact$partial('header', ['title' => 'Docs'])
You can add more context via $theme->set('featureFlags', [...]) before calling render() and later access it with $this->get('featureFlags') or {{ $featureFlags['beta'] }} in templates.
Rendering flow #
PageController(or the default route closure) loads aPageviaPageService.ThemeService::render($page, 'default')dispatchespage.rendering→page.renderedevents so modules can adjust content.- Template variables are merged, layouts located under
layouts/<name>.velvet.php, and the compiled HTML is wrapped in aVelvetCMS\Http\Response.
Assets & cache busting #
- Reference assets using
$asset('css/app.css')so paths stay correct when the theme name changes. - The asset route handles conditional requests; if you deploy new assets, increment an app-wide version (
config('theme.assets.version'), for example) and append it as a query string ({{ $asset('css/app.css') }}?v={{ config('app.version') }}) to bust browser caches.
Multi-theme setups #
- Keep shared components in a module, call
$theme->addPath()to register itspartials/folder, and ship brand-specific overrides inside each tenant theme. - For headless scenarios, skip the theme entirely and render API responses—VelvetCMS doesn’t hardcode any blade-style directives into routes, so you can route
/api/*to pure JSON while still serving themed pages elsewhere.
Use this baseline alongside the Template Engine reference to build maintainable themes without touching core files.