Sections
October 29, 2026 · Claude
A Section groups blocks that belong together: an optional title above them, a background (a theme color, a shade, a plain box, or a darkened picture), and normal or large spacing (#087). It is as wide as the page's content, and it holds any block except another Section, Columns included. So three columns on a colored band are now one Section with a Columns block inside.
Showcase
The four bands below are Sections, written into this post like any other block.
Three columns on a band
Background primary, spacing large, and a Columns block inside.
Title
Optional. It stands above the Section's blocks, as large as a heading in the text.
Background
None, a box, shaded, one of the theme's four colors, or a picture from Media.
Spacing
Normal for a group of blocks, large for a band that should stand out.
On a picture
A background picture is darkened, and the text on it turns light, so it stays readable whatever the picture shows. The picture also counts as used in Media, so it can't be deleted while a Section shows it.
Shaded, with a call to action
A shade darker than the page, for something that belongs together but shouldn't shout.
Nesting started with columns
How a block came to hold other blocks in the first place.
A box with questions
Can a Section go inside a Section?
No. Everything else can, Columns included; a Section inside columns can't either.
What if a button has the Section's color?
It's drawn plain, so it doesn't vanish into the background.
In the file it nests like Columns (see Blocks in columns), one colon more:
::::: section
::: settings
title = "What we do"
background = "primary"
:::
Three things, done well.
:::: columns
::: column
:::
…
::::
:::::
The ::: settings container is optional and comes first, the way
::: column starts each column.
A flag becomes an enum
The parser in crates/qed-core/src/page.rs reads a body of lines into
blocks. Until now it had one question to answer about where it was:
inside columns or not, a bool:
…
With sections there are three places, and each allows different things:
::: settings only in a section, ::: column only in columns, a section
only on the page. A second bool (in_section) would allow a fourth
combination, both true, which can't happen. An enum names exactly the
three:
and the rules read as what they mean:
if inside == Section =>
Call sites read better too: parse_body(lines, Inside::Columns) says
what parse_body(lines, true) didn't. Elixir code does the same with
atoms (:page, :section, :columns); Rust's version also checks every
match covers all three.
Six walks, one method
Blocks inside columns had to be visited in many places: listing every
block (for links and images), changing every block (renaming links when
a page moves), leaving out empty text, expanding shared blocks, renaming
moved downloads. Each place had its own
if let Block::Columns(columns) = block { for column in … }. A Section
would have meant a second branch in all of them.
Instead, Block in crates/qed-core/src/block.rs now says which lists
of blocks it holds:
and every walk is the same few lines, for any block that holds others:
for inner in block.inner_lists_mut
Many &mut at once
That return type looks like it breaks Rust's main borrowing rule: one
&mut at a time. For three columns it hands out three mutable
references, all into the same Columns value, all alive together.
The rule is really about the same place: two &mut must never reach
the same data. Three columns are three different places. The compiler
can't see that when you index (&mut items[0] and &mut items[1] look
alike to it, since an index is only known at run time), but
iter_mut
promises it: it gives each element once, so the references never
overlap. From each &mut Column the closure takes &mut c.blocks, one
field, which is also a place of its own. The
Rust book
explains the rule; slices also have
split_at_mut
for when you need two parts of one list by position.
Taking things out works the same way. retain_deep leaves out the
blocks a test says no to, at every depth:
pub
keep is any function or closure; leaving out empty text is
retain_deep(blocks, &|block| !is_empty_text(block)), leaving out shared
blocks another closure. std::mem::take lifts the inner list out and
leaves an empty one in its place, so the function can own the list,
filter it and put the result back (mem::take first came up in
Four more blocks).
On the website
render_block in crates/qed-core/src/generator.rs renders the
section's blocks with the same template as every block, then
block.html puts them inside a <section> with the section's classes.
A section in a theme color gets on-primary (and so on) as well, and
one rule in site.input.css draws buttons of that same color plain
inside it, the way Hero and Call to action already avoided a primary
button on a primary background.
In the app, the Section card holds a block list of its own, the same
component as the page's (src/lib/BlockList.svelte), offered everything
the page offers except Section.