ijsbakje

enclume

Hit your content on the anvil until a website comes out.

License: The MIT License (MIT)

Enclume

Enclume (/ɑ̃klym/ · french noun for anvil) is a simple static site generator built for simplicity and flexibility.

I built Enclume after years of using Astro for my personal website. I was getting annoyed at the amount of configuration and indirection layers needed for a simple blog, and I yearned for the simpler days of editing source from /var/www via FTP. So with Enclume, I said no more complicated config, no more centralized variables, we are bringing the static in static site generator, while keeping the build step for templating and parsing content.

Quickstart

Enclume can be installed from source (requires the Rust toolchain installed)

cargo install enclume --locked

Some functionalities are gated behind a crate feature:

How does it work?

To live up to its purpose, Enclume needs to adapt to all sorts of website structures while adding as little complexity as possible. This is a minimal website you can build right now

src/
  globals.yaml
  templates/
    layout.html
    post.html
  root/
    index.html
    style.css
    blog/
      post.md
      other_post.html

or just use enclume init to scaffold something similar.

To generate the matching static website, you can use

enclume build --base src --out dist

A dev command is also available which serves the built website on localhost and rebuilds when the source changes.

Parsing

Enclume provides four parsers: Markdown (.md), HTML (.html), JSON (.json) and YAML (.yaml). The program walks your root/ folder and parses files along the following logic:

So parseable files are determined by the presence of a YAML frontmatter of the kind

---
template: "post.html"
vars:
  title: "This is a blog post"
---
Some content

The frontmatter should include the following keys:

Note: The output field is particularly useful for non-HTML output like rss.xml or sitemap.xml.

Templates

Templates are built using the Tera template engine. The following variables and functions are provided to the templating engine and can be used in your templates:

For example, this could be the layout

<!-- layout.html -->
<!DOCTYPE html>
<html lang="en">
<head>
    {% block head %}
    <link rel="stylesheet" href="{{ asset(path=\"style.css\") }}" />
    <title>{% block title %}{% endblock title %} - My Webpage</title>
    {% endblock head %}
</head>
<body>
    <div id="content">{% block content %}{% endblock content %}</div>
</body>
</html>

and the blog post template

<!-- post.html -->
{% extends "base.html" %}

{% block title %}{{vars.title}}{% endblock title %}

{% block content %}
  {{content}}
{% endblock content %}

Pages are built with clean URLs by default: about.md becomes about/index.html. Pass --no-clean-urls to build about.html instead.

Pass --hash-assets to add a content hash to the name of every css and js file (style.css becomes style.<hash>.css), which asset() resolves. This

Site-wide variables

The optional src/globals.yaml is meant for site-wide variables

# globals.yaml
title: "My Webpage"
base_url: "https://example.com"
author: "Jane Doe"

Every template reads them under globals:

<title>{{ globals.title }}</title>
<a rel="author" href="/about">{{ globals.author }}</a>

Listing other pages

A template cannot read the filesystem, but it can list the pages the build parsed. The pages() function takes a glob pattern, relative to root/ and returns the matching pages in source-path order:

<!-- blog-list.html -->
<ul>
{% for post in pages(pattern="blog/**/*.md") %}
    {% if post.url != __url__ %}
    <li><a href="{{ post.url }}">{{ post.vars.title }}</a> ({{ post.vars.publishDate }})</li>
    {% endif %}
{% endfor %}
</ul>

Each entry holds path (the source file), url (where it is served, clean URLs included) and vars (everything the file declared under vars).

Sitemap and feeds

Sitemap and feeds need to be mapped manually using the output variable

<!-- root/sitemap.md -->
---
template: sitemap.xml
output: sitemap.xml
---
<!-- templates/sitemap.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
{% for page in pages(pattern="blog/**/*.md") %}
    <url><loc>{{ globals.base_url }}{{ page.url }}</loc></url>
{% endfor %}
</urlset>

Table of contents

The headings of a Markdown page are collected into a toc variable passed to the template

{% if toc %}
<nav class="toc">
    <ul>
    {% for heading in toc %}
        <li class="level-{{ heading.level }}"><a href="#{{ heading.id }}">{{ heading.title }}</a></li>
    {% endfor %}
    </ul>
</nav>
{% endif %}

Each entry holds the level of its heading (1 for #, 2 for ##, and so on), the id a link to it ends with, and the title of the heading with its inline markup flattened.

Math

Markdown content can hold LaTeX formulas. They are typeset while the site is built, by a Rust port of KaTeX, into the markup KaTeX itself produces: HTML, plus a MathML copy of every formula for screen readers and for text selection. No JavaScript runs in the browser.

Inline math is written between single dollars, like $E = mc^2$.

Display math is written between double dollars, on a line of its own:

$$
\int_0^1 x \, dx = \frac{1}{2}
$$

or as a `math` block:

```math
\int_0^1 x \, dx = \frac{1}{2}
```

Icons

An icon is a namespace and a name between double colons. It renders as an inline <svg> which takes the size and the colour of the text around it, so prose needs no stylesheet:

Press ::lucide:arrow-left:: to go back, or see the branch history with ::octicon:git-branch::.

Three sets are always available by default:

An Octicon may be named without its size, so ::octicon:git-branch:: and ::octicon:git-branch-24:: draw the same icon. Simple Icons are named as the brand publishes them, so ::si:rust:: and ::si:Rust:: both work.

Font Awesome Free sits behind the fontawesome feature, which is off by default: cargo build --features fontawesome enables ::fa:rocket::, ::fa-solid:rocket::, ::fa-regular:bell:: and ::fa-brands:github::.

AI usage

I used AI as an assistant: review what I did, give me some ideas on how to implement stuff, and for clear and simple task, do it and I'll review. But the core decisions and initial project build was all human.