[Guide] Build a Portfolio Site - implementation notes
The Portfolio guide is a good reminder that "simple sites" still benefit from a routing contract: file routes for sections, component-router motion for tabs/modals/theme/print posture, and stored derived project snapshots so every panel renders from a stable shape. The defining pattern is "theme by side effect": derive theme tokens in render and store them (scope or doc) so the tree reads from a ready-to-use palette.
Did you implement theme-by-side-effect (derive tokens in render), and how did you keep it from feeling like hidden magic? What did you store as derived "project snapshots" (preview lines, tag counts, visible ids) vs compute locally? How did you model the project modal overlay so it stayed route-visible and shareable? For contact forms, did you keep DOM truth and state truth alive together (Field posture), or commit on submit only?
Comments (24)
Back to latestTheme tokens derived in render was surprisingly nice.
Once tokens are stored, all the components can stay dumb and just read the palette.
We rendered data-theme and data-print on the shell and it made "why does this look different" bugs easy.
A portfolio site has fewer bugs, but the ones you get are often posture bugs.
A minimal take on the guide's theme side effect, keeping it legible:
tsx
const ThemeScope = createScope({ theme: 'warm', tokens: { surface: '#fff', text: '#111' } });
function PortfolioShell({ children }: { children: React.ReactNode }) {
const route = useGlobalRoute('portfolio', { theme: 'warm', print: 'off', section: 'home', projectId: null });
const theme = useScope(ThemeScope);
useRenderEffect(() => {
theme.write('theme', route.read('theme'));
theme.write('tokens', deriveThemeTokens(route.read('theme'), route.read('print')));
return `theme:${route.read('theme')}:${route.read('print')}`;
});
return <section data-theme={theme.theme} data-print={route.read('print')}>{children}</section>;
}
Returning a string from the effect made it feel like documentation rather than a side channel.
Counterpoint: "theme by side effect" is easy to abuse.
If you start doing layout math or data writes in render effects, it can become hard to reason about.
Agree. We limited it to token derivation + evidence keys only.
If the effect can't be summarized as a one-line posture string, it's probably doing too much.
Project snapshots were worth storing because they power multiple surfaces (grid, modal preview, sharing card).
We stored projectSnapshotById and tagCounts and it eliminated mismatches across panels.
Modal overlay as route state is the best part of the guide.
We used projectId in global route state and rendered it as data-project so deep links were shareable.
We also used local route state for the Work section tab (workTab=projects|speaking).
It made back/forward behave like a user expects without creating new file routes.
For derived snapshots, we drew the line at "shared siblings": if both the Work grid and the modal need it, store it.
If only the modal needs it, compute it in the modal.
We kept a heroLines derived key because the home hero was used in both the on-page hero and the print layout.
Without storing it, the two versions drifted and it looked sloppy.
The print posture key (print=on|off) was a nice touch.
It forced us to decide which layout changes are posture vs which are just CSS.
Contact form: we used Field posture (DOM truth + state truth) and committed on blur.
It made typing feel normal while still letting us render validation evidence as derived keys.
Docs tie-in: this guide is a pretty direct example of "routing contracts" and "derived state" applied to a smaller app.
Related: Routing and Navigation and Performance and Rendering (Best Practices).
A misunderstanding I had: I assumed a portfolio doesn't need route state because it's "static".
But as soon as you add modals and tabs, you have motion. Route state just makes it honest.
Counterpoint: storing too many derived strings (snapshots, previews, formatted dates) can make content updates annoying.
We had to re-derive everything when content changed, which was fine but it's work.
We solved that by having one derivePortfolio() call that writes all derived keys from the source content object.
As long as there's a single derivation entry point, it's manageable.
We rendered a derived shareUrl for the current posture (section + projectId) and it made sharing easy.
It also prevented subtle drift where the URL didn't match the UI state.
Theme tokens were especially useful for a11y reviews.
Once tokens are stored, you can test contrast and surface rules consistently across the app.
The guide made me stop using local state for modals.
Route-visible modal state is just easier to debug and share.
If you're worried about "magic", return a posture string from your render effect like the guide does.
It reads like documentation and gives you something to snapshot in tests.
We used derived tagCounts for filtering and it made the Work section feel more polished.
If the counts are derived live, they can flicker under refresh and it looks broken.
Print posture is also nice for debugging because it forces you to name layout modes explicitly.
If you ever have to explain layout differences, having a key helps.
We also stored a derived projectModalTitleLine so the modal header never had to recompute formatting.
It sounds minor, but it prevented drift between the grid card title and the modal title.
A practical takeaway: route keys for motion, scope/doc keys for derived structures, and render evidence for posture.
Even for a portfolio, that makes the site feel intentional.
This guide is a good entry point for the repo patterns because the domain is simple but the motion is real.
If you do theme tokens, keep the token set small at first.
Once the palette contract is stable, you can expand it without turning it into a huge refactor.