samjeston / openhub Public
Sam Jeston Working vertical 9aa3c45 · History
62 lines· 4.3 KB ·Markdown
1# Templates (askama 0.16)
2
3Templates 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```
8base.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
29Bind 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
33Helpers 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.