Managoat.Docs (managoat_docs v0.1.0)

Copy Markdown View Source

A documentation manual embedded at compile time, so a Phoenix app can serve it at a path of its own (/docs) instead of publishing a second site.

The host keeps a module (MyApp.Docs, say) whose body is nothing but this:

use Managoat.Docs,
  root: Path.expand("../..", __DIR__),   # where snippet includes resolve
  docs_dir: "docs",                       # the pages, relative to root
  nav: "docs/nav.yml",                    # the nav, relative to root
  mount: "/docs",                         # the path the manual is served at
  extra_resources: ["CHANGELOG.md"],      # other files a page includes
  languages: ~w(bash elixir json)         # parsers the image bakes

Every option has the default shown except root, which is required. The macro reads the nav and every page it names in the host module's body, so each of them is an @external_resource of the host module, which is where recompilation has to be triggered when a page changes. It generates:

FunctionReturns
nav/0the sidebar: {title, slug} for a page, {section, [{title, slug}]} for a section
nav_source/0the same nav in source shape, {title, file}, for the guardrails
slugs/0every page slug (the home page is "")
get/1{:ok, %{title: t, body: markdown}} or :error
search_index/0one entry per page with its h2h6 headings and their ids
search_index_json/0the index pre-encoded, safe to inline in a <script>
path_for_slug/1"" → the mount, "setup"mount <> "/setup"
external_resources/0every file read at compile time, relative to root
root/0, docs_dir/0, mount/0, languages/0the options, resolved

A page's body is plain markdown: the compile step (Managoat.Docs.Compiler) has expanded snippet includes, turned admonitions into blockquotes and rewritten relative .md links to paths under the mount. The host renders it with Managoat.Docs.Markdown.to_trusted_html/1 and wraps the result in Phoenix.HTML.raw/1.

The dialect

The markdown a manual may use beyond CommonMark and GFM tables is deliberately small, inherited from the MkDocs Material site Fountain's pages were published as before they moved in-app:

  • --8<-- "path/to/file" on a line of its own inlines that file, read relative to root;
  • !!! note "Title" followed by a four-space-indented body becomes a blockquote with a bold title line;
  • [text](other-page.md#anchor) is rewritten to mount/other-page#anchor, resolved against the linking page's own directory.

Nothing else is rewritten. If a page starts using something beyond that, check it as served; there is no second renderer to disagree with.

The nav

nav.yml has a nav: block that is a hand-written list two levels deep:

nav:
  - Home: index.md
  - Setup: setup.md
  - Guides:
      - Deploy: guides/deploy.md
      - Operate: guides/operate.md

Managoat.Docs.Compiler.parse_nav/1 reads exactly those two line shapes and raises at compile time on anything else, a nested section included: a page silently missing from the manual is the bug the parser exists to make impossible, so an unreadable nav fails the compile rather than shrinking the site.

The guardrails

What makes the manual safe to change is Managoat.Docs.GuardrailCase, a case template the host uses in a test of its own. Each check exists because of an incident in Fountain's history: every page the nav names exists and every page on disk is named; every internal link and every #anchor resolves against the rendered heading ids; every file read at compile time is COPYd into the image's build stage, because a missing one does not break a link, it kills mix release while CI stays green.