Templates (askama 0.16)
Templates are compiled into the binary at build time, so cargo build reports template errors as Rust type errors pointing at the #[derive(Template)] struct. Each page's struct sits next to its handler in src/web/*.rs.
Layouts
base.html blocks: title, page_class, subheader, content
├── layout_repo.html repository header and tabs; blocks: page_title, content
└── layout_settings.html account settings side nav; block: settings
- Every page struct needs
ctx: Ctx, becausebase.htmlreadsctx.user,ctx.flashandctx.path. - Repo pages also need
r: RepoCtxandtab: &'static str.tabis one ofcode,commits,branches,pullsorsettingsand selects the active tab. - Settings pages need
tab(profileorkeys). - In repo pages,
page_titleis a prefix ending in·, e.g.{% block page_title %}Branches · {% endblock %}. - Never create a file named
base.htmlin a subdirectory. askama resolves{% extends %}relative to the current template's directory first, sorepo/*.htmlextending"base.html"would pick uprepo/base.htmland fail with a cyclic dependency. That's why section layouts are namedlayout_*.htmland kept at the root.
Partials ({% include %}, which share the including template's scope)
| Partial | Variables it expects in scope |
|---|---|
partials/diff.html |
diff: DiffSummary, file_ref (commit SHA or URL-encoded ref to link "View file" at), base (repo URL) |
partials/commit_list.html |
groups: Vec<CommitGroup>, base |
partials/refsel.html |
refsel: RefSelector |
partials/clone.html |
r: RepoCtx, view with a refname field (used for archive links) |
partials/repo_list.html |
repos: Vec<Repo> |
Bind them with {% let %} before including, e.g. {% let file_ref = commit.id.clone() %}, or {% let repos = recent %} to reuse the repo list.
Calling Rust from templates
Helpers live in src/web/fmt.rs and are called by full path:
{{ crate::web::fmt::icon("git-branch")|safe }} {# names: match arms in src/icons.rs #}
{{ crate::web::fmt::avatar(user.username, 20)|safe }}
<time title="{{ crate::web::fmt::datetime_full(c.committer_time) }}">{{ crate::web::fmt::ago(c.committer_time) }}</time>
{{ crate::web::fmt::plural(view.commit_count, "commit") }} {# "1 commit", "3 commits", "2 branches" #}
{{ base }}/tree/{{ crate::web::fmt::urlpath(view.refname) }}/{{ crate::web::fmt::urlpath(entry.path) }}
/login?next={{ crate::web::fmt::urlencode(ctx.path) }}
- askama passes struct fields by reference (
&c.committer_time) but method-call results and literals by value. Numeric helpers therefore takeimpl fmt::Num<i64>/impl fmt::Num<usize>, which accepts both. Follow that pattern for new numeric helpers. String helpers take&str. - Methods on view structs are fine too (
r.base(),r.pull_url(pr.number),f.display_path(),l.class()). - Use
urlpathfor ref names and file paths in URL paths (keeps/, encodes spaces,#and so on). Useurlencodefor query-string values. - Only mark output
|safewhen it's HTML we generate:icon,avatar, highlighted lines, andmarkdown::renderoutput. Never put|safeon user-provided strings.
Syntax that works here
{% if let Some(x) = opt %}…{% endif %},{% else if %},{% for (a, b) in pairs %},loop.index,loop.last.{% match value %}{% when crate::web::code::BlobContent::Code with { lines } %}…{% endmatch %}. Name enum variants by full path.- Comparing an
Option<enum>:data.mergeability == Some(crate::git::Mergeability::Clean). - Comparing a loop item against a field:
{% if *b == view.base %}.
Markup conventions
- There is no inline JavaScript (the CSP forbids it). Use the
data-*hooks fromstatic/app.js, which are listed in the rootCLAUDE.md. Destructive forms getdata-confirm="…". - Dropdowns are
<details class="dropdown">with a<summary>and a.dropdown-menu, so they work without JS. - Show empty states with
<div class="blankslate">(an icon, an<h3>and a<p>). - Show errors on re-rendered forms with
<div class="flash flash-error">. For messages after a redirect, useredirect_flashin the handler instead. - Forms POST to same-origin paths, and every page must still work with JS disabled.