Scriptorium User's Guide

This user's guide is a work-in-progress. Please keep checking back for more information. If you have any questions or feedback, don't hesitate to ask me on the fediverse or on the Gleam Discord (user Nicd).

Table of Contents

  1. Prerequisites & Installation
  2. Default Filesystem Layout
  3. Writing a Post
  4. Customizing Your Blog

Prerequisites & Installation

Scriptorium requires the following software:

  • Gleam 1.4+
  • Node.js (tested on version 20)

In addition, you will need at least a preliminary understanding of the Gleam language. The default blog setup does not require any special knowledge, but customizing the generation process will require writing Gleam code.

Typically creating a blog with Scriptorium requires creating a new Gleam project. When you have a Gleam project, you can add the library as a dependency by using gleam add scriptorium.

To set up a basic blog, replace the project's main file with the following:

import gleam/result
import gleam/option
import gleam/io
import scriptorium/builder
import scriptorium/config.{type Configuration}
import scriptorium/defaults

pub fn main() {
  let config =
    defaults.default_config(
      "My Blog",
      "https://my.blog.example/",
      "en",
      config.Author(
        name: "Person McPerson",
        email: option.Some("person@example.com"),
        url: option.Some("https://fedi.instance.example/@person"),
      ),
      "© Person McPerson",
    )

  io.debug(build(config))
}

pub fn build(config: Configuration) {
  // Parse the files
  use db <- result.try(builder.parse(config))
  // Compile Markdown into HTML
  let compiled = builder.compile(db, config)
  // Render content into Lustre elements
  let rendered = builder.render(db, compiled, config)
  // Write rendered content into the filesystem
  builder.write(rendered, config)
}

This is the minimum setup for building a blog. Now the blog can be generated with gleam run.

Default Filesystem Layout

By default the blog generator expects the following folders to exist:

  • data – Master folder for input data, inside the root of the project folder.
    • posts – Folder for posts.
    • pages – Folder for pages.

As an illustrative example of the contents, the filesystem contents of this blog at the time of writing were:

  • data
    • menu
    • pages
      • 404.md
      • guide.md
    • posts
      • 2024-04-14-hello-world.md
      • 2024-04-21-scriptorium-published.md

Writing a Post

Post Filename

To write a post, create a new file in the ./data/posts folder. The filename must consist of the following:

  • an ISO 8601 formatted date, i.e. YYYY-MM-DD,
  • a dash,
  • an optional zero-padded 2-digit order number used for ordering when two posts were written on the same day and don't have time information, followed by a dash,
  • a slug that is a free-form name for the post used in the post filename and thus the final URL, and
  • the file extension .md.

Note that because the slug is used in the filename and in the URL, it should only contain filename and URL safe text. The recommendation is to stick to regular characters, dashes, and underscores, without any spaces. "Regular characters" here does not exclude non-latin characters, as they are safely percent-encoded in a URL.

Example filenames:

  • 2025-01-09-my-post.md
  • 2025-01-10-01-first-post-for-the-day.md
  • 2025-01-10-02-second-post-for-the-day.md

Post Contents

Scriptorium posts are written in Markdown. Marked.js is used for compiling Markdown, so its documentation should be consulted when there is a question about how something is rendered.

Header

There are, however, some special parts at the start of a post. Let's look at an example:

Post Title
tag1, tag2, tag3
time: 21:15 Europe/Helsinki
description: Example post
image: https://example.com/example.jpg

Hello, and welcome to this example post!

The first line in a file is the post title. After that, optionally, come post tags on their own line, separated by a comma. Post tags are used as-is, so they also should contain only filename and URL safe content, and no spaces.

After the tags are headers. Headers are a collection of key-value pairs, separated by a colon. They can contain various metadata about the post, and the user can implement their own headers by customizing the post view. There are some predefined headers:

  • time – Defines the time when the post was written. The format is hh:mm TZ, where the first part is the local time using a 24 hour clock, and the second is the local timezone identifier.
  • description – A description of the post that is used for link embeds on external services.
  • image – An image that is used for link embeds on external services. Should be on the smaller side and must be an absolute URL.
Splitting

Often it is not sensible to show the whole post in list views with many posts. In this case the post can be divided into two pieces. In the list view, only the first part is shown, and in the individual post page, both parts are shown.

To do this, insert <!-- SPLIT --> in the post. It's recommended to put this on its own line and not inside any HTML or Markdown content that would get broken when split apart.

Customizing Your Blog

Scriptorium is designed to be customizable. It offers you helper functions to easily render your blog, but nearly all of them can be replaced with custom versions, with more or less trouble depending on the use case. Do note, however, that customizing your Scriptorium build does require knowledge of Gleam. Also, since Scriptorium uses Lustre extensively and view functions generate and accept Lustre elements, it is good to peruse Lustre's documentation too.

All configuration is collected into the Configuration type to be fed into the generator. Thus the first step in customizing is to look at things that can be set through the Configuration type. To generate a sensible default configuration, use the function scriptorium/defaults.default_config, which takes things you would commonly change as arguments.

Now let's say you wish to change the amount of posts per page, which is in the Rendering type under key rendering of the main configuration, while keeping everything else as default. You could do something like this:

import scriptorium/config
import scriptorium/defaults.{default_config}

let default_configuration = default_config(
  // Some values here
)

let rendering = config.Rendering(
  ..default_configuration.rendering,
  posts_per_page: 20
)

let final_configuration = config.Configuration(
  ..default_configuration,
  rendering:
)

When you desire more advanced customization, the API surface is full of functions that you can replace with your own. These functions can be dropped into your configuration to replace the default ones. Below are some example customizations.

Adding Custom CSS

This works if you only intend to do minor changes or additions to the builtin styles. Essentially you edit the base template and add a new meta tag to it. Here it is in a code sample:

import lustre/attribute
import lustre/element/html
import scriptorium/config
import scriptorium/defaults
import scriptorium/rendering/views/base

// Add overridden template to the configuration
let c =
  defaults.default_config(
    // Default configs
  )

let c =
  Configuration(
    ..c,
    rendering: config.Rendering(
      ..c.rendering,
      views: config.Views(..c.rendering.views, base: custom_base),
    ),
  )

/// Custom base view function. It must match the BaseView type in
/// https://hexdocs.pm/scriptorium/scriptorium/rendering/views.html#BaseView
fn custom_base(db, config) {
  // Generate Scriptorium original base view function
  let orig = base.generate(db, config)

  // Return custom view function that takes the same arguments as the original
  // (see the type BaseView for a description of the arguments).
  fn(inner, extra_meta, title_prefix) {
    // Call the original function with a customized `extra_meta` argument.
    orig(
      inner,
      [
        // Add link to custom CSS file into the meta tags.
        html.link([
          attribute.rel("stylesheet"),
          attribute.href("/static/css/overrides.css"),
        ]),
        ..extra_meta
      ],
      title_prefix,
    )
  }
}

Do note that since we import Lustre, we must also add it as a direct dependency to our Gleam project with gleam add lustre. Additionally, Scriptorium will not copy the CSS file to the output directory by itself, you will have to do that yourself.