Sam Jeston
History
| 1 | # Templates (askama 0.16) |
| 2 | |
| 3 | 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`. |
| 4 | |
| 5 | ## Layouts |
| 6 | |
| 7 | ``` |
| 8 | base.html blocks: title, page_class, subheader, content |
| 9 | ├── layout_repo.html repository header and tabs; blocks: page_title, content |
| 10 | └── layout_settings.html account settings side nav; block: settings |
| 11 | ``` |
| 12 | |
| 13 | - Every page struct needs `ctx: Ctx`, because `base.html` reads `ctx.user`, `ctx.flash` and `ctx.path`. |
| 14 | - Repo pages also need `r: RepoCtx` and `tab: &'static str`. `tab` is one of `code`, `commits`, `branches`, `pulls` or `settings` and selects the active tab. |
| 15 | - Settings pages need `tab` (`profile` or `keys`). |
| 16 | - In repo pages, `page_title` is a prefix ending in ` · `, e.g. `{% block page_title %}Branches · {% endblock %}`. |
| 17 | - **Never create a file named `base.html` in a subdirectory.** askama resolves `{% extends %}` relative to the current template's directory first, so `repo/*.html` extending `"base.html"` would pick up `repo/base.html` and fail with a cyclic dependency. That's why section layouts are named `layout_*.html` and kept at the root. |
| 18 | |
| 19 | ## Partials (`{% include %}`, which share the including template's scope) |
| 20 | |
| 21 | | Partial | Variables it expects in scope | |
| 22 | | --- | --- | |
| 23 | | `partials/diff.html` | `diff: DiffSummary`, `file_ref` (commit SHA or URL-encoded ref to link "View file" at), `base` (repo URL) | |
| 24 | | `partials/commit_list.html` | `groups: Vec<CommitGroup>`, `base` | |
| 25 | | `partials/refsel.html` | `refsel: RefSelector` | |
| 26 | | `partials/clone.html` | `r: RepoCtx`, `view` with a `refname` field (used for archive links) | |
| 27 | | `partials/repo_list.html` | `repos: Vec<Repo>` | |
| 28 | |
| 29 | Bind them with `{% let %}` before including, e.g. `{% let file_ref = commit.id.clone() %}`, or `{% let repos = recent %}` to reuse the repo list. |
| 30 | |
| 31 | ## Calling Rust from templates |
| 32 | |
| 33 | Helpers live in `src/web/fmt.rs` and are called by full path: |
| 34 | |
| 35 | ```jinja |
| 36 | {{ crate::web::fmt::icon("git-branch")|safe }} {# names: match arms in src/icons.rs #} |
| 37 | {{ crate::web::fmt::avatar(user.username, 20)|safe }} |
| 38 | <time title="{{ crate::web::fmt::datetime_full(c.committer_time) }}">{{ crate::web::fmt::ago(c.committer_time) }}</time> |
| 39 | {{ crate::web::fmt::plural(view.commit_count, "commit") }} {# "1 commit", "3 commits", "2 branches" #} |
| 40 | {{ base }}/tree/{{ crate::web::fmt::urlpath(view.refname) }}/{{ crate::web::fmt::urlpath(entry.path) }} |
| 41 | /login?next={{ crate::web::fmt::urlencode(ctx.path) }} |
| 42 | ``` |
| 43 | |
| 44 | - askama passes struct fields by reference (`&c.committer_time`) but method-call results and literals by value. Numeric helpers therefore take `impl fmt::Num<i64>` / `impl fmt::Num<usize>`, which accepts both. Follow that pattern for new numeric helpers. String helpers take `&str`. |
| 45 | - Methods on view structs are fine too (`r.base()`, `r.pull_url(pr.number)`, `f.display_path()`, `l.class()`). |
| 46 | - Use `urlpath` for ref names and file paths in URL paths (keeps `/`, encodes spaces, `#` and so on). Use `urlencode` for query-string values. |
| 47 | - Only mark output `|safe` when it's HTML we generate: `icon`, `avatar`, highlighted lines, and `markdown::render` output. Never put `|safe` on user-provided strings. |
| 48 | |
| 49 | ## Syntax that works here |
| 50 | |
| 51 | - `{% if let Some(x) = opt %}…{% endif %}`, `{% else if %}`, `{% for (a, b) in pairs %}`, `loop.index`, `loop.last`. |
| 52 | - `{% match value %}{% when crate::web::code::BlobContent::Code with { lines } %}…{% endmatch %}`. Name enum variants by full path. |
| 53 | - Comparing an `Option<enum>`: `data.mergeability == Some(crate::git::Mergeability::Clean)`. |
| 54 | - Comparing a loop item against a field: `{% if *b == view.base %}`. |
| 55 | |
| 56 | ## Markup conventions |
| 57 | |
| 58 | - There is no inline JavaScript (the CSP forbids it). Use the `data-*` hooks from `static/app.js`, which are listed in the root `CLAUDE.md`. Destructive forms get `data-confirm="…"`. |
| 59 | - Dropdowns are `<details class="dropdown">` with a `<summary>` and a `.dropdown-menu`, so they work without JS. |
| 60 | - Show empty states with `<div class="blankslate">` (an icon, an `<h3>` and a `<p>`). |
| 61 | - Show errors on re-rendered forms with `<div class="flash flash-error">`. For messages after a redirect, use `redirect_flash` in the handler instead. |
| 62 | - Forms POST to same-origin paths, and every page must still work with JS disabled. |