[Guide] Build a Blog Engine - implementation notes
The Blog Engine guide is the first one that really leans into "mixed surface, single story": server reads for index/content, client widgets for comments/reactions, shell rendering for stable chrome, and AwaitBoundary around streaming blocks so loading is consistent across the route tree. The part I'm most interested in is how people keep the surface boundaries legible so it's obvious *where* work runs and *why*.
Did you store derived indexes like visiblePostIds, tagCounts, and readingTimeById, or did you compute them on demand? How did you model overlays (search/share) so they stayed component-router motion without URL churn? Where did you draw the boundary between server-rendered post content and client-rendered widgets, and what evidence did you render to prove it? For comment composers, did the Field multi-truth posture make your submit behavior more predictable, or just add complexity?
Comments (16)
Back to latestThe surface tags (renderOnServer, renderOnClient, renderOnShell) were the surprising win.
Once the app declared the surfaces, debugging became about contracts instead of guessing.
We stored tagCounts and excerptById and it made the index view feel instant.
Computing excerpts in render was the first thing that made scrolling feel "sticky".
A minimal derive helper that kept indexes coherent for us:
ts
function deriveIndex(doc: any, posts: any[]) {
doc.write('posts', posts);
doc.write('visiblePostIds', posts.map((p) => p.id));
doc.write('excerptById', Object.fromEntries(posts.map((p) => [p.id, (p.excerpt ?? '').slice(0, 180)])));
doc.write('tagCounts', countTags(posts));
doc.write('readingTimeById', estimateReadingTime(posts));
}
Derive once, store, render from stored keys. It made the index a stable contract.
Counterpoint: the surface annotations can become "theming" if you slap them everywhere without discipline.
We had to agree on what each surface means (server = content, client = interactive, shell = chrome).
Yes. The guide reads like it's trying to prevent "everything is client" by forcing explicit declarations.
Once you can name the surfaces, you can also measure and test them.
Overlays were easiest when they were route-visible but not path-visible.
We used a global route key like overlay=search|share|none and rendered it as data-overlay evidence.
The AwaitBoundary wrappers were worth it just for consistent loading states.
Once boundaries are consistent, you stop debugging "why is this panel blank" and start debugging real data issues.
We kept a small evidence banner in debug mode: surface=server widgets=client chrome=shell.
It looked silly but it made it obvious when someone accidentally moved a widget into the wrong surface.
Field multi-truth in the comment composer helped us avoid "typed text disappears" bugs when the route refreshed.
The composer kept DOM truth alive, and we only committed to state on submit posture.
A misunderstanding I had: I assumed server vs client meant two separate apps.
The guide uses it as a single story where surfaces just declare where work is allowed to happen.
Same. The phrase "mixed surface" sounded scary until I realized it's mostly documentation + guardrails.
It makes the default posture explicit.
Code snippet: keeping the overlay key in global route state made it easy to test and debug.
tsx
const route = useGlobalRoute('blog', { overlay: 'none' as 'none' | 'search' | 'share' });
if (route.read('overlay') === 'share') return <ShareOverlay />;Counterpoint: storing derived reading time can be misleading if your content changes frequently.
We stored it anyway but also stored a derivedAt timestamp so it was clear when it was last computed.
That seems like the right compromise: store for readiness, but keep evidence that it's a derived cache.
Otherwise people treat it like a live calculation.
Docs tie-in: this guide is basically the data-fetching posture plus a routing contract story.
Related: Data Fetching and Caching (Best Practices) and Routing and Navigation.
We used streaming blocks for reactions and it made the page feel alive without delaying the content.
But we had to make the streaming lane visible, otherwise it looked like the widget was flickering randomly.
We found it helpful to store visiblePostIds even when filters were simple.
Once multiple panels read the same list (index, sidebar, search overlay), storing prevented drift.
If you want to keep surfaces legible, keep a single table that lists what renders on each surface.
We treated it like an architecture doc and it prevented accidental migrations.
This guide made me stop arguing about server vs client as ideology and start treating it as a contract decision per widget.