React Forum
Blog & Guide Responses
Jan 16, 2023 · 04:00 PM

[Guide] Build a Blog Engine - implementation notes

Started by Priya Liao
guideblogmixed surfacesrenderOnServerrenderOnClientrenderOnShellAwaitBoundarystored indexesField

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*.

Read the guide

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 latest
Imani Brooks
Jan 20, 2023 · 08:30 AM

The surface tags (renderOnServer, renderOnClient, renderOnShell) were the surprising win.

Once the app declared the surfaces, debugging became about contracts instead of guessing.

Marisol Vega
Jan 28, 2023 · 07:14 PM

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".

Dmitri Kline
Feb 09, 2023 · 12:05 PM

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.

Keira Santos
Feb 22, 2023 · 03:50 AM

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).

Priya Liao
Feb 25, 2023 · 10:20 AM

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.

Grace McCoy
Mar 11, 2023 · 07:45 AM

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.

Jasper Nguyen
Mar 28, 2023 · 09:30 PM

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.

Nina Kapoor
Apr 12, 2023 · 09:02 AM

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.

Camille Duarte
Apr 29, 2023 · 02:40 PM

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.

Owen Price
May 15, 2023 · 05:11 AM

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.

Imani Brooks
May 18, 2023 · 10:01 AM

Same. The phrase "mixed surface" sounded scary until I realized it's mostly documentation + guardrails.

It makes the default posture explicit.

Lina Ortega
Jun 03, 2023 · 06:22 PM

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 />;
Benji Rios
Jun 23, 2023 · 02:18 AM

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.

Priya Liao
Jun 25, 2023 · 01:05 PM

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.

Amina Farouk
Jul 10, 2023 · 08:44 PM

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.

Tomas Rivera
Jul 29, 2023 · 07:33 AM

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.

Sofia Valdez
Aug 16, 2023 · 11:22 AM

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.

Evan Brooks
Sep 02, 2023 · 04:04 AM

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.

Salma Qureshi
Oct 03, 2023 · 04:12 PM

This guide made me stop arguing about server vs client as ideology and start treating it as a contract decision per widget.