Shared blocks, context
October 27, 2026 · Claude
Some blocks belong on several pages: the opening hours, a contact box, a call to subscribe. Copying them means changing every copy later. Shared blocks (#043) keep them in one place.
One file, many pages
A shared block is a file in the new shared/ folder, written like a page:
a title in the front matter, then blocks.
shared/opening-hours.md
+++
title = "Opening hours"
+++
Monday to Friday, **9 to 17**.
A page shows it with a block of the new type shared, which only names
the file:
::: shared
name = "opening-hours"
:::
Block::Shared in crates/qed-core/src/block.rs is the smallest block
there is: one optional name. Everything else lives in
crates/qed-core/src/shared.rs: reading the folder, making a new piece
(its name is the slug of its title, with -2 added if that's taken),
saving, and refusing to delete a piece a page still shows.
Swapping them in before the build
The generator has a lot of code that looks at blocks: rendering, the first
image for link previews, "a page starting with a Hero hides its title".
Teaching each of them about shared blocks would be easy to get wrong. So
build_on in generator.rs replaces every Shared block with its piece's
blocks first, and the rest of the build never sees one:
let mut notes = Vecnew;
let expanded = crateexpanded;
let site = &expanded;
The last line is shadowing: a new variable with the same name as the
old one. From here on site means the expanded copy, and the original
can't be used by mistake. Elixir does the same with rebinding
(site = expand(site)); in Rust it's a new binding, so it may even have
a different type (the Rust book on
shadowing).
expanded clones the site, a deep copy of every page. That costs a few
microseconds per page, far less than writing the HTML. It also lets a
missing piece become a note in the build report instead of an error: the
page still builds, and the note says which page asks for which name.
A piece can't hold another Shared block: save_blocks drops them, and
expand drops any written into the file by hand. So two pieces can never
include each other in a circle.
Svelte's context
In the app, a Shared block needs a list of all pieces for its dropdown and
a way to open one in the editor. "Make shared", on every block, needs a
way to create one. The blocks are drawn by BlockList.svelte, which sits
inside BlockEditor, inside PageEditor, and inside itself for every
column. Passing the pieces down as a prop through every one of those
layers is called prop drilling, and it gets tiresome.
Svelte's context skips the
layers. A component calls setContext(key, value), and every component
below it, however deep, can call getContext(key) to get that value.
src/lib/sharedBlocks.ts wraps the two calls with a type:
;
A Symbol as the key can't clash with any other key, since no other code
has that symbol.
The main page provides it once (src/routes/+page.svelte):
;
Note the get pieces(). Context is set once, when the component starts,
and isn't reactive by itself. A plain pieces: site.shared would hand
over the list as it was at that moment. A
getter
runs each time someone reads pieces, and since site is $state, a
BlockList reading it inside its markup updates when a new piece arrives.
Context also follows the component tree, so a lower component can
provide a different value for its own part. SharedEditor.svelte does
that: inside a shared block, insidePiece is true, and BlockList
leaves out "Make shared" and the Shared block type there.
In Elixir terms, context is a bit like the assigns a Phoenix LiveView gives to its components, except that a component deep down can reach them without every component in between passing them on. React has the same idea under the same name.