NAME

Punk::Mount::Markdown - a directory of markdown as a documentation site

SYNOPSIS

package MyApp;
use Punk;

markdown '/docs' => 'docs', title => 'MyApp Guide';

1;

DESCRIPTION

Point the markdown keyword at a nested directory of .md files and it becomes a complete documentation site: rendered HTML with syntax-highlighted code, a sidebar reflecting the directory tree, a per-page table of contents built from the headings, ranked search, and the images sitting alongside the markdown served as static files.

The whole site is built at boot. The tree is walked, every page is rendered through Markdown::Simple and wrapped by Template::Stencil, the search index is filled, and the finished bytes are frozen into a lookup table. A request is then a hash hit and a response triplet, with no markdown parse, no template render and no Perl frame. The cost of that is a startup pass linear in the size of the tree, paid once per worker; see "reload" for the edit loop.

Building at boot also means a missing directory, an unreadable document or a broken template fails at startup with the rest of your configuration, rather than on whichever request first happens to reach it.

The mount rides Punk's ordinary mount table, so it behaves like static does: longest prefix wins, and a request served by the mount bypasses before/after hooks, guards and CSRF.

URLS

A page's address is its path under the directory with the .md dropped, and an index file collapses to its containing directory:

docs/index.md              ->  /docs
docs/guide/intro.md        ->  /docs/guide/intro
docs/guide/index.md        ->  /docs/guide

A request carrying a trailing slash or a .md suffix is redirected to the canonical form, so a document never has two addresses. Anything in the tree that is not markdown is served as a static file with the same caching, conditional-request and sendfile behaviour Punk::Static gives it, which is what makes a relative ![](diagram.png) in a document work.

FRONT MATTER

A document may open with a restricted --- block:

---
title: Getting Started
nav: Start here
order: 1
---

# Getting Started

Recognised keys are title, nav (the sidebar label, when it should differ from the title), order (the sort key within a section) and draft (when true, the page is skipped entirely). Anything else is ignored rather than an error, so a document carrying front matter for some other tool still renders.

This is deliberately not YAML. Running a YAML parser on every page at boot to read two scalars would put a dependency on the C path for no benefit.

Where there is no title, the first # heading is used, and failing that the filename with hyphens and underscores turned into spaces.

OPTIONS

markdown '/docs' => 'docs',
    title    => 'MyApp Guide',
    reload   => 1,
    edit_url => 'https://github.com/me/app/edit/main/docs/%s';
  • title - the site name, shown in the header and the page title. Defaults to the mount prefix.

  • index - the filename that means "this directory". Default index.md.

  • recursive - descend into subdirectories. Default true.

  • sort - order (default) groups pages by section and orders them by front-matter order:, or alpha to keep the plain path order.

  • reload - re-stat a page on request and re-render it when the source has changed. Off by default, since the point of the mount is that pages are frozen; turn it on for the edit loop under punk dev.

  • highlight - syntax-highlight fenced code blocks. Default true.

  • gfm - GitHub Flavored Markdown. Default true.

  • toc - build a per-page table of contents from the h2 and h3 headings. Default true.

  • search - build a Search::Trigram index at boot and serve a search page from it. Default true.

  • search_path - where that page lives, relative to the mount. Default /search.

  • template_dir - your own templates instead of the shipped ones. The directory must supply page.tmpl, wrapper.tmpl and app.css; see "TEMPLATES".

  • wrapper - the wrapper template name. Default wrapper.tmpl.

  • assets - where the shipped stylesheet is served from, relative to the mount. Default /_punk.

  • footer - raw HTML for the page footer.

  • headers - a hashref of extra response headers to send with every page.

TEMPLATES

The shell is rendered by Template::Stencil. The shipped templates live in templates/ beside this module; point template_dir at a directory of your own and it replaces them, so that directory has to supply all three files:

  • page.tmpl

    The body of a page. It receives the data below and is expected to emit {% raw content %} somewhere. This one template renders all three kinds of response - a document, the search results and the 404 - so branch on kind if they should differ.

  • wrapper.tmpl

    The surrounding document: <html>, the header, the sidebar, and {% content %} where page.tmpl's output is slotted in. It receives the same data. Rename it with the wrapper option if you would rather call it something else.

  • app.css

    The stylesheet, served from the mount's assets path. It is read verbatim rather than rendered, so it is a plain CSS file and its braces mean nothing to the template engine. It lives with the templates because a caller replacing the shell almost always wants to replace its styling too.

Both templates receive:

  • kind - page, search or notfound

  • title - the page title

  • site_title - the site name, from the title option

  • content - the rendered markdown, to be emitted with {% raw content %}

  • nav - the sidebar HTML, likewise raw

  • toc - the table of contents HTML, likewise raw, or empty

  • url - the current page's mount-relative address

  • prefix - the mount prefix, for building links

  • search - true when a search index was built

  • search_path, assets - both mount-relative

  • footer - raw HTML, or empty

The navigation and the table of contents arrive as ready HTML rather than as data to loop over because Stencil has no recursion and no dynamic includes, so an arbitrarily deep tree cannot be walked in a template. They are rebuilt per page at boot, which is what lets the current entry carry a current class without any JavaScript.

If you only want to restyle the site, supply your own app.css and leave the templates alone: copy the shipped templates/ directory, replace the stylesheet, and point template_dir at your copy.

SEE ALSO

Punk, Punk::Static, Markdown::Simple, Template::Stencil, Search::Trigram

AUTHOR

LNATION <email@lnation.org>

LICENSE AND COPYRIGHT

Copyright (c) 2026 LNATION.

This is free software; you can redistribute it and/or modify it under the same terms as the Perl 5 programming language system itself.