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
-
Visit the latest release page or use the following
wgetcommand:wget https://github.com/Bornholm/amatl/releases/download/v0.32.1/amatl_0.32.1_linux_amd64.tar.gzReplace
amd64by your architecture. -
Extract the binary then move it to your preferred location, for example
/usr/local/bintar -xzf amatl_0.32.1_linux_amd64.tar.gz sudo mv ./amatl /usr/local/bin -
Check that your installation is ok by running the
--helpcommandamatl helpThe 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
-
Visit the latest release page and download the file named:
amatl_0.32.1_windows_amd64.tar.gzReplace
amd64with your architecture if needed. -
Uncompress the archive with your preferred archive extractor.
-
Move
amatl.exeto a directory included in yourPATH(e.g.,C:\Program Files\Amatl\) or run it directly from the extracted folder. -
Open PowerShell or Command Prompt and run:
amatl.exe helpYou 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.
🍎 On MacOS
-
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.gzReplace
amd64with your CPU architecture (e.g.,arm64for Apple Silicon). -
Extract the archive and move the binary:
tar -xzf amatl_0.32.1_darwin_amd64.tar.gz sudo mv ./amatl /usr/local/bin -
Verify the installation:
amatl helpYou 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 usingtoc{}.
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://andhttps://— 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_USERNAMEAMATL_HTTP_BASIC_AUTH_PASSWORDWhen these variables are set, credentials are automatically applied to all HTTP(S) requests during URL resolution.
Variables and Templating
Available for:
Markdown,HTML,
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,
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:


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("data:image/jpeg;base64,...")">
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,
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.
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.
Page title
The three layouts take the page title from the first of these that is set:
- the
titlelayout variable, given with--html-layout-vars; - the
titlekey of the document front matter; - the text of the first
h1of 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 !