Layouts
Blogin renders pages with its own HAML engine. A build wraps each post's
rendered body in show.haml inside base.haml, and each section listing in
index.haml inside base.haml.
HAML Compatibility lists every construct the
engine reads, and Template Expressions is
the language inside =, -, and #{}.
The layout files#
| File | Role |
|---|---|
base.haml |
Page shell, wraps everything. Required. |
show.haml |
A single post. Required. |
index.haml |
A section or taxonomy listing. Required for listings. |
home.haml |
The site root. Optional, falls back to index.haml. |
tag.haml, tags.haml |
A single tag's posts, and the tag index. Optional, fall back to index.haml. |
404.haml |
The not-found page. Optional, falls back to a built-in page. |
_header.haml, _footer.haml |
Optional chrome, pulled in by base.haml. |
_sidebar.haml |
Optional sidebar, rendered when your base.haml includes it. |
_nav.haml, _search.haml |
The nav menu and search form partials. The scaffold puts search in the navbar. |
A chrome partial that is not there is skipped rather than reported, so a site
without a sidebar renders full width. A missing base.haml, show.haml, or index.haml
is a build error that names the layout.
Rendering shared chrome once#
A partial that renders the same on every page, such as a sidebar of links or a
footer, is otherwise re-rendered once per page. Wrap it in cache-fragment to
render it once per build and reuse the result:
!= cache-fragment('sidebar', { render(:partial<sidebar>) })
The name you give is advisory. What decides whether the rendered bytes are reused is the set of values the fragment read while rendering. A sidebar reading only site-level values renders once for the whole build and every later page reuses it. One reading the current url or the current post renders per page, and gets its own entry each time. On a site with a heavy sidebar and hundreds of pages, that turns hundreds of renders into one.
Asking for reuse cannot produce a stale fragment, so you never have to work out in advance whether a fragment is safe to cache. Wrapping one that varies costs a little bookkeeping and nothing else. The cache is cleared at the start of each build, so an edit to the partial or its data is picked up on the next build.
The home page#
The site root renders through home.haml when it exists, falling back to
index.haml. This gives the root a distinct landing page, a hero and feature
callouts, separate from the home-section listing that the same posts produce at
their section path. Like a listing, home.haml can reach posts and
pagination-html, or ignore them for a static landing.
The 404 page#
Every build writes 404.html at the site root, which hosts like GitHub Pages and
Netlify serve for a missing URL. It renders through 404.haml wrapped in
base.haml when that layout exists, and falls back to a plain built-in page
otherwise.
Per-section layouts#
A section overrides a layout by placing it in a matching subdirectory. A post in
content/essays/ renders through layouts/essays/show.haml when present,
otherwise layouts/show.haml, resolved by the nearest ancestor. The same applies
to index.haml and the chrome partials.
A section can also name a layout in blogin.json rather than by directory. Give
the section a layout key and its posts render through layouts/<name>.haml,
falling back to show.haml when that file is absent:
"sections": {
"essays": { "layout": "essay" }
}
Posts under content/essays/ then render through layouts/essay.haml, resolved
along the same nearest-ancestor search path as the directory-based override.
Table of contents#
A post view exposes toc-html, a nested list of the post's headings linked to
their anchors, and has-toc, true when the post's front matter sets toc: true.
A show layout renders one by gating on the flag:
- if has-toc
%nav.toc
!= toc-html
The list nests by heading level and each entry links to the heading's slug id, the same id the renderer puts on the heading itself.
What a template can reach#
Templates read post fields and site data as bare identifiers: title, date,
body, site-title, section. A single post exposes body (the rendered HTML,
injected unescaped) and post-nav-html, links to the newer and older posts in the
same section. It also exposes toc-html and has-toc for a table of contents,
word-count and reading-time (whole minutes), and related with has-related,
a list of posts sharing taxonomy terms, most-shared first, ready for the entry
partial. It also exposes tags with has-tags, the post's own tags as
{ name, url } links to their term pages. A listing exposes heading (its h1
text), posts, and pagination-html. The nav tree is available as nav-nodes,
with nav-current(node) marking the current section.
Template Data is the complete reference: every method on each context, and the shape of each collection. A name the view does not offer is a build error naming the file, line, and column, with a suggestion when it is close to a real one.
Color theme#
Every layout can offer a light and dark toggle with two methods. Put
theme-script in the <head> and a theme-toggle button in the navbar:
%head
!= theme-script
%nav
!= render(:partial<nav>)
!= theme-toggle
theme-script inlines a small script that runs before the page paints. It reads
the saved choice from localStorage and otherwise defaults to light, then sets
data-theme and data-bs-theme on the <html> element, so there is no flash of
the wrong theme. theme-toggle renders a moon and sun button
that flips the theme and remembers the choice.
Styling follows the attribute. The Bootstrap profile reads data-bs-theme and
themes itself. Under the none profile the generated style.css defines the page
colors for [data-theme="dark"], and Blogin's own stylesheet themes its
components (code blocks, definition lists, the toggle) the same way. Target
[data-theme="dark"] in your own CSS to extend it. The scaffold wires all of this
into a fresh site, so a new build has a working toggle before you edit anything.
Template filters#
Layouts can call a few helper functions on any value:
format-date(date, "%B %e, %Y")formats aYYYY-MM-DDdate with a strftime subset (%Y %m %d %e %B %b %A %a).truncate(text, 80)shortens text at a word boundary with an ellipsis.group-by(posts, "date")groups a list of entries by a field into{ key, items }pairs, newest key first, for archive-style listings.