Building pages
A page is any .html, .md, or engine-native (.njk/.liquid) file under your markup in
directory. Its output path mirrors its source path: src/markup/about.md โ dist/about.html.
Front matter
Start a page with a YAML front-matter block. layout picks the wrapping template; everything else
is available under page.*:
---
layout: default
title: About
description: Who we are and why.
order: 2
---
# About us
We build things with Poops.
Common fields: title, description, layout, date, order, published, nav, navTitle.
Any custom field you add is yours to use in templates and it flows into the search index.
Poops also computes read-only fields on page: content (the rendered body), url, wordcount,
excerpt, and filePath. excerpt is the first prose paragraph as plain text (headings, comments
and code skipped, capped at 160 chars) โ the fallback for a missing description, used by the
description, og and jsonld filters. filePath is the source file's path relative to your
project root (posix separators), for building "Edit on GitHub" links โ see
Building a documentation site.
The excerpt describes what a reader gets, so it is taken after the template engine has run: a first
paragraph written as {{ site.description }}, or supplied by an
{% include %}, is resolved before the text is taken. When it resolves to
nothing usable the excerpt is empty and description falls through to site.description โ never to
the tag's source text. Heading ids come from the same place: a heading written as
# {{ site.title }} anchors at the words it renders, so swapping which
variable feeds it does not remint the URL.
Layouts
Put base templates in _layouts/ (a directory ignored for output, but on your includePaths). A
Nunjucks layout defines a content block:
<!DOCTYPE html>
<html lang="{{ page.lang or site.lang or 'en' }}">
<head>
<meta charset="UTF-8">
<title>{{ page.title or site.title }}</title>
{{ page | description(site) }}
<link rel="stylesheet" href="{{ relativePathPrefix }}css/styles.min.css">
</head>
<body>
{% include "site-header.html" %}
<main>{% block content %}{% endblock %}</main>
{% include "site-footer.html" %}
</body>
</html>
The page body is rendered, then dropped into {% block content %}.
Tip
Always prefix asset and link URLs with relativePathPrefix. It resolves to the correct number
of ../ for the page's depth, so a page at dist/blog/post.html still finds css/styles.css.
Partials & includes
Reusable snippets live in _partials/ (also on includePaths). Include them by file name:
{% include "site-header.html" %}
In Liquid, use render:
{% render "site-header.liquid" %}
Global and page data
Three sources of data reach your templates:
siteโ set once in the markup config (site.title,site.url, โฆ).datafiles โ JSON/YAML loaded as globals named after the file._data/links.jsonbecomeslinks, so{{ links.github }}works everywhere.pageโ the current page's front matter.
{
"markup": {
"in": "src/markup",
"out": "dist",
"options": {
"site": { "title": "My Site" },
"data": ["_data/links.json", "_data/authors.yaml"]
}
}
}
Note
File names are normalized: spaces, dashes and dots become underscores. the awesome-links.json
is available as {{ the_awesome_links }}.
Markdown
Markdown files are rendered to HTML and then run through the template engine, so template expressions work inside Markdown too. Fenced code blocks are syntax-highlighted at build time (highlight.js) โ you ship a CSS theme, not a highlighter.
```js
const greet = (name) => `Hello, ${name}!`;
```
Info
Registered highlight languages include js, ts, css, scss, html, json, bash,
python, ruby, php, go, rust, yaml, sql, diff and more. Omit the language to let
highlight.js auto-detect.
Mermaid diagrams are the exception. highlight.js has no mermaid grammar, so a ```mermaid fence
would fall through to auto-detection and come back wrapped in spans for a language it guessed โ
it is left alone instead, and compiles to <pre class="mermaid">, which is the markup
mermaid looks for.
flowchart TD poops[poops] ==>|builds| theme[poops-docs-theme] theme -->|documents| poops
Poops ships no mermaid and injects no script โ load it on the pages that want diagrams, and no page without one carries the library. A fence on a page that never loads mermaid shows its diagram source as text, which reads on its own.
This page loads it, so the fence above renders. The script is written into the Markdown source by hand โ Markdown passes raw HTML through, so a page that wants diagrams carries its own loader and no other page pays for it:
<script type="module">
import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs'
const nodes = document.querySelectorAll('pre.mermaid')
for (const n of nodes) n.dataset.src ??= n.textContent
const render = () => {
mermaid.initialize({ startOnLoad: false, theme: document.documentElement.dataset.theme === 'dark' ? 'dark' : 'default' })
for (const n of nodes) { n.innerHTML = n.dataset.src; n.removeAttribute('data-processed') }
mermaid.run({ nodes })
}
render()
new MutationObserver(render).observe(document.documentElement, { attributeFilter: ['data-theme'] })
</script>
Info
The source is stashed in data-src before the first render because mermaid replaces the
element with its SVG. Without the copy, the theme toggle hands mermaid its own output to parse
the second time round.
Useful filters
Poops adds template filters usable in both engines โ slugify, markdown, toc, date,
jsonify, svg, highlight, groupby, the array helpers concat (returns a new array with the
value appended) and push (appends in place), and the image helpers srcset, exif, images:
<h1>{{ page.title }}</h1>
<time>{{ page.date | date("MMMM D, YYYY") }}</time>
{{ "src/icons/logo.svg" | svg }}
For markdown source, run markdown before toc so code-fence content doesn't get misread as headings:
{{ page.content | markdown | toc }}
Next: Images & galleries.