marsh is a Bash script for building static websites using Markdown.
Sections in this document:
- Requirements
- Installation
- Quick Start
- Usage
- Site Configuration
- Documents
- Archives and Syndication
- Templates
- Advanced Template Usage
- Upgrading From Earlier Versions
- Development Process
- License
- BSD/Linux/macOS or similar
- Bash 3.2 or later (supports macOS Bash version out of the box)
- Discount Markdown processor
- GNU Parallel (optional, highly recommended for performance)
Copy marsh to a directory in your PATH such as /usr/local/bin and make it executable.
cp marsh /usr/local/bin/marsh
chmod +x /usr/local/bin/marshDiscount and GNU parallel can be installed using your system's package manager.
# Fedora
dnf5 install discount parallel
# Ubuntu
apt-get install discount parallel
# Homebrew
brew install discount parallelNote: sudo may be required for some commands.
You can also build Discount provided as a submodule in the contrib directory using the build-discount script in the tools directory. A working C compiler toolchain is required.
Once you have installed the necessary dependencies, a good way to get started is building the example site. Run the following commands from the root directory of this repository on your machine:
chmod +x marsh
./marsh build --log-level=verbose example
marsh will build the example site using the marsh-config.yaml configuration file in the specified example directory.
The configuration file specifies public as the build output path, which is relative to the configuration file directory, so the site is built and published to example/public. The --log-level=verbose parameter prints more information during the build than the standard info log level, so you can see in greater detail what is being built in real-time.
When the command is completed, open example/public/index.html in your web browser to view the built example site.
The source documents for the example site are located at example/source, and the example template is located at templates/example. You can inspect these directories and files to get a basic idea of where to put things and how they work. More detail is covered in the sections that follow.
If you run into any problems or are simply curious, you can also run the test suite to ensure marsh works correctly on your system using the command:
./marsh-test --marsh=./marsh
The basic syntax is:
marsh build [config_file]
Where config_file is the path to the build configuration file for your site. You may also specify a directory containing a build configuration file named marsh-config.yaml. Where config_file is not specified, marsh defaults to using the marsh-config.yaml file in the current working directory.
Output is written to the path defined in the build configuration file, e.g., "public".
If you have GNU parallel installed, marsh will build multiple documents simultaneously for speed. The number of concurrent jobs can be specified using the -j or --jobs parameters, default being the number of logical processors available on your machine.
marsh build -j 4
You can specify the location for the Discount markdown application using the --markdown parameter, useful if the installation location is not in your PATH.
marsh build --markdown="/path/to/markdown"
You can specify the --log-level parameter to adjust the amount of logging detail marsh prints. The verbose log level prints the paths of all files as they are published.
marsh build --log-level verbose
Full usage:
marsh --help
The included marsh-test test suite may be run to verify marsh compatibility with your system. Run marsh-test --help for usage information.
The marsh-config.yaml build configuration file defines build targets, source paths, templates, and archives.
---
Build:
Name: My Website
Description: My website description
URL: https://example.com
Path: public
Targets:
- Name: My Website
Path: .
Source: source
Template:
Source: templates/example
...All paths are relative to the configuration file directory. See the example directory for a more extensive configuration example.
The main document format is Markdown. marsh converts Markdown documents to HTML, with additional formatting defined by the configured template(s), and finally saves the rendered output to the paths specified in the build configuration.
Within Markdown documents, relative path Markdown syntax links ending in the configured Markdown file extensions are by default rewritten to use the file extension .html (configurable via the --markdown-document-extensions-replacement command line parameter). This allows Markdown documents to link directly to each other, useful for source documents on repository hosting, while ensuring rendered pages also link to each other.
Files of other types and file extensions are copied as-is to the paths specified in the build configuration.
Markdown documents may use any of the following file extensions (configurable via the --markdown-document-extensions command line parameter):
.markdown(always included; not configurable).md.mkd.mkdn.mdown.mdtxt.mdtext.text
Markdown documents may begin with YAML frontmatter containing metadata such as title and author information. This allows documents to be self-describing.
Example:
---
Type: article
Date: 2026-02-20
Title: Example Document
Language: English
Language_Code: en
Authors: [ Mr. Marsh <mrmarsh@example.com> ]
Copyright: 2026 Mr. Marsh
License: Creative Commons Attribution-ShareAlike 4.0 International
License_Abbr: CC BY-SA 4.0
License_URL: https://example.com/license.html
---
Example Document
================
Document content goes here.
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque in
convallis felis. Aenean id sodales est, sed aliquet lectus. Sed.
See the example directory for additional document examples.
Templates may include document metadata for display on rendered pages and embedding in syndicated feeds. See the Templates section for information on using document metadata in templates via template tags.
marsh supports all core Markdown syntax.
Reference:
- Markdown Basic Syntax at Markdown Guide
- Markdown Help at CommonMark
- Markdown at Daring Fireball (original author)
- Markdown Wikipedia article
marsh automatically detects the presence of responsive image variants following a simple file naming convention. Files with names matching the specified image and having @2x, @3x, etc. suffix before the file extension will be included in the HTML srcset attribute on the img element. This only applies to images specified using Markdown syntax.
Given the following source structure:
images/
├─ image.png
├─ image@2x.png
└─ image@3x.png
index.markdown
The following Markdown image syntax:
Becomes this HTML:
<img src="images/image.png" srcset="images/image.png 1x,
images/image@2x.png 2x, images/image@3x.png 3x" alt="Alternate text" />You can also use pixel width variants with suffixes ending in w, e.g., image@800w.png. Width and resolution variants should not be used together for the same image.
Markdown images with a title are treated as implicit figures. The title becomes the figure caption and the image is linked to itself:
The following Markdown image syntax:
Becomes this HTML:
<figure>
<a href="image.png">
<img src="image.png" alt="Alternate text" />
</a>
<figcaption>Title/caption</figcaption>
</figure>Automatic responsive images are supported within implicit figures.
Specially formatted HTML comments may be used to insert HTML div elements with id or class attributes. These elements may be used to wrap content for styling or other purposes.
The following Markdown document:
<!-- #unique-thing -->
Contents will be wrapped in a div element with the id unique-thing.
<!-- /#unique-thing -->
<!-- .notice -->
Contents will be wrapped in a div element with the class name notice.
<!-- /.notice -->
<!-- .foo.bar -->
Contents will be wrapped in a div element with the class names foo and bar.
<!-- /.foo.bar -->Becomes this HTML:
<div id="unique-thing">
<p>Contents will be wrapped in a div element with the id unique-thing.</p>
</div><!-- end div#unique-thing -->
<div class="notice">
<p>Contents will be wrapped in a div element with the class name notice.</p>
</div><!-- end div.notice -->
<div class="foo bar">
<p>Contents will be wrapped in a div element with the class names foo and bar.</p>
</div><!-- end div.foo.bar -->In addition to its own extensions, marsh supports the Markdown syntax extensions supported by Discount such as tables and fenced code blocks, which are common across many implementations.
marsh can automatically generate archives of collections of documents, such as chronological news and blog posts, in HTML format and the web syndication formats Atom and JSON Feed.
This example build configuration:
---
Build:
Name: My Website
Description: My website description
URL: https://example.com
Path: public
Targets:
- Name: My Website
Path: .
Source: source/*
Template:
Source: templates/example
- Name: Website News
Path: news
Source: source/news
Template:
Source: templates/example
Archives:
- Name: Latest News
Path: index.markdown
Encoding: html
Count: 3
Page_Size: 2
- Name: News Archive
Path: all/index.markdown
Encoding: html
- Name: News Feed
Path: feed.xml
Encoding: atom
Count: 10
- Name: News Feed
Path: feed.json
Encoding: json
Count: 10
...Given the following source structure:
source/
├─ news/
│ └─ 2026/
│ ├─ 01/
│ │ ├─ 01-news-item-one.markdown
│ │ └─ 02-news-item-two.markdown
│ └─ 02/
│ ├─ 01-news-item-three.markdown
│ └─ 02-news-item-four.markdown
├─ about.markdown
└─ index.markdown
Builds this site structure:
public/
├─ news/
│ ├─ 2026/
│ │ ├─ 01/
│ │ │ ├─ 01-news-item-one.html
│ │ │ └─ 02-news-item-two.html
│ │ └─ 02/
│ │ ├─ 01-news-item-three.html
│ │ └─ 02-news-item-four.html
│ ├─ all/ <--
│ │ └─ index.html <--
│ ├─ feed.json <--
│ ├─ feed.xml <--
│ ├─ index.html <--
│ └─ page/ <--
│ ├─ 1/ <--
│ │ └─ index.html <--
│ └─ 2/ <--
│ └─ index.html <--
├─ about.html
└─ index.html
In this example, the Latest News HTML archive includes the latest three documents as specified by Count, limited to two per page as specified by the Page_Size. The generated document public/news/index.html is the canonical archive reference and first page, and additional pages are generated as documents in the numbered page subdirectories (the document public/news/page/1/index.html is essentially identical to the canonical page).
Only HTML archives support pagination, and if Page_Size is omitted, the page directories will not be created. Atom and JSON Feed archives do not support pagination and are generated as single files.
The News Archive HTML archive includes all documents and generates the document public/news/all/index.html as specified by Path in the archive configuration, further illustrating how multiple archives can be created from the same source content.
Generated archives do not affect the individual documents, which are still built as usual.
While marsh internally compiles the collection of documents to include in an archive, it relies on templating to generate the archive in the desired format. See the Templates section for more information. The template at templates/example provides examples of template partials under partials/archive.
Templates control how documents are rendered to become complete web pages with features like headers and navigation, as well as images, layout, and style. Templates also control how syndication feeds are rendered.
A marsh template is a directory containing a marsh-template.yaml configuration file and one or more partial template files, also known as partials. The template configuration declares the resources that marsh will use during rendering.
The smallest useful HTML template usually consists of Base, Head, and Body partials.
Given a minimal template directory structure like this:
templates/
└─ my-template/
├─ marsh-template.yaml
└─ partials/
├─ base.html
├─ body.html
└─ head.html
The marsh-template.yaml configuration file should have the contents:
---
Template:
Partials:
Base: partials/base.html
Head: partials/head.html
Body: partials/body.html
...Partials paths are relative to the location of the configuration file.
See the Advanced Template Usage section and the template at templates/example for more information.
Template tags allow you to include information about a document, archive, or other metadata in the rendered output, and can be placed within partial templates wherever desired. Template tags are formatted using the name of the tag surrounded by double curly braces, e.g., {{ document.title }}. When building, marsh replaces these tags with their associated values, such as the title of the document according to its YAML frontmatter metadata.
Tag values come from three main sources:
- Generated metadata created by
marshitself. - Recognized metadata defined in document frontmatter.
- Custom metadata defined in document frontmatter.
Generated metadata is created by marsh itself. The names of these template tags are reserved and their values may not be overridden in document frontmatter.
site.rootpath: Relative link from the current rendered page to the site root.site.abspath: Absolute path or URL for the site root.target.rootpath: Relative link from the current rendered page to the target root.target.abspath: Absolute path or URL for the target root.target.sitemap: Rendered sitemap markup for the current target. Empty where no sitemap document is configured or found.archive.rootpath: Relative link from the current archive page to the archive root directory. Empty for non-archive pages.archive.abspath: Absolute path or URL for the archive root directory. Empty for non-archive pages.document.content: Rendered content for the current page itself, or for archive pages, the embedded archive subdocument content.document.href: Relative link from the current rendered page to the current page itself, or for archive pages, the embedded archive subdocument.document.uri: Absolute path or URL for the current rendered page or archive subdocument. For directory-style page paths ending inindex.html,document.uriis normalized to the directory form, e.g.,/news/index.htmlbecomes/news/.document.page-uri: Absolute path or URL for the current archive page.document.page-canonical-uri: Absolute path or URL for the canonical archive page.document.page-first-uri: Absolute path or URL for the first archive page.document.page-prev-uri: Absolute path or URL for the previous archive page.document.page-next-uri: Absolute path or URL for the next archive page.document.page-last-uri: Absolute path or URL for the last archive page.document.page-href: Relative link to the current archive page.document.page-canonical-href: Relative link to the canonical archive page.document.page-first-href: Relative link to the first archive page.document.page-prev-href: Relative link to the previous archive page.document.page-next-href: Relative link to the next archive page.document.page-last-href: Relative link to the last archive page.
Generated metadata tags are useful for rendering calculated paths, links, archive navigation, and embedded sitemap navigation. In general, use the *-href forms for HTML links inside rendered pages, and use the *-uri forms for canonical metadata, embedding in syndication feeds, and wherever absolute references are most appropriate.
Recognized document metadata tags correspond to the metadata defined in the frontmatter for each document. The underlying variable representations for these tags may be used by marsh for internal purposes, hence "recognized".
Document tag names begin with document. and end with the name of the metadata field, lowercased and with underscores converted to dashes. For example, the document metadata field Project_URL may be referenced using the tag name document.project-url.
document.title: The document title.document.type: The document type, such aspage,article,post, or another user-defined type.document.date: The document date or timestamp.document.language: The document language name, such asEnglish.document.language-code: The document language code, such asen.document.authors: The list of document authors.document.copyright: The copyright notice for the document.document.credits-url: A URL for credits or attribution information.document.license: The document license name.document.license-abbr: A short abbreviation for the document license.document.license-url: A URL for the document license.document.project: The project or site name associated with the document.document.project-version: The project or site version.document.project-url: A URL for the project or site.document.redirect-url: A redirect destination URL for redirect pages.document.state: The list of document state values, such asdraftorpublished.
These tags are useful for a variety of purposes such as display on HTML pages and embedding in syndication feeds.
Custom document metadata may also be defined in a document's frontmatter and accessed in templates using document.* tags. Custom document metadata naming must not collide with generated metadata and related tags, and values must be plain text strings. Arrays/lists and other data types are not supported.
This example document frontmatter with custom metadata:
---
Original_Language: English
Original_Language_Code: en
...Makes these custom tags available for use in templates:
{{ document.original-language }}
{{ document.original-language-code }}
Custom document metadata tags generally provide the same utility as recognized document metadata tags.
Conditional tags allow you to include or exclude content based on template tag values. They use a syntax similar to regular template tags but with additional operators for comparison and filtering.
A conditional tag is composed of the name of the template tag whose value will be compared, a conditional operator, and the comparison value. Invalid conditional tag sections are not processed and instead are removed from the rendered output.
isorequals: Checks whether the tag value equals the condition valueis notordoes not equal: Checks whether the tag value does not equal the condition valuecontains: Checks whether the tag value contains the condition valuedoes not contain: Checks whether the tag value does not contain the condition value
Examples:
{% if document.type is "article" %}
<!-- Content for article documents -->
{% endif %}
{% if document.state contains "published" %}
<!-- Content for documents with published state -->
{% endif %}
{% if document.authors is not "" %}
<!-- Content for documents with authors -->
{% endif %}
{% if document.state does not contain "draft" %}
<!-- Content for documents without draft state -->
{% endif %}Conditional tags can be nested within each other. Each nested conditional must have its own if and endif tags.
{% if document.type is "article" %}
{% if document.state contains "published" %}
<!-- Content for published articles -->
{% endif %}
{% endif %}You can use else to provide alternative content when the condition evaluates to false:
{% if document.state contains "draft" %}
<!-- Content for draft documents -->
{% else %}
<!-- Content for non-draft documents -->
{% endif %}Text transforms modify template tag values during rendering. A transform is added after a tag name using the pipe character.
Consider the following template tag:
{{ document.title | slug }}
The slug text transform produces a URL-style text fragment, changing a document title such as "My favorite document" into "my-favorite-document".
Multiple transforms may be used by appending additional pipe characters and transform names, and are applied in order from left to right.
Useful transforms include:
case:lower, andcase:upperfor converting text to lowercase or uppercasedateand date format variants such asdate-type:rfc2822anddate-type:rfc3339for formatting document datestrimfor removing leading and trailing whitespaceslugfor converting text into a simple URL-style slugdecode:entitiesfor decoding HTML entitiesescape:htmlandescape:jsonfor safe HTML and JSON outputplaintextfor stripping HTML markup down to plain textexcerptandexcerpt-type:htmlfor generating short summaries, e.g.,excerpt:100converts to plain text and limits to 100 characters
Items transforms are useful when working with list-style metadata such as document.authors and document.state:
items:joinrenders values joined together using a delimiter, e.g.,items:join:,produces "a,b,c"items:join-type:oxfordrenders values joined together as a natural-language list, e.g., "a, b, and c"items:json-arrayrenders values as JSON array contents, e.g., "a","b"items:wrap-type:htmlrenders each value wrapped in HTML tags, e.g.,items:wrap-type:xml:tagproduces<tag>a</tag><tag>b</tag><tag>c</tag>items:wrap-type:html-style-linkrenders each value as an HTML<link rel="stylesheet" href="..." />tagitems:wrap-type:html-script-srcrenders each value as an HTML<script src="..."></script>tag
Heading transforms are useful in select cases:
headings:push: Push headings down by one level (Markdown#becomes##, etc.)headings:shift: Shift headings up by one level (Markdown##becomes#, etc.)headings:remove: Remove headings entirely
Examples:
{{ document.title | trim | slug }}
{{ document.date | date-type:rfc3339 }}
{{ document.authors | items:join-type:oxford }}
{{ document.content | excerpt:300:2 }}
{{ template.assets.styles | items:wrap-type:html-style-link }}
{{ template.assets.scripts | items:wrap-type:html-script-src }}
Text transforms may be applied to recognized and custom document metadata tags, as well as rendered document content.
Here are some practical examples of how conditional tags and text transforms are used in real templates.
Display different content based on document metadata such as date and authors:
<!-- Only for documents with the type article -->
{% if document.type is "article" %}
<p class="metadata">
<!-- Display publish date where available -->
<!-- using a text transform for formatting -->
{% if document.date is not "" %}
<span class="date">Published on {{ document.date | date-type:rfc3339 }}</span>
{% endif %}
<!-- Display authors list only for articles with authors -->
{% if document.authors is not "" %}
<span class="authors">by {{ document.authors | items:join-type:oxford }}</span>
{% endif %}
</p>
{% endif %}Conditionally load assets based on the current page context:
<head>
<!-- Load stylesheets for all pages as defined in the template configuration -->
<!-- using a text transform to automatically create the appropriate link tags -->
{{ template.assets.styles | items:wrap-type:html-style-link }}
<!-- Load additional stylesheets for article documents -->
{% if document.type is "article" %}
<link rel="stylesheet" href="{{ site.rootpath }}css/articles.css">
{% else %}
<!-- Otherwise, load additional stylesheets for news documents -->
<link rel="stylesheet" href="{{ site.rootpath }}css/news.css">
{% endif %}
</head>Conditionally load assets based on the current page context:
<head>
<!-- Load stylesheet for article documents -->
{% if document.type is "article" %}
<link rel="stylesheet" href="{{ site.rootpath }}css/articles.css">
{% else %}
<!-- Otherwise, load stylesheet for news documents -->
{% if document.type is "news" %}
<link rel="stylesheet" href="{{ site.rootpath }}css/news.css">
{% endif %}
{% endif %}
</head>Conditionally display page navigation for paginated HTML archives:
<!-- Only for paginated HTML archives -->
{% if document.paginated is "true" %}
<nav class="archive-pagination">
<!-- Display the current page number and total number of pages -->
<p>Page {{ document.page }} of {{ document.page-total }}.
<!-- Display links to the previous and next pages where available -->
{% if document.page-has-prev is "true" %}
<a href="{{ document.page-prev-href }}">← Previous Page</a>
{% endif %}
{% if document.page-has-next is "true" %}
<a href="{{ document.page-next-href }}">Next Page →</a>
{% endif %}
</p>
</nav>
{% endif %}The Templates section above covers the minimum structure needed to begin rendering pages. marsh also provides additional template features for more advanced customization of site output.
These features are useful when you want to:
- Split a template into logical, modular components for ease of management and reuse
- Add stylesheets, scripts, fonts, images, or other assets to a template
- Embed a sitemap document on other pages for use as a reusable navigation section
- Customize an existing template using override files, instead of creating a new template from scratch
- Render specific archives using different templates and overrides
- Select specific document types or exclude specific document states from generated archives
The following subsections describe these advanced features in more detail.
Template configuration may define multiple partials for document page rendering, archive rendering, and special functions like redirecting one page to another.
The Base partial is required. Partials other than the Base partial may be inserted into other partials using template tags prefixed with template..
The following is a list of recognized partials, their suggested use, and their associated template tags. See also templates/example/marsh-template.yaml for a comprehensive example template configuration.
Building blocks for turning documents into complete web pages.
Base: The required outer wrapper partial for documents. Has no template tag and may not be inserted into other partials.Head: The contents of the HTML<head>section for a rendered page. Inserted using{{ template.head }}.Body: The main body wrapper for a rendered page. Inserted using{{ template.body }}.Document: The document content wrapper for a rendered page. Inserted using{{ template.document }}.Header: A reusable page header partial. Inserted using{{ template.header }}.Footer: A reusable page footer partial. Inserted using{{ template.footer }}.Nav: A reusable navigation partial. Inserted using{{ template.nav }}.Notice: A reusable notice or aside partial. Inserted using{{ template.notice }}.
Partials purpose-built to handle special situations.
Redirect: A dedicated outer partial used when rendering redirect pages. Has no template tag and may not be inserted into other partials. Automatically selected bymarshwhere document frontmatter metadata includesRedirect_URL. Ideal contents are minimal HTML page markup with<meta http-equiv="refresh" content="0; url={{ document.redirect-url }}">in the HTML<head>section.
Archive-specific HTML partials supersede document partials, allowing you to render archive pages differently from the rest of your pages. Where an HTML archive partial is not specified, marsh uses the corresponding regular HTML document partial.
Archive.HTML.Base: The outer wrapper for HTML archive pages. Has no template tag and may not be inserted into other partials.Archive.HTML.Head: Archive-specific head markup. Inserted using{{ template.head }}when rendering an HTML archive.Archive.HTML.Body: Archive-specific body wrapper. Inserted using{{ template.body }}when rendering an HTML archive.Archive.HTML.Document: Archive-specific document content wrapper. Inserted using{{ template.document }}when rendering an HTML archive.Archive.HTML.Header: Archive-specific header partial. Inserted using{{ template.header }}when rendering an HTML archive.Archive.HTML.Footer: Archive-specific footer partial. Inserted using{{ template.footer }}when rendering an HTML archive.Archive.HTML.Nav: Archive-specific navigation partial. Inserted using{{ template.nav }}when rendering an HTML archive.Archive.HTML.Notice: Archive-specific notice or aside partial. Inserted using{{ template.notice }}when rendering an HTML archive.
For rendering Atom syndication feeds.
Archive.Atom.Base: The required outer wrapper for the Atom feed. Has no template tag and may not be inserted into other partials.Archive.Atom.Body: The main body wrapper for the Atom feed. Inserted using{{ template.body }}when rendering an Atom archive.Archive.Atom.Document: The document content wrapper for rendering each Atom entry. Inserted using{{ template.document }}when rendering an Atom archive.
For rendering JSON Feed syndication feeds.
Archive.JSON.Base: The required outer wrapper for the JSON Feed. Has no template tag and may not be inserted into other partials.Archive.JSON.Body: The main body wrapper for the JSON Feed. Inserted using{{ template.body }}when rendering a JSON Feed archive.Archive.JSON.Document: The document content wrapper for rendering each JSON Feed entry. Inserted using{{ template.document }}when rendering a JSON Feed archive.
In addition to the partial-specific template.* tags above, marsh provides generated template tags for template asset lists using the prefix template.assets.. Each tag resolves to a newline-delimited list of page-relative asset paths for the items in that asset category.
template.assets.fonts: List of font assets.template.assets.styles: List of stylesheet assets.template.assets.scripts: List of script assets.template.assets.images: List of image assets.template.assets.audio: List of audio assets.template.assets.video: List of video assets.template.assets.documents: List of document assets such as PDF files.template.assets.binaries: List of binary assets such as ZIP files.template.assets.other: List of other non-specific types of assets.
Templates may declare static assets in marsh-template.yaml. These assets are copied into the build output along with the rendered documents for targets using the template.
Supported asset categories are:
FontsStylesScriptsImagesAudioVideoDocumentsBinariesOther
Paths are relative to the template directory.
Example template configuration with assets:
---
Template:
Partials:
Base: partials/base.html
Head: partials/head.html
Body: partials/body.html
Assets:
Styles:
- css/site.css
Scripts:
- js/site.js
Images:
- images/logo.png
...In this example, the files css/site.css, js/site.js, and images/logo.png are copied from the template into the build output for the target.
You can reference any specific asset in a partial template by combining its path with generated metadata template tags. You can also reference the list of assets for any category by its associated template tag, and apply text transforms to customize how it is rendered. The following example demonstrates both types of asset inclusion in template partials.
The following example partial:
<!DOCTYPE html>
<html>
<head>
{{ template.assets.styles | items:wrap-type:html-style-link }}
</head>
<body>
<img class="logo" src="{{ site.rootpath }}images/logo.png" />
{{ document.content }}
{{ template.assets.scripts | items:wrap-type:html-script-src }}
</body>
</html>Combined with the template configuration with assets above, this example partial would produce the following rendered output for a document one directory level below the root path, e.g., /pages/my-page.html:
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="../css/site.css" />
</head>
<body>
<img class="logo" src="../images/logo.png" />
<!-- document contents included here -->
<script src="../js/site.js"></script>
</body>
</html>In the site build configuration, targets may define a site map document, which is a normal document in your source tree that typically contains links to most or all of the other pages on your site, like a table of contents.
marsh builds a special version of the site map document and makes it available for embedding on other pages using the template tag {{ target.sitemap }}. Relative links are rewritten to be path-correct in relation to the individual pages on which the site map is embedded.
This is useful where you want a navigation structure to be derived from a source document and made reusable across multiple pages in the same target.
Example site build configuration specifying a site map source document using Sitemap.Source:
---
Build:
Targets:
- Name: Example Site
Path: .
Source: source/*
Sitemap:
Source: site-map/index.markdown
...You can also specify Search: true to treat the Sitemap.Source path as a lookup key, instead of an explicit path. With Search: true, marsh will find all documents in the target whose names match the lookup key, and for each document use the nearest matching site map within the target source tree. This essentially allows creating different site maps for different subdirectories, as long as the file names are the same.
Example site build configuration specifying a site map lookup key using Sitemap.Source paired with Sitemap.Search: true.
---
Build:
Targets:
- Name: Example Site
Path: docs
Source: source/docs
Sitemap:
Source: sitemap.markdown
Search: true
...Note that the resolved path for a site map must remain inside the site source tree.
A template override is an additional template configuration file that is applied in addition to the primary target template, allowing you to customize an existing template without creating a complete separate customized copy. Overrides are specified alongside the template specification in your site build configuration file (not the template configuration file).
Example site build configuration specifying a template and template override:
---
Build:
Targets:
- Name: Example Site
Path: .
Source: source/*
Template:
Source: templates/example
Overrides:
- templates/example/marsh-template-overrides.yaml
...Overrides are useful when you want to:
- Change one or more partial paths
- Remove a partial by setting its path to an empty string
- Filter or rewrite template asset paths without changing the base template
Example override file:
---
Template:
Partials:
Footer: ""
Filters:
- Regex: /opensans/d
Assets:
- Fonts
- Styles
...This example override configuration modifies two things in relation to the primary template configuration:
- The footer partial is removed from the partials list by setting its path to an empty string
- Fonts or stylesheets with asset paths matching
opensansare removed from the template asset lists
Overrides are best used as small customization layers on top of an existing template. Creating a separate template may be better where more extensive modifications are desired.
Archives may use the target's template configuration, or they may specify their own template source and overrides. This makes it possible for one target to render normal pages, HTML archives, and syndication feeds with different template behavior where needed.
Archive-level template configuration is specified inside the definition for each archive in the site build configuration:
---
Build:
Targets:
- Name: Example Site News
Path: news
Source: source/news
Template:
Source: templates/example
Archives:
- Name: Latest News
Path: index.markdown
Root_Path: .
Encoding: html
Types: [ news ]
Exclude_States: [ draft ]
Template:
Source: templates/archive-html
Overrides:
- templates/archive-html/custom.yaml
- Name: News Feed
Path: feed.xml
Root_Path: .
Encoding: atom
Types: [ news ]
Exclude_States: [ draft ]
...In this example, the Latest News HTML archive uses its own archive template and override file separate from the target template.
Where an archive does not specify its own template, like the News Feed Atom syndication feed in this example, marsh uses the target template.
Values for archive.rootpath and archive.abspath template tags are derived from related archives as a group. Multiple archives that define the same Types and Excluded_States are considered related. Individual archives can override this behavior by specifying Root_Path. Archive root paths are relative to the target path.
Archive-specific template configuration is useful when you want to:
- Render HTML archive pages differently from normal documents and other archives for the same target
- Apply template overrides to a single archive without affecting the rest of the target
Archives may also select which documents to include and how archive subdocument content is rendered:
Typeslimits an archive to include only the specified document typesExclude_Statesomits documents with matching state values, such asdraft
For example, an archive configuration might specify Types: [ news ] and Exclude_States: [ draft ] to include only news documents not marked as draft.
Templates may also define a dedicated redirect partial. When a document provides redirect metadata, marsh can render that document using the redirect partial instead of the normal page partials.
The redirect destination is available in templates using the tag document.redirect-url.
This is useful for placeholder pages, moved content, or preserving older URLs while sending readers to a new location.
This section describes how to keep your sites working when upgrading to newer versions of marsh from earlier versions. It does not cover all new features and functionality in newer versions, only changes in configuration and behavior that may require action on your part to ensure your site remains compatible.
Remote source tree fetching as previously configured by Remote and Fetch has been deemed no longer in scope and thus removed. Source trees must now be local.
Old:
Build:
Targets:
- Name: My Target
Source: source
Remote: https://github.com/.../my-repo.git
Fetch: trueNew:
Build:
Targets:
- Name: My Target
Source: sourceThe Build.Targets[].Templates mapping has been replaced by the singular Build.Targets[].Template mapping. Config has been replaced by a list of Overrides.
Remote template fetching as previously configured using Remote and Fetch has been deemed no longer in scope and thus removed. Source trees must now be local.
Build:
Targets:
- Name: My Target
Templates:
- Source: templates/my-template
Config: templates/my-template/marsh-template-overrides.yaml
Remote: https://github.com/.../my-repo.git
Fetch: trueBuild:
Targets:
- Name: My Target
Template:
Source: templates/my-template/marsh-template.yaml
Overrides:
- templates/my-template/marsh-template-overrides.yamlThe Build.Targets.Navigation configuration mapping has been replaced by the Build.Targets[].Sitemap.Source mapping. Setting Search to true replicates the previous behavior of treating the specified sitemap file name as a lookup key, where each found sitemap applies only to the its source subtree, and multiple sitemaps can coexist. Setting Search to false or omitting it entirely treats the specified sitemap as a single global sitemap for the target.
Old:
Build:
Targets:
- Name: My Target
Navigation: sitemap.markdownNew:
Build:
Targets:
- Name: My Target
Sitemap:
Source: sitemap.markdown
Search: trueThe Build.Targets.Navigation configuration mapping has been revised. Root_Path must be provided where a nested path is specified. Encoding must now be explicitly provided. Exclude has been renamed Exclude_States to be explicit.
Old:
Build:
Targets:
- Name: My Target
Archives:
- Name: My News Archive
Path: news/index.markdown
Types: [ news ]
Exclude: [ draft ]New:
Build:
Targets:
- Name: My Target
Archives:
- Name: My News Archive
Path: news/index.markdown
Root_Path: news
Encoding: html
Types: [ news ]
Exclude_States: [ draft ]The document template tag has been replaced with document.content to explicitly reference the rendered content of the current page or archive subdocument for inclusion.
Old:
{{ document }}
New:
{{ document.content }}
Previously, the document.authors template tag automatically removed RFC 2822 email addresses from author names and replaced spaces with non-breaking space HTML entities to avoid line breaks in the middle of author names, e.g., John Doe <john@example.com> would become John Doe. Additionally, multiple author names were joined by Oxford commas. This internal magic behavior has been removed.
This template tag now outputs the raw list of authors. Users must now use the text transforms email:remove-angle-addr, escape:nbsp, and items:join-type:oxford to produce the same result as before.
Old:
{{ document.authors }}
New:
{{ document.authors | email:remove-angle-addr | escape:nbsp | items:join-type:oxford }}
The navigation template tag has been replaced with target.sitemap. This includes the rendered sitemap markup for the current target, empty where no sitemap document is configured or found.
Old:
{{ navigation }}
New:
{{ target.sitemap }}
Previously, the template.assets.styles and template.assets.scripts template tags were automatically wrapped in HTML. This internal magic behavior has been removed.
These template tags now output the raw lists of paths. Users must now use the text transforms items:wrap-type:html-style-link and items:wrap-type:html-script-src to produce the same result as before.
Old:
{{ template.assets.styles }}
{{ template.assets.scripts }}
New:
{{ template.assets.styles | items:wrap-type:html-style-link }}
{{ template.assets.scripts | items:wrap-type:html-script-src }}
Remote source tree fetching has been deemed no longer in scope and thus the --fetch argument has been removed. Source trees must now be local.
The CLI terminal output has been revised and verbosity during building has been reduced. The new --log-level argument accepts the values info (default) and verbose, and the latter more closely mimics the previous behavior.
The default list of extensions used for choosing which files to treat as Markdown documents has been revised and may be configured using the --markdown-document-extensions parameter. The extension .markdown is always included regardless of user configuration. See the CLI help for more information.
The file extension for rendered HTML-from-Markdown documents is now configurable using the --markdown-document-extensions-replacement parameter, default .html. An important change is that this parameter now affects processing of Markdown-syntax relative links in documents. Relative links from your Markdown documents to other Markdown documents may now use the actual source document extension, e.g., .markdown or .md, instead of the rendered document extension, e.g., .html. In your documents, simply change your relative links from [Some Markdown Document](markdown-document.html) to [Some Markdown Document](markdown-document.md) where .md is the actual source document extension, and marsh will do the rest. Your built site will work as before, and the literal source references will also allow you to navigate between Markdown documents in supported applications such as GitHub's Web UI for browsing repository source trees.
When generating relative links, marsh now removes the file name index.html, creating links like about/ instead of about/index.html. The list of filenames to remove may be configured using the --remove-link-filenames parameter and specifying an empty string (--remove-link-filenames="") disables this functionality. See the CLI help for more information.
The code in the main marsh script is written by humans. LLMs analyzed the source for errors and inconsistencies. LLMs also informed the strategy for migrating the original architecture used in versions 0.8.3 and earlier to the planner-executor architecture in versions 1.0.0-alpha and later, performed static analysis throughout the migration, and suggested draft comments and commit messages throughout the migration.
The marsh-test test suite was originally written by humans. LLMs created some helper functions and added many additional test cases.
The documentation in README.markdown is written by humans. LLMs analyzed the documentation for gaps, errors, and inconsistencies.
The example website at example was originally written by humans. LLMs created the most of the mock news articles content for the example site.
The example template at templates/example is written by humans.
In summary:
- No AI-generated content exists in the main script, documentation, or example template. AI was only used for analysis-related tasks.
- The test suite and example site content contain AI-generated content.
Copyright 2026 Bradley Sepos
Released under the MIT License. See LICENSE for details.