Theme Customization & Architecture

Understand the separation between Rhapsody's core views and your application's custom themes. Learn how to safely override, extend, or completely replace the default design system.


Theming & Customisation

Rhapsody makes it easy to create custom themes or override the default design. All theme files live in the project’s /views/themes/ directory – not inside /app/views/ or the core package.

The framework uses Tailwind CSS v4.3 for the default design. If you extend the core theme, you inherit these styles automatically.


Theme Directory Structure

The active theme is determined by the APP_THEME environment variable in your .env file.

APP_THEME=my_custom_theme

If APP_THEME is not set, the framework falls back to the default theme.

The themes directory is located at:

/views/themes/
├── default/               ← fallback theme (always present)
│   ├── layouts/
│   │   └── main.twig
│   │   └── navbar.twig
│   ├── home/
│   └── errors/
├── my_custom_theme/       ← your active theme
│   ├── layouts/
│   │   └── main.twig      ← custom layout (optional)
│   │   └── navbar.twig
│   ├── home/
│   └── errors/
└── ...

Extending the Core Theme

If you want to keep the framework’s default design (Tailwind styling, layout structure, and helpers) but change specific parts, you can extend the core templates.

Use the @core namespace in your Twig templates to reference core views. For example:

{% extends "@core/layouts/main.twig" %}

{% block content %}
    <!-- Your custom content here -->
{% endblock %}

This approach inherits the core layout, styles, and global variables (like base_url, auth, flash, etc.). You can override any block defined in the core layout.

Tip: The core design uses Tailwind CSS v4.3. If you extend @core/layouts/main.twig, you automatically get these styles and the Vite asset pipeline.

Core Views – A Limited Fallback

The framework core ships with only a small set of views:

  • Documentation pages (@core/docs/...)
  • Authentication pages (login, register) – accessible as auth/login.twig, etc.
  • Error pages (404, 500) – accessible as errors/404.twig

These are available as a fallback only for those specific pages. If you render any other view (e.g., home/index.twig) and it does not exist in your active theme or the default theme, the framework will not look in the core for it. Instead, Twig will throw a LoaderError.

You are responsible for providing all the views your application needs – either in your active theme or the default theme. The core is not a complete view library.


Overriding Core Templates

To override any of the core‑provided views (login, register, error pages, etc.), simply create a file with the same relative path inside your active theme.

For example, to override the default 404 error page, create:

/views/themes/my_custom_theme/errors/404.twig

The framework will use your version instead of the core one. You don’t need to use @core when overriding – the loader checks your theme first.

The search order for any template is:

  1. Active theme – /views/themes/modern/
  2. Default theme – /views/themes/default/
  3. Core views – only for the specific pages listed above (via @core namespace or fallback)

Navigation Menus

Naturally, you'll want to create your own navigation menu. To do so, you will need to create a navbar.twig view file inside /layouts in your template directory: /views/themes/{theme-name}/layouts/. If you plan on reusing the core styles, it would be a good idea to view the core navbar to see its structure / styles.

/vendor/arout/rhapsody-core/resources/views/themes/default/layouts/navbar.twig

Or even better, simply copy it to your theme directory and modify it there.

Note: it may be necessary to copy over core's main.twig as well, and adjust the location of the navigation block, if you want your navbar on the bottom of the screen, or other conditions where the default location may interfere with your layout.

Creating a Completely Custom Theme (From Scratch)

If you want a totally custom design that does not rely on the framework's default styles or layout, you can build your own parent template.

  • Do not use @core in your templates.
  • Create your own layouts/main.twig inside your theme.
  • Include your own CSS (e.g., via <link> tags, or use your own build tool).
  • You are responsible for providing the necessary global Twig variables (like base_url, auth, flash) if you need them – they are still available globally, but you must output them as needed.

Every theme must define the same blocks as the default theme

Twig's behavior when a child template overrides a block that doesn't exist anywhere in the layout it extends is to silently drop that content — no error, no warning, just missing output. For a block as foundational as content or scripts, that's a uniquely confusing way to fail: the page renders successfully, just without whatever a page template put in that block.

To catch this before it ever reaches that point, any theme other than default has its layouts/main.twig checked at boot for these exact block names — the same ones views/themes/default/layouts/main.twig itself defines:

title, description, styles, head_extensions, body_class,
navigation, full_width_content, main, content, scripts

If any are missing, the application throws a fatal error at boot listing exactly which ones — not a blank page, not silently-missing content on whichever page happens to use that block first. A block is allowed to be empty ({% block scripts %}{% endblock %} is completely valid and satisfies the check), it just has to exist.

Here's a minimal but fully compliant custom main.twig — every required block is present, several intentionally empty:

<!DOCTYPE html>
<html>
<head>
    <title>{% block title %}My Custom Site{% endblock %}</title>
    <meta name="description" content="{% block description %}{% endblock %}">
    <link rel="stylesheet" href="https://rhapsodyframework.com/css/my-styles.css">
    {% block styles %}{% endblock %}
    {% block head_extensions %}{% endblock %}
</head>
<body class="{% block body_class %}{% endblock %}">
    {% block navigation %}<header>My Header</header>{% endblock %}

    {% block full_width_content %}{% endblock %}

    <main class="{% block main %}{% endblock %}">
        {% block content %}{% endblock %}
    </main>

    <footer>My Footer</footer>
    <script src="https://rhapsodyframework.com/js/app.js"></script>
    {% block scripts %}{% endblock %}
</body>
</html>

You can then extend your own layout in any child template:

{% extends "layouts/main.twig" %}
{% block content %}
    <!-- custom content -->
{% endblock %}

Preserving Layout Content with {{ parent() }}

This is the single most common mistake when building a theme, or a page within one, and unlike the missing-block problem above, nothing catches it automatically — it fails exactly as silently as Twig's default behavior for blocks always does.

When layouts/main.twig puts something meaningful inside a block — a script tag, a stylesheet link, anything — any child template that overrides that same block completely replaces that content unless it explicitly calls {{ parent() }} first. This is ordinary Twig inheritance behavior, not a Rhapsody-specific rule, but it's easy to forget precisely because the failure is invisible: the page still renders, it just quietly lost whatever the parent put there.

The most consequential real example: the React bridge is typically wired up with {{ vite_assets('resources/js/app.jsx') }} placed inside layouts/main.twig's scripts (or head_extensions) block. Any page template that overrides that same block to add its own inline script — a hero slider, a checkout form, anything — will silently break every React island on that specific page if it doesn't call parent() first:

{% extends 'layouts/main.twig' %}

{% block scripts %}
    {{ parent() }}      <script>
        // this page's own script
    </script>
{% endblock %}

The symptom is exactly as confusing as it sounds: everything works — the page loads, other pages using the same layout work fine — except one specific feature (a React-powered widget, a piece of interactive JS from the layout) is simply absent on this one page and nowhere else, with no error anywhere to point at why. If a page seems to be "missing" something that other pages on the same theme have, checking whether that page overrides a block without calling parent() first is the first thing worth checking.

The rule of thumb: if you're overriding a block that the layout you're extending puts anything into — not just scripts, this applies equally to styles, head_extensions, navigation, or any other block — call parent() as the first line of your override unless you specifically intend to replace that content entirely.


Adding View Namespaces

If you have reusable template components (e.g., from a module), you can add custom Twig namespaces in bootstrap.php:

$loader->addPath('/path/to/module/views', 'module');

Then render: $this->view('@module/index.twig').


Summary

  • Extend core → use @core and override blocks (inherits Tailwind design).
  • Override a core template → place a file with the same path in your active theme (for login, register, errors).
  • Build a custom theme from scratch → create your own layouts/main.twig and avoid @core.
  • Every non-default theme's layouts/main.twig must define all 10 required blocks (empty is fine) — checked at boot, with a fatal error listing exactly what's missing if it doesn't.
  • Call parent() when overriding a block the layout already puts something into (scripts, styles, head_extensions, navigation) — otherwise that content is silently dropped on whichever page overrides it, including things like the React bridge's vite_assets() call.
  • All theme files live in /views/themes/.
  • The core design is built with Tailwind CSS v4.3.
  • Core views are limited – only documentation, auth, and errors. For all other views, you must provide them yourself.