Skip to content
agovena.
agovena.
Get started
Community

Developer guides

Theme contracts and assets

Build storefront and Admin presentation with Theme manifests, Blade views, settings, and Vite assets.

On this page

Presentation is a separate boundary

A Theme owns the appearance of the storefront and can also provide the Admin surface. It must not decide prices, grant permissions, reserve stock, or mark payments paid. Keep those behaviors in Core services and Module or Extension contracts. Theme templates render the data and actions supplied by application screens.

ThemeManager discovers directories under themes/. It selects the active Theme from the appearance.active_theme setting, falling back to Default or another installed Theme if the configured ID is absent. Activation is a settings change, not a Module migration lifecycle.

Start from the actual scaffold

bash
php artisan agovena:make-theme studio

The generator writes themes/studio/theme.json only. It does not copy Default's views, CSS, settings schema, or Vite inputs. A generated manifest is not yet a usable storefront Theme.

An illustrative completed manifest:

json
{
  "id": "studio",
  "name": "Studio",
  "version": "0.1.0",
  "description": "A custom storefront presentation.",
  "css": "themes/studio/resources/css/theme.css",
  "preview": "preview.png",
  "capabilities": ["storefront"]
}

The runtime fields are id, name, version, description, css, optional admin_css, optional preview, and capabilities. An absent or empty CSS value resolves to themes/<id>/resources/css/theme.css; setting css to null does not disable CSS. Preview paths are relative to the Theme directory. capabilities advertises presentation features and is unrelated to a product's capabilities.

To provide Admin, include admin in capabilities, supply the appropriate Admin views, and set admin_css to the Theme's Admin entry. Default's actual manifest declares both storefront and admin, along with its supported presentation features.

Sources: ThemeManifest and Default manifest.

Blade view contract

$theme->view('catalog.index') returns theme::catalog.index. The service provider maps theme:: to the active Theme's views/ directory. Supply the views that your enabled application surfaces request. This registration is not a general per-file inheritance chain from Default.

A useful implementation inventory is:

text
themes/studio/
  theme.json
  settings.schema.php
  resources/css/theme.css
  views/layouts/storefront.blade.php
  views/layouts/checkout.blade.php
  views/catalog/...
  views/checkout/...
  views/account/...
  views/pages/...
  views/partials/...
  views/errors/...

Inspect each screen's render() method for the exact view name and variables. For example, CategoriesIndex renders catalog.categories with categories, theme, and themeConfig, then uses layouts.storefront. CheckoutPage uses checkout.index and layouts.checkout, with line, totals, requirements, and interaction state supplied by the component.

Preserve the relevant Livewire bindings and actions when replacing markup. The reference layouts also contain CSRF metadata, locale, branding, consent, navigation, and asset integration. Removing those to obtain a visually minimal template can break actual behavior. See Default storefront layout.

Optional Modules can request Theme account views too. Test the capability combinations you support, not just the homepage. An unsupported view is not automatically supplied by declaring a capability string in theme.json.

Admin surface fallback

ThemeManager::themeFor(ThemeSurface::Admin) selects the active Theme when it advertises Admin, otherwise Default if Default does. AgovenaServiceProvider prepends that Theme's views location for Admin layouts. This keeps the default Admin available to a storefront-only Theme.

Providing an Admin surface means preserving the control-center contracts: navigation, permission-aware actions, errors, tables, forms, and loading states. It is not permission to replace server authorization with hidden buttons. A Theme should not import provider business logic merely to render a provider's label or status.

Appearance settings

settings.schema.php may return a ThemeSettingsSchema or an array of ThemeSettingField instances. Fields describe key, label, type, default, group, optional help, optional options, and sort.

This is the same field pattern used by Default:

php
<?php

use App\Agovena\Theme\ThemeSettingField;
use App\Agovena\Theme\ThemeSettingsSchema;

return new ThemeSettingsSchema([
    new ThemeSettingField(
        key: 'appearance.default_color_mode',
        label: 'admin.appearance.theme_fields.appearance.default_color_mode',
        type: 'select',
        default: 'system',
        group: 'appearance',
        options: ['system', 'light', 'dark'],
        sort: 10,
    ),
]);

ThemeConfig reads persisted values from theme.<id>, then schema defaults, then the caller's fallback. Its bool() and string() accessors provide typed consumption. This is presentation configuration, not an encrypted provider credential store.

Homepage sections have an explicit normalization allowlist: hero, featured_products, featured_categories, trust_strip, promo_split, and rich_text. Text is stripped of tags and bounded, links are filtered, and media paths are restricted. A new section type needs implementation and validation, not just a new JSON value.

Sources: Default settings schema, ThemeSettingsSchema, and ThemeConfig.

Asset contract

The Core vite.config.js lists explicit inputs. Adding a Theme manifest does not automatically add a build entry. Add the Theme's CSS and any new JavaScript entry to the build configuration in your implementation, then reference the built entry from its layout through Laravel's Vite integration.

blade
@vite([$theme->cssEntry, 'resources/js/storefront.js'])

Do not point a production stylesheet tag directly at an unbuilt source path. Keep the manifest path, Vite input, and layout reference aligned, including case. Verify the production build, not only the Vite development server.

Default's CSS entry imports tokens, reset, a shared card primitive, storefront and checkout components, consent, error-page components, and accessibility utilities. Follow its native CSS and ITCSS organization. Preserve existing component and state classes when reusing the corresponding behavior. Theme variables use --theme-*; shared Admin primitives also have their own contract. Do not replace the established styling structure with a utility framework simply because it is familiar.

Error pages and resilience

A Theme can provide views/errors/<status>.blade.php. Error selection tries the active Theme, then Default for that exact status. The renderer accepts HTTP error statuses from 400 to 599, supplies status, theme, and possibly null themeConfig, and preserves the original HTTP status.

ThemeErrorRenderer avoids handling JSON, authentication, and validation exceptions through this HTML path. Error templates must tolerate unavailable settings and use static CSS fallbacks. Do not query the failing application service again merely to decorate an outage page.

Verification checklist

bash
php artisan test tests/Feature/ThemePlatformTest.php
php artisan test tests/Feature/ThemeAdminSurfaceTest.php
php artisan test tests/Feature/ThemeErrorPagesTest.php
npm run build

Also inspect actual browser behavior at mobile and desktop widths, both color modes, keyboard navigation, form errors, disabled buttons, cart updates, checkout, customer pages, and supported Module views. PHP tests use withoutVite() in the base test case, so passing them alone does not verify production assets. See contributing for browser-test setup.

Search documentation

Search guides, commands and API endpoints

What are you looking for?

Documentation