Deno logoDeno
Building a static site with Lume.

How to build a static site with Lume

Óscar Otero


This is a guest blog written by Óscar Otero, creator of Lume.

Deno is a great choice to create dynamic web applications and APIs, especially when used with Deno Deploy to generate responses at the edge.

But for use cases that don't require dynamic responses from the server, like blogs, documentation, and landing pages, a static site (a pre-built site with the exact HTML, CSS and JavaScript files to be delivered to the browser) is a better and more performant alternative.

In this tutorial we'll learn how to create a static site using Lume (pronounced /lume/), a fast and extremely flexible, composable, and extensible static site generator written in Deno.

View source or live demo.

Setup

The recommended way to setup a Lume project is by running the command:

deno run -Ar --unstable https://deno.land/x/lume/init.ts

After a couple of questions, you will see 3 new files in your working directory:

  • _config.ts: The configuration file to customise Lume.
  • deno.json: The Deno configuration file with some useful tasks to run Lume.
  • import_map.json: The import map file used by Deno to resolve bare specifiers.

Additionally, it's highly recommended to use the Deno extension if you're on VSCode. (Learn about configuring Deno with VSCode.)

Building a blog: the easy way

Blogs are the most common usage example for a static site generator and Lume, of course, can build one for you. If you are not looking for something too complicated, probably the "Simple blog" theme is enough. You only need to import it in the _config.ts file and use it.

import lume from "lume";
import blog from "https://deno.land/x/lume_theme_simple_blog@v0.2.1/mod.ts";

const site = lume().use(blog());

export default site;

Then save your posts using the markdown + front matter format in the /posts/ folder. For example:

---
title: Static sites with Lume + Deno Deploy
date: 2022-11-05
author: Óscar Otero
tags:
  - Deno
  - Static site generators
---

Deno is always a great choice to create dynamic web applications and APIs,
especially when it's combined with Deno Deploy to generate responses at the
edge. ...

Run deno task serve and you will see your new blog at localhost:3000!

But I want to craft something by myself

Okay, understood! Instead of using an existing theme, let's build something from scratch.

Getting started

Lume doesn't require any file structure to work, but for this demo, we will save all posts files in a directory named /posts. So we have the following structure:

|_ posts/
|  |_ my-first-post.md
|  |_ my-second-post.md
|_ config.ts
|_ deno.json
|_ import_map.json

Run deno task serve to see the site at localhost:3000. You will see a 404 error page because the index file doesn't exist yet.

Getting a 404 page not found

But you can see the post. By default, the urls of the posts are calculated based on the source path. For example the file /posts/my-first-post.md outputs the URL http://localhost:3000/posts/my-first-post/.

My first post

The layout

As you can see, the page only shows the markdown content rendered as HTML. Let's create a layout to wrap this content into a proper HTML structure. By default, the layouts are stored in the special folder _includes, so we need to create this folder and the file post.njk inside with the following content:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>{{ title }}</title>
</head>
<body>
  <main>
    <article>
      <header>
        <h1>{{ title }}</h1>
        <p>By {{ author }}</p>
      </header>

      {{ content | safe }}
    </article>
  </main>
</body>
</html>

The .njk extension is for Nunjucks a popular templating language created by Mozilla and supported by Lume by default. Of course, you can use other formats like JavaScript, TypeScript or even JSX, Pug, Eta, etc (using plugins), but let's keep it simple for now.

We'll create a _data.yml file inside the posts folder with the following content:

layout: post.njk
type: post

This file assigns these two variables to all posts in this directory.

Let's ignore the type variable for now.

The layout variable is a special value containing the filename of the layout used to render the page. This means that all pages in this directory will use the _includes/post.njk file as the layout. Note that the layout can access to the values of the front matter of the posts, like title or author.

Our post looks a bit better now!

First post but with some style

List all posts

Let's say we want to show a list of all posts in the homepage. For this example I'm going to create the homepage in TypeScript, so we need to create the index.tmpl.ts file in the root with the following code:

import type { PageData } from "lume/core.ts";

export default function ({ search }: PageData) {
  const posts = search.pages("type=post");

  return `
    <h2>Posts</h2>
    <ul>
      ${
    posts.map((post) =>
      `<li><a href="${post.data.url}">${post.data.title}</a></li>`
    ).join("")
  }
    </ul>
  `;
}

Building pages with TypeScript in Lume is easy, we only need to export default a function that returns the page content as string.

This function receives the page context data as the first parameter, including some interesting helpers like search that we can use to query and return all pages with the type=post variable. (Remember the type value that we inserted in the posts/_data.yml file before? It was only a flag to easily select these pages here.)

Our homepage looks something like this:

The index page

I could include the full HTML code in the page but I love layouts, so I've created the following _includes/homepage.njk layout file:

---
title: The Óscar's blog
---

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>{{ title }}</title>
</head>
<body>
  <main>
    <header>
      <h1>{{ title }}</h1>
    </header>

    {{ content | safe }}
  </main>
</body>
</html>

Now we have to configure the index.tmpl.ts file to use this layout. TypeScript files don't have front matter to store data, they just expose the values as named exports:

import type { PageData } from "lume/core.ts";

export const layout = "homepage.njk";

export default function ({ search }: PageData) {
  const posts = search.pages("type=post");

  return `
    <h2>Posts</h2>
    <ul>
      ${
    posts.map((post) =>
      `<li><a href="${post.data.url}">${post.data.title}</a></li>`
    ).join("")
  }
    </ul>
  `;
}

The homepage looks a bit better now:

Styling the home page

Adding styles

Lume has a bunch of plugins to work with JavaScript, to support formats like JSX, MDX, etc. For styling purposes, there are some available plugins to work with SASS, PostCSS, WindiCSS or LighningCSS.

For simplicity, for this demo we're going to use the awesome missing.css (by the way, the documentation site is also built with Lume). This css file provides some nice styles out of the box, without need to modify our HTML. To do that, just include the <link rel="stylesheet" href="https://the.missing.style/"> line in the two layouts we just created.

The home page but much better styled

Ready to deploy

One of the many advantages of the static sites is the hosting. You can host your site anywhere, with almost no server requirements.

In this demo we are going to use Deno Deploy to serve the site. Thanks to its design, where all incoming requests are handled by a JavaScript file, we can customise how the files of your site will be delivered.

Step 1: Create a project in Deno Deploy

Go to Deno Deploy and create a new project. You will see a screen like this:

Create a new project in Deno Deploy

An important thing when selecting the branch, is to use the GitHub Action mode. This is because Deno Deploy doesn't have a CI to build the site, so we need to use the GitHub Actions workflows to build and upload the static site to Deno Deploy.

Then click in the Link button and you will see the following screen:

Get the GitHub Action for Deno Deploy

Deno Deploy has generated the code needed to configure the GitHub Actions workflow. Copy this code and save it into .github/workflows/deploy.yml.

Step 2: Configure GitHub Actions

The GitHub Action workflow file that we have just created needs a couple of tweaks marked as #TODO comments:

  • Set up the build step. In our case we need to setup Deno and run deno task build.
  • Update the entrypoint to use deno_std's file_server.ts, with the root set to ./_site.

Note that if you want to use Lume's middleware, such as expires for caching or not_found to show a custom 404 page, you would need to create a new serve.ts, add your middleware, and set that as the entrypoint for Deno Deploy.

After these modifications, this is the final version:

name: Deploy
on: [push]

jobs:
  deploy:
    name: Deploy
    runs-on: ubuntu-latest
    permissions:
      id-token: write # Needed for auth with Deno Deploy
      contents: read # Needed to clone the repository

    steps:
      - name: Clone repository
        uses: actions/checkout@v3

      - name: Setup Deno environment
        uses: denoland/setup-deno@v1
        with:
          deno-version: v1.x

      - name: Build site
        run: deno task build

      - name: Upload to Deno Deploy
        uses: denoland/deployctl@v1
        with:
          project: oscarotero-lume-blog-demo
          entrypoint: https://deno.land/std@0.167.0/http/file_server.ts
          root: ./_site

Push the changes to GitHub and your site will be built and uploaded to Deno Deploy in a few seconds.

Successful deployment on Deno Deploy

Click in the View blue button to see your new blog at *.deno.dev (or a different domain if you add a custom domain).

Oscars blog served from Deno Deploy

And that's all. It was easy, wasn't it? Take a look at the Lume documentation site to learn more about this static site generator and see real examples in the Showcase section.

Stuck? Come say hi in Deno's Discord!