Example blog

Blogatto ships with a complete working example at examples/simple_blog. This page walks through it step by step so you can see how all the pieces fit together.

Project layout

examples/simple_blog/
  src/
    simple_blog.gleam     # Build script
  blog/
    hello-world/
      index.md            # Blog post
    getting-started/
      index.md            # Blog post
  gleam.toml

Dependencies

The example’s gleam.toml pulls in Blogatto plus the standard library and Lustre:

name = "simple_blog"
version = "1.0.0"
target = "erlang"

[dependencies]
gleam_stdlib = ">= 0.44.0 and < 2.0.0"
lustre = ">= 5.6.0 and < 6.0.0"
gleam_time = ">= 1.7.0 and < 2.0.0"
blogatto = ">= 1.0.0 and < 2.0.0"

Build script

The build script lives in src/simple_blog.gleam. It configures every Blogatto feature: markdown rendering, routes, RSS, sitemap, and robots.txt.

Markdown configuration

The markdown config tells Blogatto where to find posts and how to render them:

let md_config =
  markdown.default()
  |> markdown.markdown_path("./blog")
  |> markdown.route_prefix("blog")
  |> markdown.template(blog_post_template)

RSS feed

let rss =
  feed.new(
    "Simple Blog",
    site_url,
    "A simple example blog built with Blogatto",
  )
  |> feed.language("en-us")
  |> feed.generator("Blogatto")

This generates an RSS 2.0 feed with a title, description, and language tag.

Sitemap and robots.txt

let sitemap_config = sitemap.new("/sitemap.xml")

let robots_config =
  robots.RobotsConfig(sitemap_url: site_url <> "/sitemap.xml", robots: [
    robots.Robot(
      user_agent: "*",
      allowed_routes: ["/"],
      disallowed_routes: [],
    ),
  ])

The sitemap collects all routes and blog post URLs into an XML sitemap. The robots.txt allows all crawlers access to the entire site.

Assembling the config

All pieces come together with the builder pattern:

let cfg =
  config.new(site_url)
  |> config.output_dir("./dist")
  |> config.markdown(md_config)
  |> config.route("/", home_view)
  |> config.feed(rss)
  |> config.sitemap(sitemap_config)
  |> config.robots(robots_config)

Then a single blogatto.build(cfg) call generates the entire site:

case blogatto.build(cfg) {
  Ok(Nil) -> io.println("Site built successfully in ./dist")
  Error(err) -> io.println("Build failed: " <> error.describe_error(err))
}

Homepage view

The homepage receives the full list of parsed blog posts and renders them as a linked list, sorted newest-first:

fn home_view(posts: List(Post(Nil))) -> Element(Nil) {
  let sorted_posts =
    list.sort(posts, fn(a, b) { timestamp.compare(b.date, a.date) })

  html.html([attribute.lang("en")], [
    html.head([], [
      html.meta([attribute.charset("UTF-8")]),
      html.meta([
        attribute.name("viewport"),
        attribute.content("width=device-width, initial-scale=1"),
      ]),
      html.title([], "Simple Blog"),
    ]),
    html.body([], [
      html.header([], [
        html.h1([], [element.text("Simple Blog")]),
        html.p([], [
          element.text("A simple example blog built with Blogatto."),
        ]),
      ]),
      html.main([], [
        html.h2([], [element.text("Articles")]),
        html.ul(
          [],
          list.map(sorted_posts, fn(p) {
            html.li([], [
              html.a([attribute.href("/blog/" <> p.slug)], [
                element.text(p.title),
              ]),
              element.text(" — "),
              html.em([], [element.text(p.description)]),
            ])
          }),
        ),
      ]),
      html.footer([], [
        html.p([], [
          element.text("Built with "),
          html.a([attribute.href("https://github.com/veeso/blogatto")], [
            element.text("Blogatto"),
          ]),
        ]),
      ]),
    ]),
  ])
}

Key points:

Blog post template

The template function wraps each blog post’s rendered markdown in a full HTML page:

fn blog_post_template(p: Post(Nil)) -> Element(Nil) {
  let lang = option.unwrap(p.language, "en")

  html.html([attribute.lang(lang)], [
    html.head([], [
      html.meta([attribute.charset("UTF-8")]),
      html.meta([
        attribute.name("viewport"),
        attribute.content("width=device-width, initial-scale=1"),
      ]),
      html.title([], p.title),
      html.meta([
        attribute.name("description"),
        attribute.content(p.description),
      ]),
    ]),
    html.body([], [
      html.header([], [
        html.nav([], [
          html.a([attribute.href("/")], [element.text("← Home")]),
        ]),
      ]),
      html.main([], [
        html.article([], [
          html.h1([], [element.text(p.title)]),
          html.p([], [html.em([], [element.text(p.description)])]),
          html.div([], p.contents),
        ]),
      ]),
      html.footer([], [
        html.p([], [
          element.text("Built with "),
          html.a([attribute.href("https://github.com/veeso/blogatto")], [
            element.text("Blogatto"),
          ]),
        ]),
      ]),
    ]),
  ])
}

Key points:

Blog posts

Each blog post lives in its own directory under blog/. The directory name becomes the slug.

hello-world/index.md

---
title: Hello World
slug: hello-world
date: 2025-01-15 00:00:00
description: Welcome to my new blog built with Blogatto
---

# Hello World

Welcome to my very first blog post! This blog was built using **Blogatto**,
a static site generator for Gleam.

## What is Blogatto?

Blogatto is a framework for building static blogs with Lustre and Markdown...

getting-started/index.md

---
title: Getting Started with Blogatto
slug: getting-started
date: 2025-01-20 00:00:00
description: Learn how to set up your first static blog with Blogatto
---

# Getting Started with Blogatto

Setting up a blog with Blogatto is straightforward...

Required frontmatter fields are title, slug, date, and description. Any additional fields (e.g. author, tags) are collected into the post’s extras dictionary.

Generated output

Running gleam run from the example directory produces:

dist/
  index.html                     # Homepage
  blog/
    hello-world/
      index.html                 # Blog post page
    getting-started/
      index.html                 # Blog post page
  feed.xml                       # RSS feed
  sitemap.xml                    # Sitemap
  robots.txt                     # Robots policy

Running the example

cd examples/simple_blog
gleam run

The site is written to ./dist. Open dist/index.html in a browser to see the homepage with links to both blog posts.