Blogatto
A Gleam framework for building static blogs with Lustre, Markdown, and Djot.
Blogatto generates your entire static site from a single configuration: blog posts from Markdown (.md) or Djot (.dj/.djot) files with YAML frontmatter, static pages from Lustre views, RSS and Atom feeds, sitemaps, and robots.txt — all rendered via Maud components.
Features
-
Blog posts from Markdown (
.md) or Djot (.dj/.djot) files with YAML frontmatter -
Multilingual posts via
index-{lang}.mdfile naming convention - Static pages from Lustre view functions
- RSS 2.0 and Atom 1.0 feed generation with customizable filtering and serialization
- Sitemap XML generation with alternate language links
- Robots.txt generation
- Static asset copying
- Custom Maud components shared across Markdown and Djot rendering
- Build-time syntax highlighting for code blocks via Smalto
- Configurable blog post templates
- Dev server with file watching, auto-rebuild, and live reload
Installation
Blogatto is available on Hex. Add it as a dependency to your project:
gleam add blogattoQuick start
import blogatto
import blogatto/config
import blogatto/config/feed/atom
import blogatto/config/feed/rss
import blogatto/config/post
import blogatto/config/robots
import blogatto/config/sitemap
import blogatto/error
import blogatto/post.{type Post}
import gleam/io
import gleam/list
import gleam/time/timestamp
import lustre/attribute
import lustre/element.{type Element}
import lustre/element/html
const site_url = "https://example.com"
pub fn main() {
// Post config with custom heading component
let post_config =
post.default()
|> post.path("./blog")
|> post.route_prefix("blog")
|> post.h1(fn(id, children) {
html.h1([attribute.id(id), attribute.class("post-title")], children)
})
// RSS feed
let rss_feed =
rss.new("My Blog", site_url, "My personal blog")
|> rss.language("en-us")
|> rss.generator("Blogatto")
// Atom feed
let atom_feed =
atom.new(
id: site_url <> "/",
title: atom.PlainText("My Blog"),
updated: timestamp.system_time(),
)
|> atom.subtitle("My personal blog")
// Build configuration
let cfg =
config.new(site_url)
|> config.output_dir("./dist")
|> config.static_dir("./static")
|> config.post(post_config)
|> config.route("/", home_view)
|> config.rss_feed(rss_feed)
|> config.atom_feed(atom_feed)
|> config.sitemap(sitemap.new("/sitemap.xml"))
|> config.robots(robots.RobotsConfig(
sitemap_url: site_url <> "/sitemap.xml",
robots: [
robots.Robot(
user_agent: "*",
allowed_routes: ["/"],
disallowed_routes: [],
),
],
))
case blogatto.build(cfg) {
Ok(Nil) -> io.println("Site built successfully!")
Error(err) -> io.println("Build failed: " <> error.describe_error(err))
}
}
fn home_view(posts: List(Post(Nil))) -> Element(Nil) {
let sorted =
list.sort(posts, fn(a, b) { timestamp.compare(b.date, a.date) })
html.html([], [
html.head([], [html.title([], "My Blog")]),
html.body([], [
html.h1([], [element.text("My Blog")]),
html.ul(
[],
list.map(sorted, fn(p) {
html.li([], [
html.a([attribute.href("/blog/" <> p.slug)], [
element.text(p.title),
]),
])
}),
),
]),
])
}
Running gleam run will generate the dist directory with the following structure:
dist/
├── blog/
│ └── my-post/
│ └── index.html
├── index.html
├── robots.txt
├── sitemap.xml
├── rss.xml
└── atom.xmlDev server
Blogatto includes a built-in development server that watches your source files for changes, automatically rebuilds the site, and live-reloads the browser via SSE.
Create a separate dev entrypoint module (name it like your main module, but with _dev postfix and put it in the dev folder, e.g. dev/my_blog_dev.gleam):
import blogatto/dev
import blogatto/error
import gleam/io
import my_blog // your module that exposes your blogatto config
pub fn main() {
let cfg = my_blog.config()
case
cfg
|> dev.new()
|> dev.build_command("gleam run -m my_blog")
|> dev.port(3000)
|> dev.start()
{
Ok(Nil) -> io.println("Dev server stopped.")
Error(err) -> io.println("Dev server error: " <> error.describe_error(err))
}
}
Run with: gleam dev
The dev server will:
- Perform an initial build by running the configured build command
-
Serve the output directory over HTTP at
http://127.0.0.1:3000 -
Watch
src/, post source paths, and static assets for changes - Debounce rapid file changes (~300ms) and rebuild automatically
- Live-reload the browser on successful rebuilds
Configuration
| Option | Default | Description |
|---|---|---|
build_command | "gleam run" | Shell command to rebuild the site |
port | 3000 | HTTP server port |
host | "127.0.0.1" | Bind address |
live_reload | True | Inject live-reload script into HTML responses |
Note for Linux users: The file watcher requires
inotify-toolsto be installed.
Documentation
Full documentation is available at blogat.to, covering blog post structure, configuration, post components, static pages, RSS and Atom feeds, sitemaps, dev server, and error handling.
API reference is on HexDocs.
Development
gleam build # Compile the project
gleam test # Run the tests
gleam format src test # Format codeLicense
Blogatto is licensed under the MIT License. See LICENSE for details.