Views
Forge renders templates from resources/views using Jinja2 with Forge
directives. Templates live in .forge.py files.
Rendering
from craft.http.controller import Controller
class PostController(Controller):
def index(self, request):
return self.view("posts.index", {"posts": Post.all()})
View names use dot notation: posts.index resolves to
resources/views/posts/index.forge.py.
Errors propagate. A missing template raises
TemplateNotFound, an unknown directive raisesTemplateSyntaxErrornaming it and its line, and withAPP_DEBUGon an undefined variable raisesUndefinedErrorwhen it is printed, iterated or has an attribute read ({% if flash %}on a missing name is still simply false). The exception handler turns each into a 500. Forge used to swallow every error and return a placeholder, so a broken view looked like a working page; the standaloneview()helper incraft.supportnow propagates errors the same wayself.view()does.
Layouts
<!-- resources/views/layouts/app.forge.py -->
<!doctype html>
<html>
<head><title>@yield("title")</title></head>
<body>
@yield("content")
</body>
</html>
<!-- resources/views/posts/index.forge.py -->
@extends("layouts.app")
@section("title", "Posts")
@section("content")
<h1>{{ __('recent_posts') }}</h1>
@endsection
@extends and @include accept dot notation and resolve it to a path.
Directives
| Directive | Compiles to |
|---|---|
@csrf |
Hidden _token input |
@method("PUT") |
Hidden _method input |
@auth … @endauth |
Renders only when signed in |
@guest … @endguest |
Renders only when signed out |
@can('ability', model) … @endcan |
Gate check |
@if(cond) @elseif(cond) @else @endif |
Conditional |
@foreach(items as item) or @foreach(item in items) … @endforeach |
Loop |
@extends("layouts.app") |
Template inheritance |
@section("name") … @endsection |
Named block |
@section("name", "value") |
Inline block |
@yield("name") |
Block placeholder |
@include("partials.nav") |
Include a template |
Directives are rewritten before Jinja compiles the template, so Jinja syntax
works alongside them. Anything else shaped like a directive - @for, @push,
@isset, @include with data - is refused with the list above rather than sent
to the browser as text. CSS at-rules such as @media and @import are left
alone.
Forms and CSRF
Every state-changing form needs a token. @csrf emits it:
<form method="POST" action="/posts">
@csrf
<input name="title" value="{{ old('title') }}">
<button type="submit">Save</button>
</form>
Without it the request is rejected with 419. See Security.
HTML forms only support GET and POST. For other verbs:
<form method="POST" action="/posts/1">
@csrf
@method("PUT")
</form>
Helpers available in every template
| Helper | Returns |
|---|---|
csrf_token() |
The raw token |
csrf_field() |
The hidden input |
auth() |
The signed-in user, or None |
can(ability, *args) |
Gate check |
route(name, **params) |
URL for a named route |
config(key, default) |
Configuration value |
session(key, default) |
Session value |
old(key, default) |
Previously submitted input |
__(key, locale) |
Translation |
@auth
<p>{{ __('dashboard') }}, {{ auth().get_attribute('name') }}</p>
<a href="{{ route('posts.index') }}">{{ __('recent_posts') }}</a>
@endauth
Sharing data
Make a value available to every template:
class AppServiceProvider(ServiceProvider):
def boot(self):
self.app.make("view").share("app_version", "1.0")
Escaping
Autoescaping is on. {{ value }} escapes HTML:
{{ "<script>alert(1)</script>" }} → <script>alert(1)</script>
Render trusted HTML with | safe, and only when you control the source:
{{ captcha_html | safe }}
Checking a template exists
forge = app.make("view")
if forge.exists("posts.index"):
...