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 and comments skipped, capped at 160 chars) โ€” use it as the fallback for a missing description, e.g. in the meta tag below or the og/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.

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>
  <meta name="description" content="{{ page.description or page.excerpt or site.description }}">
  <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, โ€ฆ).
  • data files โ€” JSON/YAML loaded as globals named after the file. _data/links.json becomes links, 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.

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.