Templating
Templates live in the templates/
directory:
~/bearcove/home-from-scratch
❯ ls templates/
page.html.jinja shortcodes
Templates on disk have the .jinja
extension, to:
- Emphasize that they’re using the Jinja templating language
- Enable syntax highlighting in supported editors.
The Zed code editor supports Jinja syntax highlighting.
I'm sure VS Code does too.
Macros
Via minijinja, you have the full power of jinja available, including defining macros:
{% macro youtube_embed(id, alt="YouTube Video Thumbnail") %}
<div class="youtube-thumbnail-link paragraph-like">
<a href="https://www.youtube.com/watch?v={{ id }}{{ extra or '' }}" target="_blank" rel="noopener" class="noclip" data-youtube-id="{{ id }}" data-extra="{{ extra or '' }}">
<div class="thumbnail-container" style="position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden;">
<img
src="https://img.youtube.com/vi/{{ id }}/maxresdefault.jpg"
alt="{{ alt | escape_for_attribute }}"
style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; object-fit: cover; overflow: hidden; border-radius: 8px;"
onerror="this.onerror=null; this.src='https://img.youtube.com/vi/{{ id }}/0.jpg';"
/>
<div class="play-button" style="position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 68px; height: 48px; background-color: rgba(0,0,0,0.7); border-radius: 8px; display: flex; justify-content: center; align-items: center;">
<div style="width: 0; height: 0; border-style: solid; border-width: 10px 0 10px 20px; border-color: transparent transparent transparent #fff;"></div>
</div>
</div>
</a>
</div>
{% endmacro %}
Defining shortcodes
Shortcodes are just templates defined in templates/shortcodes
.
There are two shortcodes that home kind of expects: templates/shortcodes/media.html.jinja
:
<p>
{%- set title_attr = title | escape_for_attribute -%}
{%- set alt_attr = alt | escape_for_attribute -%}
{{- get_media(src).markup(title=title_attr, alt=alt_attr, width=width, height=height, class=class) -}}
</p>
And templates/shortcodes/figure.html.jinja
:
<figure>
{%- set title_attr = title | escape_for_attribute -%}
{%- set alt_attr = alt | escape_for_attribute -%}
{{- get_media(src).markup(title=title_attr, alt=alt_attr, width=width, height=height, class=class) -}}
<figcaption>{{ title | basic_markdown | safe }}</figcaption>
</figure>
You can make those as fancy as you want.
You can import and call macros from shortcodes, for example,
templates/shortcodes/youtube.html.jinja
could be:
{% import "macros.html" as macros %}
{{ macros.youtube_embed(id, alt=alt) }}
Invoking shortcodes
There are two ways to invoke shortcodes, depending if they have a body or not.
On https://fasterthanli.me, the bearsays
shortcode is defined as:
{% import "macros.html" as macros %}
{% if mood is not defined %}
{% set mood = "neutral" %}
{% endif %}
<div class="dialog">
<div class="dialog-head" title="Cool bear says:">
{{ get_media("/content/img/reimena/cool-bear-" ~ mood ~ ".jxl").markup(width=42, height=42, alt="Cool bear") }}
</div>
<div class="dialog-text markup-container">
{{ body }}
</div>
</div>
Notice {{ body }}
— it’s invoked like so:
> *:bearsays*
>
> It looks like this!
Main Template Types
Here are the main types you’ll be using in templates. They’re pretty straightforward, but let’s go through some examples.
LoadedPage
A single page on the site. Could be an article, a series part, whatever.
Properties:
path
(String): Content path of the pageroute
(String): URL route for the pageurl
(String): Full URL to the pagetitle
(String): Page titlehtml
(HTML String): Full HTML contenthtml_until_playwall
(HTML String): HTML content up to the paywall markerhtml_until_more
(HTML String): HTML content up to a<!-- more -->
markerplain_text
(String): Plain text content without HTMLshort_desc
(String): Truncated description for meta tagsdate
(DateTime): Publication dateupdated_at
(DateTime, optional): Last update datereading_time
(Number): Estimated reading time in minutestags
(Array of String): Tags associated with the pagedraft
(Boolean): Whether the page is a draftarchive
(Boolean): Whether the page is archivedthumb
(MediaVal, optional): Thumbnail imageparent_thumb
(MediaVal, optional): Parent page’s thumbnailtoc
(Array): Table of contents entriesseries_link
(Object, optional): Information about series, if page is part of onecrates
(Array): Referenced Rust cratesgithub_repos
(Array): Referenced GitHub repositorieslinks
(Array): External links referencedis_old
(Boolean): True if the page is over two years oldexclusive_until
(DateTime, optional): When exclusive content becomes publicvideo_info
(Object): Video-related information
Methods:
get_listing(page_number, per_page)
: Returns aListing
object with child pagesget_children()
: Returns child pages as an array ofLoadedPage
objects
Example:
<h1>{{ page.title }}</h1>
<div class="date">{{ page.date | format_day_month_year }}</div>
<div class="content">{{ page.html }}</div>
Listing
A collection of pages. Used for article lists, series, search results, that kinda thing.
Properties:
kind
(String): Type of listing (“articles”, “episodes”, “series”, “series-parts”)items
(Array ofLoadedPage
): List of page objectspage_number
(Number): Current page numberper_page
(Number): Items per pagehas_more
(Boolean): Whether there are more pages
Example:
{% for article in listing.items %}
<h2><a href="{{ article.url }}">{{ article.title }}</a></h2>
{% endfor %}
{% if listing.has_more %}
<a href="?page={{ listing.page_number + 1 }}">Next page</a>
{% endif %}
MediaVal
A media asset. Usually an image.
Properties:
width
(Number): Width in pixelsheight
(Number): Height in pixels
Methods:
markup(width, height, alt, title, id, class)
: Renders HTML markup for the mediabitmap_variant_url(codec)
: Returns URL for a specific variant of the media
Example:
{{ page.thumb.markup(alt="Thumbnail", width=300) }}
<img src="{{ page.thumb.bitmap_variant_url('webp') }}">
SearchResults
Search results. Pretty self-explanatory.
Properties:
results
(Array ofSearchResult
): Search result itemsnum_results
(Number): Total number of resultsterms
(Array of String): Search termshas_more
(Boolean): Whether there are more results
SearchResult
A single search result.
Properties:
page
(LoadedPage
): The found pagetitle_snippet
(HTML String): Highlighted title snippetbody_snippet
(HTML String): Highlighted body snippet
Globals
Global site info and utilities.
Properties:
page
(LoadedPage
, optional): Current pageuser_info
(Object, optional): Current user informationviewer
(Object): Current viewer propertiesconfig
(Object): Site configurationsponsors
(Array): Site sponsors
Methods:
random_article()
: Returns a randomLoadedPage
get_tag_listing(tag, page_number, per_page)
: Returns aListing
for a tagsearch_page(query, per_page, page_number)
: ReturnsSearchResults
Example:
{% set random = globals.random_article() %}
<a href="{{ random.url }}">{{ random.title }}</a>
DateTime
A date and time. You’ll mostly use it through filters.
Methods exposed through filters:
format_day_month_year()
: Returns “Mon DD, YYYY” formatformat_month_year()
: Returns “Month YYYY” formatformat_time_ago()
: Returns human-readable relative timeformat_rfc3339()
: Returns RFC3339 formatted dateis_future()
: Returns whether date is in the future
Built-in Functions
Jinja 2 has a bunch of built-in functions. Here are the ones that Home adds on top:
asset_url(path)
Gets a URL for an asset, with cache busting. Works with relative paths.
<link rel="stylesheet" href="{{ asset_url('/content/css/style.css') }}">
get_media(path)
Gets a MediaVal
object for a path. Works for images and other media.
{{ get_media("/content/images/logo.png").markup(alt="Logo", width=200) }}
get_recent_pages()
Gets the 25 most recent published articles and series parts. Useful for RSS feeds.
{% for page in get_recent_pages() %}
<item>
<title>{{ page.title }}</title>
<link>{{ page.url }}</link>
<pubDate>{{ page.date | format_rfc3339 }}</pubDate>
</item>
{% endfor %}
url_encode(string)
Encodes a string for use in URLs.
<a href="/search?q={{ url_encode(query) }}">Search results</a>
html_escape(string)
Escapes HTML special characters.
<div data-content="{{ html_escape(content) }}"></div>
html_until_playwall
Render the page until reaching the <!-- playwall -->
marker
{{ page.html_until_playwall }}
html_until_more
Render the page until reaching the <!-- more -->
marker, similar to Zola
{{ page.html_until_more }}
get_page_from_route(route)
Gets a LoadedPage
object from a website route.
{% set about_page = get_page_from_route("/about") %}
<a href="{{ about_page.url }}">{{ about_page.title }}</a>
get_page_from_path(path)
Gets a LoadedPage
object from a content path.
{% set article = get_page_from_path("/content/articles/rust-performance.md") %}
<h2>{{ article.title }}</h2>
all_icons()
Gets all available syntax highlighting icons.
{% for icon in all_icons() %}
<i class="icon-{{ icon }}"></i>
{% endfor %}
basic_markdown(text)
Renders markdown to HTML.
{{ basic_markdown("**Bold text** and _italic text_") | safe }}
random_article()
Gets a random Rust-tagged article.
{% set random = random_article() %}
<div class="random-recommendation">
<h3>Random article: <a href="{{ random.url }}">{{ random.title }}</a></h3>
</div>
get_tag_listing(tag, page_number=1, per_page=25)
Gets a Listing
object with paginated content for a specific tag.
{% set rust_articles = get_tag_listing(tag="rust", page_number=1, per_page=10) %}
<ul>
{% for article in rust_articles.items %}
<li><a href="{{ article.url }}">{{ article.title }}</a></li>
{% endfor %}
</ul>
{% if rust_articles.has_more %}
<a href="?page={{ rust_articles.page_number + 1 }}">Next page</a>
{% endif %}
search_page(query, per_page, page_number)
Gets a SearchResults
object with pages matching a query.
{% set results = search_page(query="Rust performance", per_page=10, page_number=1) %}
<div class="search-results">
{% for result in results.results %}
<div class="result">
<h3><a href="{{ result.page.url }}">{{ result.title_snippet | safe }}</a></h3>
<p>{{ result.body_snippet | safe }}</p>
</div>
{% endfor %}
</div>
Built-in Filters
asset_url(path)
Same as the asset_url function, but as a filter. Gets a URL with cache busting.
<img src="{{ '/content/images/logo.png' | asset_url }}">
url_encode(string)
Encodes a string for use in URLs.
<a href="/search?q={{ query | url_encode }}">Search</a>
html_escape(string)
Escapes HTML special characters.
<div data-content="{{ content | html_escape }}"></div>
truncate_html(html, max=300)
Truncates HTML content while preserving structure. Default limit is 300 characters.
{{ article.content | truncate_html(max=150) | safe }}
truncate(text, len)
Truncates text to a specified length, adding “…” if truncated.
{{ article.description | truncate(len=100) }}
downcase(string)
Converts a string to lowercase.
<span class="tag">{{ tag | downcase }}</span>
shuffle(list)
Randomly shuffles a list.
{% for item in items | shuffle %}
<li>{{ item }}</li>
{% endfor %}
urlencode(string)
Encodes a string for use in URLs (same as url_encode).
<a href="https://example.com/?q={{ search_term | urlencode }}">Search</a>
to_json(value)
Converts a value to pretty-printed JSON.
<script>
const data = {{ page_data | to_json | safe }};
</script>
basic_markdown(text)
Renders markdown to HTML.
{{ comment.body | basic_markdown | safe }}
escape_for_attribute(string)
Escapes a string for use in HTML attributes. Replaces newlines with spaces and double quotes with single quotes.
<button title="{{ description | escape_for_attribute }}">More info</button>
format_time_ago(datetime)
Formats a date as a human-readable relative time (e.g., “2 days ago”).
<span class="timestamp">{{ article.date | format_time_ago }}</span>
format_rfc3339(datetime)
Formats a date in RFC3339 format.
<time datetime="{{ article.date | format_rfc3339 }}">{{ article.date | format_time_ago }}</time>
format_month_year(datetime)
Formats a date as “Month Year”.
<span class="date">{{ article.date | format_month_year }}</span>
format_day_month_year(datetime)
Formats a date as “Mon DD, YYYY”.
<span class="date">{{ article.date | format_day_month_year }}</span>
is_future(datetime)
Checks if a date is in the future.
{% if article.date | is_future %}
<span class="badge">Upcoming</span>
{% endif %}