No results

    Search failed

    npm GitHub

    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, โ€ฆ).
    • 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.

    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.