What's Amatl ?

Amatl is a simple command-line utility that can help you to transform your CommonMark (also known as Markdown) files into full-fledged HTML/PDF documents.

For example, this simple website is generated with Amatl itself.

Features

  • Create document from local or remote resources via URL resolving;
  • Integrate MermaidJS diagrams et code blocks with syntax highlighting;
  • Use custom directives to include others documents or generate tables of content;
  • Use pre-defined or custom layouts to transform your content into presentations, report, etc
  • Use Go templating to inject dynamic data into your document;
  • Rewrite you relative links and embed external resources;
  • Expose your Markdown workspace to AI assistants via the built-in MCP server.

Why the name amatl ?

Amate (Spanish: amate [aˈmate] from Nahuatl languages: āmatl [ˈaːmat͡ɬ] is a type of bark paper that has been manufactured in Mexico since the precontact times. It was used primarily to create codices.

Source: Wikipédia

Amatl is a free software project published under the MIT licence.

Getting started

Installation

🐧 On Linux

  1. Visit the latest release page or use the following wget command:

    wget https://github.com/Bornholm/amatl/releases/download/v0.32.1/amatl_0.32.1_linux_amd64.tar.gz
    

    Replace amd64 by your architecture.

  2. Extract the binary then move it to your preferred location, for example /usr/local/bin

    tar -xzf amatl_0.32.1_linux_amd64.tar.gz
    sudo mv ./amatl /usr/local/bin
    
  3. Check that your installation is ok by running the --help command

     amatl help
    

    The command should return something like this:

     NAME:
       amatl - a markdown to markdown/html/pdf compiler
    
     USAGE:
       amatl [global options] command [command options]
    
     COMMANDS:
       render
       help, h  Shows a list of commands or help for one command
    
     GLOBAL OPTIONS:
       --debug            Enable debug mode (default: false) [$AMATL_DEBUG]
       --log-level value  Set logging level (default: "info") [$AMATL_LOG_LEVEL]
       --workdir value    The working directory [$AMATL_WORKDIR]
       --help, -h         show help
    

🪟 On Windows

  1. Visit the latest release page and download the file named:

    amatl_0.32.1_windows_amd64.tar.gz
    

    Replace amd64 with your architecture if needed.

  2. Uncompress the archive with your preferred archive extractor.

  3. Move amatl.exe to a directory included in your PATH (e.g., C:\Program Files\Amatl\) or run it directly from the extracted folder.

  4. Open PowerShell or Command Prompt and run:

    amatl.exe help
    

    You should see the list of commands and global options, confirming the installation was successful.

Why does my virus-scanning software think the binary is infected?

This is a common occurrence, especially on Windows machines, and is almost always a false positive.

More informations on the official Go FAQ

🍎 On MacOS

  1. Visit the latest release page or use curl:

    curl -LO https://github.com/Bornholm/amatl/releases/download/v0.32.1/amatl_0.32.1_darwin_amd64.tar.gz
    

    Replace amd64 with your CPU architecture (e.g., arm64 for Apple Silicon).

  2. Extract the archive and move the binary:

    tar -xzf amatl_0.32.1_darwin_amd64.tar.gz
    sudo mv ./amatl /usr/local/bin
    
  3. Verify the installation:

    amatl help
    

    You should see the CLI help output, confirming that Amatl is installed.


Usage

Amatl's render command allows you to convert Markdown files into various output formats. Below are common use cases.

🖥️ Generate an HTML file

amatl render html -o output.html your-file.md

This will convert your-file.md into a standalone output.html file.

🖨️ Generate a PDF file

Note: PDF generation requires Chrome or Chromium to be installed on your system.

amatl render pdf -o output.pdf your-file.md

This creates a output.pdf file from the specified Markdown input.

📝 Generate a Markdown file (processed)

Useful for combining multiple files using the include{} directive or for generating a table of contents using toc{}.

amatl render markdown -o output.md your-file.md

This produces a processed Markdown file with all directives resolved.


URL Resolving

Amatl provides a flexible URL resolver to access various types of resources. It supports the following URL schemes:

  • file:// — Refers to local filesystem resources. Paths without a scheme are also interpreted as local files.
  • http:// and https:// — Used to access HTTP(S) resources.
  • stdin:// — Refers to data piped in via standard input (stdin).

These URL schemes can be used consistently across the application, including when specifying inputs for commands like render.

🔐 Basic authentication

Amatl supports HTTP Basic Authentication for http(s):// URLs.

To enable it, set the following environment variables:

  • AMATL_HTTP_BASIC_AUTH_USERNAME
  • AMATL_HTTP_BASIC_AUTH_PASSWORD

When these variables are set, credentials are automatically applied to all HTTP(S) requests during URL resolution.


Variables and Templating

Available for: Markdown, HTML, PDF

Amatl supports dynamic content injection using Go templates. This allows you to define reusable or customizable elements directly within your documents.

🔧 Injecting variables

Use the --vars option with a URL (e.g., a local file or stdin://) pointing to a JSON document containing your variable values. These will be accessible within the template under .Vars.

Example:

echo '{"foo": "bar"}' | amatl render pdf --vars stdin:// my-doc.md

In my-doc.md, you can access the variable like this:

# My Document

Here my value will be replaced: {{ .Vars.foo }}

Amatl also includes the full Sprig function library to enhance template expressions with string manipulation, logic functions, and more.

📄 Using metadata from YAML front matter

Amatl parses YAML front matter and exposes its content through the .Meta object in templates. This is particularly useful for static values defined in the document itself.

---
foo: "bar"
---

# My Document

Here my value will be replaced: {{ .Meta.foo }}

Directives

Available for: Markdown, HTML, PDF

Amatl extends the CommonMark syntax with what we call "directives".

A directive takes this form:


:directive{attr="value1", attr="value2", ...}

Please take note of the linefeed before and after the directive. They are required.

An attribute value is either a quoted string or a bare number or boolean: minLevel="2" and minLevel=2 mean the same thing.

See also "Container directives" to wrap several blocks of content.

Each directive triggers a specific behavior based on its type.

:include{url="<url>", select="<selector>", fromHeadings="<headingLevel>", shiftHeadings="<levelShift>"}

Include another Markdown file in your document.

An included file can include other files in turn. If a file ends up including itself, directly or through other files, the render stops with an error that lists the chain of files. The render also stops past 32 levels of inclusion, which catches a cycle through a symbolic link.

Parameters

url="<url>"
  • Required

The URL of the Markdown document to include. This can be a local file or a remote document's URL (see "URL resolving").

Relative links and images in the included document point next to that document, at every level of inclusion. Write a path with spaces either percent-encoded or between angle brackets, as in CommonMark:

![Cover](./my%20image.png)
![Cover](<./my image.png>)
select="<selector>"
  • Optional
  • Type: string

Only include the top-level nodes of the included document that match the given selector. Uses the same selector syntax as the MCP server (see "MCP Server").

For example, to include only the ## API section of a file:

:include{url="./reference.md", select="h2#api"}
fromHeadings="<headingLevel>"
  • Optional
  • Type: int

Only include sections that match the given heading level threshold.

shiftHeadings="<levelShift>"
  • Optional
  • Type: int

Shift the included headings by the given amount.

:toc{minLevel="<minLevel>", maxLevel="<maxLevel>"}

Generate a table of contents for the whole document.

Parameters

minLevel="<minLevel>"
  • Optional
  • Type: int

Only include headings matching this minimum level.

maxLevel="<maxLevel>"
  • Optional
  • Type: int

Only include headings matching this maximum level.

:attrs{attributes...}

Assign the given attributes to the following element (for example class, id, etc).

Container directives

A container directive wraps several Markdown blocks in one HTML element. Use it to lay out columns, cards or sections without writing HTML.

A line of three or more colons, followed by a name and optional attributes, opens a container. A line of at least as many colons closes it:

:::card{#security .highlight}
### Security

Sovereign hosting, end-to-end encryption.
:::

Containers nest. A closing fence closes the innermost open container, so you can use three colons at every level:

:::columns{count=3}
:::card
### Security
:::
:::card
### Simplicity
:::
:::card
### Sobriety
:::
:::

Indentation matters when a container sits in a list item. A closing fence closes the deepest open container whose opening fence is not more indented than the closing fence. Everything opened inside that container closes with it. In practice, indent a closing fence like its opening fence.

Other directives work inside a container, :include{} included.

HTML output

The container becomes an element with the amatl-<name> class, plus the classes given in its attributes.

The article, aside, figcaption, figure, footer, header, main, nav and section containers use the HTML element of the same name. Any other name gives a <div>.

Global HTML attributes such as id, class, style, title or role are copied as they are, and so are data-* and aria-* attributes. Any other attribute becomes a data-* attribute, except bg (see Background image). For example, :::columns{count=3} gives <div class="amatl-columns" data-count="3">.

An :attrs{} directive placed right before a container adds its attributes to the container.

Background image

The bg attribute sets a background image on a container:

:::section{.hero bg="./cover.jpg"}
# Our offer
:::

Amatl resolves the path like an image path, so a relative path in an included file points next to that file. If the file does not exist, the render stops with an error. HTML and PDF output embed the image as a data: URL and pass it to CSS through the --amatl-bg custom property:

<section class="amatl-section hero" style="--amatl-bg: url(&quot;data:image/jpeg;base64,...&quot;)">

amatl://components.css turns it into a centered background that covers the element. A custom layout can use var(--amatl-bg) in its own rules. Markdown output keeps the path. In an included file, amatl makes it absolute, as it does for images.

Built-in styles

The built-in layouts load amatl://components.css, which styles four containers:

Container Rendering
columns one column per child, 2 by default, set count from 1 to 6 to change it
grid as many columns as fit the page width
card a box with a border and rounded corners
callout a tinted box with a colored left border

In columns and grid, a horizontal rule (---) spans the whole row instead of taking a cell.

Change the spacing and colors with these CSS custom properties: --amatl-gap, --amatl-grid-min, --amatl-radius, --amatl-border-color, --amatl-card-bg, --amatl-accent and --amatl-callout-bg.

A container with any other name has no default style. Style it in your own layout through its amatl-<name> class.


Layouts

Available for: HTML, PDF

A layout is the HTML template that wraps your rendered Markdown. Use one to add content around the document, apply your own styles, or move parts of the document around.

Amatl ships three layouts. Pass another one with the --html-layout flag.

Built-in layouts

All three layouts load amatl://components.css, which styles the columns, grid, card and callout container directives. See "Container directives".

amatl://document.html

A print-friendly layout for reports and other text-heavy documents, in A4 by default. It is the default layout for the html and pdf commands.

View a sample document (PDF)

amatl://presentation.html

Turns the document into slides. A horizontal rule (---) starts a new slide when it sits at the top level of the document. Anywhere else, for example in a container directive, a blockquote or a list item, --- stays a visible rule in the current slide.

View a sample presentation (PDF)

amatl://website.html

A responsive layout for publishing Markdown as a web page.

Visit the example website

Page title

The three layouts take the page title from the first of these that is set:

  1. the title layout variable, given with --html-layout-vars;
  2. the title key of the document front matter;
  3. the text of the first h1 of the document.

Using a custom layout

Give the path or URL of your layout to the --html-layout flag:

amatl render html -o output.html --html-layout file://my-layout.html my-doc.md

resolve looks up relative paths from the directory of the layout, not from the directory of the document.

Template data

A layout is a Go html/template. It receives:

Field Content
.Body the rendered document, as HTML
.Meta the document front matter
.Vars the layout variables given with --html-layout-vars
.Context the rendering context, to pass to resolve

Functions

Every Sprig function is available, plus the following ones. The html* functions take an HTML string and a CSS selector.

Function Returns
htmlQueryFirst .Body "h1" the first matching element, as HTML, or an empty string
htmlQueryAll .Body "h2" every matching element, as a list of HTML strings
htmlTextContent .Body "h1:first-of-type" the text of the matching elements, without tags. When several elements match, their texts run together with no space between them
htmlRemove .Body "h1:first-of-type" the HTML without the matching elements
htmlAddAttr .Body "table" "class" "wide" the HTML with the value appended after a space to the attribute of each matching element, or the attribute created. Use it for attributes that hold a list, such as class or rel. On id, it gives id="data wide"
htmlSplit .Body "hr" the top-level content cut at each matching element, as a list of HTML strings
resolve .Context "./logo.svg" "image/svg+xml" the resource as a data: URL, with the given MIME type or a detected one

htmlQueryFirst, htmlRemove and htmlAddAttr return a fragment that you can insert anywhere in the layout. An element the HTML parser moves to the head, such as a <style> written at the top of the document, stays in the result.

This layout puts the first heading in a banner and the rest of the document below it:

<header class="banner">{{ htmlQueryFirst .Body "h1:first-of-type" }}</header>
<main>{{ htmlRemove .Body "h1:first-of-type" }}</main>

MCP Server

Amatl includes a built-in Model Context Protocol (MCP) server. It allows an AI assistant or a compatible editor to interact with the Markdown files in a workspace directory.

The server communicates over stdin/stdout (JSON-RPC 2.0 stdio transport).

🚀 Starting the server

amatl mcp serve --workspace ./docs

If --workspace is omitted, the current directory is used.

Flag Alias Environment variable Description
--workspace -w AMATL_MCP_WORKSPACE Root directory exposed to the server. Access is strictly confined to this directory.

🔧 Configuring your MCP client

Most MCP clients expect a configuration entry similar to:

{
  "mcpServers": {
    "amatl": {
      "command": "amatl",
      "args": ["mcp", "serve", "--workspace", "/path/to/your/docs"]
    }
  }
}

Refer to your client's documentation for the exact configuration format.

🛠️ Available tools

Tool Description
list_files List files in the workspace, with optional glob pattern filtering.
table_of_contents Return the hierarchical table of contents of a Markdown file.
list_sections List all headings in a file as a flat list with their selectors.
read_section Read the full content of a section (heading + body).
find_sections Recursively find all nodes matching a selector.
update_section Replace the content of a section in a Markdown file.

The tools read container directives the way amatl render does. A section that contains a :::card block includes the whole block, fences included. A heading written inside a block shows up in table_of_contents, list_sections and find_sections. It does not start a section, so read_section and update_section reject its selector. Target the top-level heading above the block instead.

The tools skip the front matter of a document. They do not expand :include{} or the other directives, because they read and edit the file as written: read_section shows the directive line, and update_section keeps it unless the new content drops it.

🔍 Selector syntax

Selectors are a CSS-like mini-language for targeting nodes in a Markdown document.

Example Matches
h2#introduction ## Introduction heading via its auto-generated ID
h2:contains("Usage") ## Usage heading matched by text
code[lang="go"] Fenced code block with language go
h2#api ~ table Table following the ## API heading
h2 All level-2 headings

IDs are automatically generated from heading text by the Markdown parser.


How to

Write your own layout

Coming soon...

Share your configuration

Coming soon...

Use in CI

Coming soon...

🙏 Attributions

This project wouldn’t be possible without the incredible work of the open-source community. Special thanks to:

Thank you for your amazing work !