Static pages
Static pages are non-blog HTML pages generated from Lustre view functions. Use them for homepages, about pages, archives, or any page that isn’t a markdown blog post.
Adding routes
Register routes with config.route(path, view). Each route maps a URL path to a view function:
import blogatto/config
let cfg =
config.new("https://example.com")
|> config.route("/", home_view)
|> config.route("/about", about_view)
|> config.route("/archive", archive_view)
Output paths
Routes map to {output_dir}/{route}/index.html:
| Route | Output file |
|---|---|
"/" |
dist/index.html |
"/about" |
dist/about/index.html |
"/archive" |
dist/archive/index.html |
Writing view functions
Every view function receives the full list of blog posts parsed during the build and returns a Lustre Element(msg):
import blogatto/post.{type Post}
import lustre/element.{type Element}
import lustre/element/html
fn about_view(_posts: List(Post(Nil))) -> Element(Nil) {
html.html([], [
html.head([], [html.title([], "About")]),
html.body([], [
html.h1([], [element.text("About this blog")]),
html.p([], [element.text("Welcome to my blog.")]),
]),
])
}
If a page doesn’t need blog post data, ignore the argument with _posts.
Using post data
The post list enables dynamic content on static pages. Common patterns:
Homepage with recent posts
import gleam/list
import gleam/time/timestamp
fn home_view(posts: List(Post(Nil))) -> Element(Nil) {
let recent =
posts
|> list.sort(fn(a, b) { timestamp.compare(b.date, a.date) })
|> list.take(5)
html.html([], [
html.head([], [html.title([], "Home")]),
html.body([], [
html.h1([], [element.text("Latest posts")]),
html.ul(
[],
list.map(recent, fn(p) {
html.li([], [
html.a([attribute.href(p.url)], [element.text(p.title)]),
])
}),
),
]),
])
}
Archive page grouped by year
import gleam/dict
import gleam/int
import gleam/list
import gleam/time/calendar
import gleam/time/timestamp
fn archive_view(posts: List(Post(Nil))) -> Element(Nil) {
// Group posts by year
let by_year =
list.group(posts, fn(p) {
let date = timestamp.to_calendar(p.date, calendar.utc_offset)
date.date.year
})
let years =
by_year
|> dict.to_list()
|> list.sort(fn(a, b) { int.compare(b.0, a.0) })
html.html([], [
html.head([], [html.title([], "Archive")]),
html.body([], [
html.h1([], [element.text("Archive")]),
..list.map(years, fn(entry) {
let #(year, year_posts) = entry
html.section([], [
html.h2([], [element.text(int.to_string(year))]),
html.ul(
[],
list.map(year_posts, fn(p) {
html.li([], [
html.a([attribute.href(p.url)], [element.text(p.title)]),
])
}),
),
])
})
]),
])
}
Filtering by language
import gleam/list
import gleam/option.{None, Some}
fn english_home(posts: List(Post(Nil))) -> Element(Nil) {
let en_posts =
list.filter(posts, fn(p) {
p.language == None || p.language == Some("en")
})
// ... render en_posts
}
Blog post templates
Blog post templates control the full page layout wrapping each rendered blog post. Set a template via markdown.template():
import blogatto/config/markdown
import blogatto/post.{type Post}
import gleam/option
import lustre/attribute
import lustre/element.{type Element}
import lustre/element/html
let md =
markdown.default()
|> markdown.markdown_path("./blog")
|> markdown.template(post_template)
fn post_template(post: Post(Nil)) -> Element(Nil) {
let lang = option.unwrap(post.language, "en")
html.html([attribute.lang(lang)], [
html.head([], [
html.meta([attribute.charset("UTF-8")]),
html.title([], post.title),
html.meta([
attribute.name("description"),
attribute.content(post.description),
]),
]),
html.body([], [
html.nav([], [
html.a([attribute.href("/")], [element.text("Home")]),
]),
html.article([], [
html.h1([], [element.text(post.title)]),
html.div([], post.contents),
]),
]),
])
}
The template receives a fully parsed Post with rendered contents. When no template is set, Blogatto uses a minimal default that wraps the title and contents in a basic HTML page.