# Blogatto

[](https://hex.pm/packages/blogatto)
[](https://hexdocs.pm/blogatto/)
[](https://conventionalcommits.org)
[](https://www.erlang.org/)
[](https://github.com/veeso/blogatto/actions/workflows/test.yml)
A Gleam framework for building static blogs with [**Lustre**](https://hexdocs.pm/lustre/), Markdown, and [Djot](https://djot.net/).
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](https://hexdocs.pm/lustre/) views, RSS and Atom feeds, sitemaps, and robots.txt — all rendered via [Maud](https://hexdocs.pm/maud/) components.
## Features
- Blog posts from Markdown (`.md`) or Djot (`.dj`/`.djot`) files with YAML frontmatter
- Multilingual posts via `index-{lang}.md` file naming convention
- Static pages from [Lustre](https://hexdocs.pm/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](https://hexdocs.pm/smalto/)
- Configurable blog post templates
- Dev server with file watching, auto-rebuild, and live reload
## Installation
Blogatto is available on [Hex](https://hex.pm/packages/blogatto). Add it as a dependency to your project:
```sh
gleam add blogatto
```
## Quick start
```gleam
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:
```txt
dist/
├── blog/
│ └── my-post/
│ └── index.html
├── index.html
├── robots.txt
├── sitemap.xml
├── rss.xml
└── atom.xml
```
## Dev 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`):
```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:
1. Perform an initial build by running the configured build command
2. Serve the output directory over HTTP at `http://127.0.0.1:3000`
3. Watch `src/`, post source paths, and static assets for changes
4. Debounce rapid file changes (~300ms) and rebuild automatically
5. 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-tools` to be installed.
## Documentation
Full documentation is available at [blogat.to](https://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](https://hexdocs.pm/blogatto/).
## Development
```sh
gleam build # Compile the project
gleam test # Run the tests
gleam format src test # Format code
```
## License
Blogatto is licensed under the MIT License. See [LICENSE](LICENSE) for details.