Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Post components

Blogatto renders Markdown and Djot sources through the same set of Maud-style components — view functions that control how each AST element becomes HTML. You can override any component to add classes, attributes, or entirely custom markup.

The component setters live on blogatto/config/post and apply to every post regardless of source format. Markdown-specific AST nodes (e.g., GFM tables) and Djot-specific AST nodes (e.g., highlight {=...=}) reuse overlapping components (table, mark, …), so a single override applies to both formats.

How it works

The post source is parsed into an AST, then rendered bottom-up: children are rendered first, then passed to the parent component function as List(Element(msg)). When implementing a custom component, you must include the children in the element you return, otherwise they won’t appear in the output.

Djot-only inline constructs that have no matching component (e.g. <span> from [text]{.class}, <ins> from {+text+}, math, symbols) are emitted as raw Lustre elements with their attributes preserved.

Default components

post.default() uses the default Maud components, which render each markdown element as its corresponding HTML element without additional attributes or styling.

import blogatto/config/post

let md = post.default()
  |> post.path("./blog")

Overriding components

Each markdown element has a corresponding setter function on PostConfig. Override individual components by piping through the setter:

import blogatto/config/post
import lustre/attribute
import lustre/element/html

let md =
  post.default()
  |> post.path("./blog")
  |> post.h1(fn(id, children) {
    html.h1([attribute.id(id), attribute.class("post-title")], children)
  })
  |> post.p(fn(children) {
    html.p([attribute.class("post-paragraph")], children)
  })

Component reference

Text elements

SetterSignatureDescription
post.pfn(List(Element(msg))) -> Element(msg)Paragraphs
post.strongfn(List(Element(msg))) -> Element(msg)Bold text
post.emfn(List(Element(msg))) -> Element(msg)Italic text
post.delfn(List(Element(msg))) -> Element(msg)Strikethrough text
post.markfn(List(Element(msg))) -> Element(msg)Highlighted text

Headings

All heading setters take fn(String, List(Element(msg))) -> Element(msg) where the first argument is a generated heading ID (useful for anchor links).

SetterDescription
post.h1Level 1 heading
post.h2Level 2 heading
post.h3Level 3 heading
post.h4Level 4 heading
post.h5Level 5 heading
post.h6Level 6 heading

Example with anchor links:

post.h2(fn(id, children) {
  html.h2([attribute.id(id)], [
    html.a([attribute.href("#" <> id)], [element.text("#")]),
    element.text(" "),
    ..children
  ])
})
SetterSignatureDescription
post.afn(String, Option(String), List(Element(msg))) -> Element(msg)Links (href, optional title, children)
post.imgfn(String, String, Option(String)) -> Element(msg)Images (src, alt text, optional title)

Example — open external links in a new tab:

import gleam/option.{None, Some}
import gleam/string

post.a(fn(href, title, children) {
  let attrs = case string.starts_with(href, "http") {
    True -> [
      attribute.href(href),
      attribute.target("_blank"),
      attribute.attribute("rel", "noopener noreferrer"),
    ]
    False -> [attribute.href(href)]
  }
  let attrs = case title {
    Some(t) -> [attribute.title(t), ..attrs]
    None -> attrs
  }
  html.a(attrs, children)
})

Code

SetterSignatureDescription
post.codefn(Option(String), List(Element(msg))) -> Element(msg)Code spans and fenced blocks (optional language, children)
post.prefn(List(Element(msg))) -> Element(msg)Preformatted code block wrapper

The code component receives Some("gleam") for fenced code blocks with a language tag, or None for inline code.

Tip: Blogatto supports build-time syntax highlighting via Smalto, which automatically tokenizes code blocks and renders styled <span> elements. When syntax highlighting is enabled, the code component receives pre-highlighted children. See Syntax highlighting for the full guide.

Example — add language class to code blocks:

import gleam/option.{None, Some}

post.code(fn(lang, children) {
  let class = case lang {
    Some(l) -> "language-" <> l
    None -> ""
  }
  html.code([attribute.class(class)], children)
})

Lists

SetterSignatureDescription
post.ulfn(List(Element(msg))) -> Element(msg)Unordered lists
post.olfn(Option(Int), List(Element(msg))) -> Element(msg)Ordered lists (optional start number, children)
post.lifn(List(Element(msg))) -> Element(msg)List items
post.checkboxfn(Bool) -> Element(msg)Task list checkboxes (checked state)

Tables

SetterSignatureDescription
post.tablefn(List(Element(msg))) -> Element(msg)Table wrapper
post.theadfn(List(Element(msg))) -> Element(msg)Table header group
post.tbodyfn(List(Element(msg))) -> Element(msg)Table body group
post.trfn(List(Element(msg))) -> Element(msg)Table row
post.thfn(Alignment, List(Element(msg))) -> Element(msg)Header cell (alignment, children)
post.tdfn(Alignment, List(Element(msg))) -> Element(msg)Data cell (alignment, children)

The Alignment type has three variants: Left, Center, Right.

Example — add alignment classes to table cells:

import blogatto/config/post.{Left, Center, Right}

post.td(fn(alignment, children) {
  let class = case alignment {
    Left -> "text-left"
    Center -> "text-center"
    Right -> "text-right"
  }
  html.td([attribute.class(class)], children)
})

Other elements

SetterSignatureDescription
post.blockquotefn(List(Element(msg))) -> Element(msg)Block quotes
post.hrfn() -> Element(msg)Horizontal rules
post.footnotefn(Int, List(Element(msg))) -> Element(msg)Footnotes (number, children)

Replacing all components at once

Use post.components() to set a complete Components record:

let my_components = post.Components(
  a: my_link,
  blockquote: my_blockquote,
  // ... all 27 fields
)

let md =
  post.default()
  |> post.components(my_components)

In most cases, overriding individual components via the setter functions is more convenient.