docs/content/templates/functions.md (3506B)
1 +++
2 title = "Functions and filters"
3 description = "Everything a template can call, and the closing line that nothing else exists"
4 weight = 40
5 toc = true
6 +++
7
8 ## Provided by sqzass
9
10 Two functions. That is the whole list.
11
12 ### asset(path)
13
14 Returns the URL a static file was written to, hash and subpath included.
15
16 ```html
17 <link rel="stylesheet" href="{{ asset("css/main.css") }}">
18 <script src="{{ asset("js/search.js") }}" defer></script>
19 ```
20
21 The argument is the logical path under `static/`, with or without a leading
22 slash. A name that was not collected is a build error listing every name that
23 was — a renamed stylesheet fails the build instead of 404ing for every visitor.
24
25 ### t(key)
26
27 Looks a UI string up in `i18n/<language>.toml`, for the language of the page
28 being rendered.
29
30 ```html
31 <a class="skip" href="#content">{{ t("skip_to_content") }}</a>
32 ```
33
34 One argument. The language is never passed in — see
35 [Languages](@/content/languages.md) for why that is deliberate. A key missing
36 from the current language is a build error.
37
38 > [!WARNING]
39 > Do not name a loop variable `t`. `{% for t in page.translations %}` shadows
40 > the function inside that block, and the first person to add a label there gets
41 > an error a long way from its cause.
42
43 ## From minijinja
44
45 Standard Jinja2 syntax works: `{% if %}`, `{% for %}`, `{% extends %}`,
46 `{% block %}`, `{% include %}`, `{% macro %}`, `{% from … import … %}`, `{% set %}`,
47 and the usual filters — `safe`, `escape`, `length`, `join`, `default`, `upper`,
48 `lower`, `replace`, `trim`, `first`, `last`, `reverse`, `sort`, `map`,
49 `select`, `selectattr`, `batch`, `slice`, `int`, `float`, `abs`, `round`,
50 `indent`.
51
52 Our own templates use `{% macro %}` and `{% from "partials/sidebar.html" import nav %}`,
53 so those two are exercised on every build.
54
55 Three filters a Jinja2 habit reaches for are **not** here: `tojson`,
56 `urlencode` and `striptags`. Each is behind a minijinja feature we leave off,
57 and each is a dependency for something already covered — a `{% for %}` loop, a
58 URL that arrives ready to use, or markup you did not have to strip because you
59 wrote it. Calling one is a build error, not an empty string, so you find out at
60 build time rather than in the page.
61
62 ## Nothing else exists
63
64 There is no `url_for`, no `markdownify`, no `date`, no `now()`, no `env()`, no
65 custom tests. Some of those are absences with reasons rather than gaps:
66
67 `now()` and `env()` would break the guarantee that two builds of the same input
68 produce identical bytes, which CI checks on every push. A template that can read
69 the clock cannot be reproducible.
70
71 `url_for` would wrap a comparison that is already one line. Page and section
72 URLs arrive ready to use, and they already carry the subpath.
73
74 Reading a name that does not exist is a build error, not an empty string — so
75 if you call something from another generator by habit, you find out at build
76 time rather than from a page with a hole in it.
77
78 ## Marking the current page
79
80 There is no helper for this, and none is needed. Exact match for a page:
81
82 ```html
83 <a href="{{ p.url }}"{% if p.url == page.url %} aria-current="page"{% endif %}>{{ p.title }}</a>
84 ```
85
86 Ancestor match for a section, using `page.section`:
87
88 ```html
89 <a href="{{ s.url }}"{% if page.section and page.section.url == s.url %} aria-current="true"{% endif %}>{{ s.title }}</a>
90 ```
91
92 For a prefix test use `startingwith`, but be careful with the root: every URL
93 starts with `/`, so the home link must be compared exactly and never as an
94 ancestor.