A markdown docs site

This site - the one you are reading - is a Punk application, and these docs are one keyword of it:

markdown '/docs' => 'docs',
    title        => 'Punk',
    template_dir => 'root/docs-templates',
    sort         => 'order',
    reload       => $ENV{PUNK_DEV} ? 1 : 0,
    footer       => '<a href="/">Punk</a> - the compiled Perl web framework';

Point markdown at a nested directory of .md files and you get a documentation site: navigation reflecting the tree, a per-page table of contents from the headings, syntax highlighting, ranked search, and any images sitting beside the markdown served as static files.

The whole site is rendered 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. A request is then a hash lookup and a triplet: no markdown parse, no template render, no Perl frame.

The tree

docs/
├── index.md            # /docs
├── guide/
│   ├── getting-started.md   # /docs/guide/getting-started
│   └── routing.md
└── recipes/
    └── markdown-site.md      # this page

Top-level directories become collapsible sections in the navigation, named after the directory. index.md is the landing page of its directory.

Front matter

A restricted --- block controls each page:

---
title: Routing
order: 2
---

title names the page (falling back to the first # heading, then the filename), order sorts it within its section under sort => 'order', nav overrides the navigation label, and draft: true excludes the page entirely.

Internal links are absolute, prefix-qualified paths - [Routing](/docs/guide/routing) - and a request for the .md spelling or a trailing slash 301s to the canonical URL. An image beside the page (![chart](funky-overview-dark.png)) is served from the same tree, with .. segments refused.

Custom chrome

The default templates are fine; this site replaces them to share its header with the landing page. template_dir must hold all three files - page.tmpl, wrapper.tmpl, app.css - and nothing is inherited from the shipped set:

  • wrapper.tmpl is the HTML shell; this site's carries the same site header as the landing page, brand link pointing home to /.
  • page.tmpl renders the page body (three lines, usually verbatim).
  • app.css styles the document, sidebar, contents and search.

The wrapper sees title, site_title, nav, content, toc, url, prefix, search, search_path, assets and footer.

On by default, server-rendered, no JavaScript: a Search::Trigram index filled at boot, a GET form at /docs/search, ranked results with highlighted snippets. search => 0 turns it off and drops the dependency.

Writing mode

reload => 1 re-checks mtimes and re-renders changed pages per request

  • this site enables it under PUNK_DEV=1, so editing a guide is save-and-refresh. In production the flag is off and the bytes are frozen at boot; a deploy restarts the app anyway.