Views & Templating
The View layer is responsible for presenting data to the user. Rhapsody uses the powerful Twig templating engine to keep your presentation logic separate from your PHP code. This makes your HTML cleaner, more secure, and easier to manage.
All view files are located in the views/ directory and use the .twig extension.
The @core Namespace and Styling
When you use the @core namespace in your templates (e.g., {{% extends "@core/layouts/main.twig" %}}),
you inherit the framework's default design system.
- Tailwind CSS v4.3.2 – The default theme uses Tailwind for all styling.
- Dark mode support – Built-in light/dark/system theme switching.
- Responsive by default – Mobile-first design with Tailwind's utility classes.
- Google Fonts – Montserrat font family is pre-loaded.
Adding Custom CSS to the Core Theme
If you want to keep the framework's default styling but add your own custom CSS, you should:
-
Copy the core
main.twigto your theme:cp vendor/arout/rhapsody-core/resources/views/themes/default/layouts/main.twig views/themes/your-theme/layouts/main.twig -
Add your custom stylesheets in the
stylesblock:{% block styles %} {# 1. Keep the core Tailwind styles (or replace them) #} <link rel="stylesheet" href="{{ base_url }}/css/app.css"> {# 2. Your custom styles #} <link rel="stylesheet" href="{{ base_url }}/css/custom.css"> {# 3. Keep the Google Fonts #} <link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <link href="https://fonts.googleapis.com/css2?family=Montserrat:ital,wght@0,100..900;1,100..900&display=swap" rel="stylesheet"> {% endblock %} -
Create your CSS files in
/public/css/:public/css/app.css public/css/custom.css
If you're using a build tool (like Vite or Webpack), you can output your compiled CSS to
/public/css/ and reference it with {{ base_url }}/css/app.css.
Replacing the Core Theme Entirely
If you want to completely replace the framework's styling (e.g., use Bootstrap instead of Tailwind):
- Create a new theme in
views/themes/your-theme/ - Create your own
layouts/main.twigfrom scratch - Set
APP_THEME=your-themein your.envfile
See the Theme Customization guide for more details – new themes must include certain core Twig blocks (e.g. {% block head %}, {% block content %}, etc.)
Global Twig Variables
The framework automatically makes the following variables available in every Twig template:
| Variable | Type | Description |
|---|---|---|
{{ base_url }} |
string | The full base URL of your application. Use this for all links. |
{{ app_url }} |
string | The APP_URL from your .env file. |
{{ app_env }} |
string | The current environment (development, production, etc.) |
{{ app_theme }} |
string | The active theme name from APP_THEME in .env. |
{{ auth.check }} |
bool | true if a user is logged in, false otherwise. |
{{ auth.user }} |
object|null | The logged-in user object (with name, email, user_id, etc.) or null if not logged in. |
{{ flash.success }} |
string|null | Success flash message (displayed once, then cleared). |
{{ flash.error }} |
string|null | Error flash message (displayed once, then cleared). |
{{ schema_markup }} |
string | JSON-LD structured data for SEO (rendered at the end of the page). |
You can also access any data passed from your controller via the second argument to
view() or react(). These are merged into the template's context.
Twig Functions
The framework provides several useful Twig functions that you can use in your templates:
| Function | Description | Example |
|---|---|---|
route() |
Generates a URL for a named route with optional parameters. | {{ route('auth.redirect', {provider: 'facebook'}) }} |
csrf_field() |
Outputs a hidden input field with a CSRF token. | {{ csrf_field() }} |
vite_assets() |
Loads Vite assets (dev server or production build). | {{ vite_assets('resources/js/app.jsx') }} |
react_component() |
Embeds a React component as an "island" in a Twig template. | {{ react_component('UserStats', {userId: 1}) }} |
captcha_form (variable) |
Raw HTML for a CAPTCHA form (rendered with |raw filter). |
{{ captcha_form|raw }} |
Using the route() Function
The route() function generates URLs based on named routes defined in your routes/web.php file.
This is the preferred way to create links because it keeps your URLs maintainable:
{# Simple route without parameters #}
<a href="{{ route('home') }}">Home</a>
{# Route with parameters #}
<a href="{{ route('profile.show', {id: user.id}) }}">View Profile</a>
{# Route with query parameters #}
<a href="{{ route('users.index', {page: 2, sort: 'name'}) }}">Page 2</a>
Named routes are defined in your routes/web.php file using the ->name() method:
$router->get('/profile/{id}', [ProfileController::class, 'show'])->name('profile.show');
Using the captcha_form Variable
If you have CAPTCHA functionality enabled, the captcha_form variable contains the raw HTML for the CAPTCHA widget.
Use the |raw filter to render it:
<form method="POST" action="{{ base_url }}/submit">
{# Render the CAPTCHA widget #}
{{ captcha_form|raw }}
<button type="submit">Submit</button>
</form>
Always use the |raw filter when outputting captcha_form, as it contains HTML markup that would otherwise be escaped by Twig.
Only use |raw with trusted content – the framework ensures captcha_form is safe.
Layouts and Inheritance
One of Twig's most powerful features is template inheritance. Instead of repeating your site's header and footer in every file, you can create a base layout and have other templates "extend" it.
Our framework uses views/layouts/main.twig as the main site template. It contains all the common HTML, including the header, navigation, and footer.
<!-- views/layouts/main.twig -->
<!DOCTYPE html>
<html>
<body>
<header>...</header>
<main>
{% block content %}{% endblock %}
</main>
<footer>...</footer>
</body>
</html>
A specific page template then extends this layout and provides content only for the content block.
{# views/home/index.twig #}
{% extends "layouts/main.twig" %}
{% block content %}
<h1>Welcome to the Home Page!</h1>
<p>This is the content for our home page.</p>
{% endblock %}
Displaying Data
You can print data passed from your controller using double curly braces: {{ ... }}. Twig automatically escapes this output to prevent XSS attacks, making it secure by default.
// In your controller
return $this->view('profile.twig', ['username' => 'Alice']);
{# In profile.twig #}
<h1>Welcome, {{ username }}!</h1>
Working with Flash Messages
Creating Flash Messages in Controllers
Flash messages are temporary messages that are stored in the session and displayed once, then automatically cleared.
You can create them using the ->with() method when returning a redirect response:
// In your controller
public function submitForm(Request $request): Response
{
// ... process form data ...
if ($success) {
return redirect('/dashboard')->with('success', 'Your profile has been updated!');
} else {
return redirect('/profile')->with('error', 'There was a problem updating your profile.');
}
}
The redirect() helper returns a RedirectResponse object, and ->with()
accepts two arguments: the flash message type and the message content.
Displaying Flash Messages in Templates
Flash messages are automatically available in all Twig templates via the flash global variable.
The main layout template already includes a section to display success and error messages.
You can display them manually in any template like this:
{% if flash.success %}
<div class="alert-success">{{ flash.success }}</div>
{% endif %}
{% if flash.error %}
<div class="alert-error">{{ flash.error }}</div>
{% endif %}
The main layout file @core/layouts/main.twig already includes this logic, so you don't need to add it to every template.
However, you can add additional flash types (like warning or info) by creating them in your controller
and displaying them in your templates accordingly.
Creating Custom Flash Messages
You're not limited to success and error. You can use any key you want:
// In your controller
return redirect('/dashboard')->with('warning', 'Your session is about to expire.');
return redirect('/dashboard')->with('info', 'A new update is available.');
And in your template:
{% if flash.warning %}
<div class="alert-warning">{{ flash.warning }}</div>
{% endif %}
{% if flash.info %}
<div class="alert-info">{{ flash.info }}</div>
{% endif %}
Flash messages are one-time use. After they are displayed, they are automatically cleared from the session. This makes them perfect for showing feedback after form submissions, logins, or other actions.
Check the FAQ or open an issue on GitHub.