Theme Basics

Structure layouts, partials, assets, and switch themes per environment.

Category: Theming

Directory layout #

themes/
  default/
    layouts/
      default.velvet.php
    partials/
    blocks/
    assets/
      css/
      js/
      images/
  • Layouts wrap the entire page. Each .velvet.php file is rendered via ThemeService::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 in routes/web.php—it sanitizes paths and sets Cache-Control, Last-Modified, and ETag headers.

Picking a theme #

  • Runtime switch: set THEME=marketing in .env (overrides config/theme.php’s active key).
  • Per-module additions: call $theme->addPath($module->path('theme')) inside Module::boot() so your module templates participate in the lookup chain.
  • Programmatic override: resolve ThemeService from the container and pass a different name in its constructor if you need tenant-based theming.

What ThemeService injects #

Every layout receives:

  • $page → current VelvetCMS\Models\Page (title, slug, timestamps, meta).
  • $content → rendered HTML body (MarkdownService already ran).
  • $site → array with name and url derived from config('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 #

  1. PageController (or the default route closure) loads a Page via PageService.
  2. ThemeService::render($page, 'default') dispatches page.renderingpage.rendered events so modules can adjust content.
  3. Template variables are merged, layouts located under layouts/<name>.velvet.php, and the compiled HTML is wrapped in a VelvetCMS\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 its partials/ 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.