Lume templating
23rd November 2024This website is built using the fantastic Lume static site generator, using the Vento template engine. The Lume and Vento docs are both really well written, but there were a few things I had to figure out for myself when setting up the site, which I'm detailing here for anyone else taking the same approach.
đ My template structure
This site's structure is fairly flat, and looks like this:
- _site/
- src/
- _includes/
- footer.vto
- header.vto
- main.vto
- posts.vto
- posts/
- _data.yml
- drafts/
- _data.yml
- a-post-im-working-on.md
- index.md
- lume-templating.md
- static/
- styles.light.css
- about.md
- index.md
- _includes/
- _config.ts
- deno.json
- deno.lock
- deploy.sh
Most of this is really standard Lume layout, but I've taken advantage of some handy features like the fact that _data.yml
Shared data propagates down the tree.
âī¸ My _config.ts
My _config.ts looks like:
import lume from "lume/mod.ts";
import date from "lume/plugins/date.ts";
import basePath from "lume/plugins/base_path.ts";
import footnote from "npm:markdown-it-footnote";
import mark from "npm:markdown-it-mark";
const markdown = {
plugins: [
footnote,
mark,
]
}
const site = lume({
src: "./src",
}, { markdown });
site.copy("/static");
site.use(date());
site.use(basePath());
export default site;
It's not particularly complicated, even if it's a little longer than the default generated config.
đ Plugins
I'm enabling some markdown-it plugins (like markdown-it-footnote and markdown-it-mark), and the Lume date and Lume basePath plugins.
The markdown-it plugins let me add footnotes[1] and highlights to my documents.
The Lume date plugin makes it easy to parse post dates when rendering the post header (at the top of the page).
The basePath plugin automatically fixes any links (e.g. to images and stylesheets) if I deploy on anything but the root / path. This lets me deploy to a subfolder on my test site without changing any of my file references. For example, my stylesheet link looks like <link rel="stylesheet" href="/static/styles.light.css" />
, but if I deploy to https://mytestsite.example/staging/ then the stylesheet will actually be located at /staging/static/styles.light.css and not /static/styles.light.css. By enabling the baseURL plugin and building my site with deno task build --location=https://mytestsite.example/staging/
, these links are automatically rewritten. This isn't so important for this site, but for more complex ones at my day job, where I might want to have someone check the content over before publishing, it gives some much needed flexibility in deployment.
The src folder
I've split my content off into the src folder. I actually made that choice because my other Lume site docs.cis has a bunch of non-content things (build scripts, deploy scripts, design files, etc) that live in the repo and I wanted Lume to only process my actual site content. I've replicated that on my personal site here.
Copying static files
I copy static assets (like CSS and images) in the fairly standard way, per the Lume docs.
đ¤ _includes
I'm using a really simple Vento layout (template) structure, with a base layout (main.vto) which drives most non-post pages (like the about page) and a posts.vto template which is specifically for posts (adding the post title and date to each page).
main.vto has normal HTML boilerplate and a section that looks like:
<div class="main">
{{ include "header.vto" }}
<div class="content">
{{ content }}
</div>
{{ include "footer.vto" }}
</div>
posts.vto looks like:
{{ layout "main.vto" }}
<div class="post">
<h1>{{ title }}</h1>
<span class="post-date">{{ date |> date("do MMMM yyyy") }}</span>
<div class="post-body">
{{ content }}
</div>
</div>
This works because setting my post to use posts.vto as the layout will propagate the content variable back up the template tree. My post content goes into the content variable in posts.vto, and that rendered text (the whole template) becomes the content variable in main.vto because I defined {{ layout "main.vto" }}
.
âī¸ _data.yml files
I use a few _data.yml files at different levels, taking advantage of the fact that they propagate down the tree (so src/posts/_data.yml will override anything already set in src/_data.yml).
src/_data.yml
My base src/_data.yml is pretty simple:
templateEngine: [vto, md]
layout: main.vto
This lets me run my content through both Vento and markdown-it, so I can include Vento template code (like if's and loops) in my Markdown files.
src/posts/_data.yml
My src/posts/_data.yml defines some post-specific things:
layout: posts.vto
tags:
- post
Any content files in the src/posts
folder will use the posts.vto
layout (discussed above) and also be tagged with post. I use the tag later when rendering my index of posts (I don't want /about showing up on the recent posts list, only things inside the src/posts folder that are tagged with post).
src/posts/drafts/data.yml
Lume automatically pics up the draft: true attribute in content file frontmatter, and leaves it out of the site build. Rather than mark each individual post as a draft until I'm ready to publish it, I put them in a drafts subfolder which has a data.yml that looks like:
draft: true
Thereby excluding everything in that folder from the build.
đ Post index
The post index (src/posts/index.md) is the last piece I had to figure out, and it's actually quite simple (even if it doesn't look so!).
---
layout: main.vto
tags: unindexed
---
## All posts
{{- set posts = search.pages("post !unindexed", "date=desc title=asc") }}
{{- if posts.length > 0 }}
<ul>
{{- for post of posts }}
<li><a href="{{ post.url }}">{{ post.title }}</a> <span class="post-list-date">{{ post.date |> date("do MMMM yyyy") }}</span></li>
{{- /for }}
</ul>
{{- else }}
There are no posts yet.
{{- /if }}
Here, we tag that index itself as unindexed, which stops is appearing in the list of posts in the search query that follows.
We then find all of the posts (everything with a post tag) that aren't unindexed with {{- set posts = search.pages("post !unindexed", "date=desc title=asc") }}
. If there are no posts, we show a message to that effect.
If there are posts found, we loop through them and create a table of links to them.
In this page, we're finally using that Lume date plugin we turned on earlier, with {{ post.date |> date("do MMMM yyyy") }}
. This formats the post date from the frontmatter (which is in the format YYYY-MM-DD and automatically parsed by Lume into the date field). This prints the date in a nice format like 23rd November 2024.
đ Conclusion
And that's it! đ
It looks like a lot, but after running deno run -A https://lume.land/init.ts
to initialise my site, these are the only changes I really had to make to create a structure that really works.
The combination of Lume, Vento, markdown-it and Deno is really slick, and beats writing yet another DIY (NIH) static site generator[2].