gssg
Batteries-not-included, templating library agnostic SSG library for Gleam.
Features
- Simple API: Just four functions to build a complete static site
- Type-safe: Leverages Gleam’s type system for compile-time guarantees
- Flexible: Works with any page type (Lustre elements, strings, custom types)
- Dynamic routes: Generate multiple pages from data (perfect for blog posts)
- Zero config: No configuration files needed, just code
Installation
gleam add gssg
Quick Start
import gleam/dict
import gleam/io
import gleam/string
import lustre/element
import gssg/ssg
pub fn main() -> Nil {
let result =
ssg.new("priv", element.to_document_string)
|> ssg.add_static_route("/", home_view())
|> ssg.add_static_route("/about", about_view())
|> ssg.build()
case result {
Ok(_) -> io.println("Build successful!")
Error(e) -> io.println("Build failed: " <> string.inspect(e))
}
}
fn home_view() {
element.html([], [
element.head([], [element.title([], "Home")]),
element.body([], [element.h1([], [element.text("Welcome!")])]),
])
}
fn about_view() {
element.html([], [
element.head([], [element.title([], "About")]),
element.body([], [element.h1([], [element.text("About Us")])]),
])
}
This generates:
priv/index.html- Your home pagepriv/about/index.html- Your about page
Usage
Creating a site
Start by creating an SSG instance with an output directory and a render function:
import lustre/element
import gssg/ssg
let site = ssg.new("output", element.to_document_string)
The render function converts your page type to HTML strings. For Lustre elements, use element.to_document_string.
Adding static routes
Add individual pages with add_static_route:
ssg.new("priv", element.to_document_string)
|> ssg.add_static_route("/", home.view())
|> ssg.add_static_route("/projects", projects.view())
|> ssg.add_static_route("/contact", contact.view())
Each route generates a file at {path}/index.html.
Adding dynamic routes
Generate multiple pages from a collection of data:
import gleam/dict
import markup/markup
pub fn main() -> Nil {
// Parse blog posts from markdown files
let assert Ok(posts) = markup.parse_dir("data/posts")
ssg.new("priv", element.to_document_string)
|> ssg.add_static_route("/", home.view())
|> ssg.add_static_route("/blog", blog_index.view(posts))
|> ssg.add_dynamic_route("/blog", dict.from_list(posts), blog_post.view)
|> ssg.build()
}
If posts contains:
#("hello-world", post1)#("getting-started", post2)
This generates:
priv/blog/hello-world/index.htmlpriv/blog/getting-started/index.html
Building the site
Call build() to render and write all files:
let result = ssg.build(site)
case result {
Ok(_) -> io.println("Build successful!")
Error(ssg.CannotWriteFile(path, reason)) -> {
io.println("Failed to write: " <> path)
}
}
Complete Example
Here’s a complete example with static pages and a blog:
import gleam/dict
import gleam/io
import gleam/string
import lustre/element
import markup/markup
import pages/home
import pages/writing/post
import pages/writing/writing
import gssg/ssg
pub fn main() -> Nil {
let assert Ok(posts) = markup.parse_dir("data/posts")
let result =
ssg.new("priv", element.to_document_string)
|> ssg.add_static_route("/", home.view())
|> ssg.add_static_route("/projects", markup.from("data/pages/projects.dj"))
|> ssg.add_static_route("/writing", writing.view(posts))
|> ssg.add_dynamic_route("/writing", dict.from_list(posts), post.view)
|> ssg.build()
case result {
Ok(_) -> io.println("build successful.")
Error(e) -> io.println("build failed: " <> string.inspect(e))
}
}
License
This project is licensed under the Apache-2.0 License.