Blocks vs Components: What Survives a Rebrand
The core architectural split in Zorvel — disposable blocks read i18n and wire content, durable components render anything you feed them.
This is a development note about the code behind Zorvel’s AI effect concept gallery. Its page structure follows one rule:
A file that reads translations is a block. A file that takes all content via props is a component.
Blocks are disposable
Blocks live in src/blocks/ and are zero-config page sections — <Hero />, <Pricing />, <Footer />. Each one reads i18n messages, builds a content config, and passes it to a component. They assemble the gallery’s content; changes to copy or section order usually start here.
// src/blocks/header.tsx — a block: reads i18n, wires a component
import { m } from '@/paraglide/messages.js';
export function Header() {
const navLinks = [{ href: '/explore', label: m['gallery.browse']() }];
return <SiteHeader navLinks={navLinks} />;
}
Components are durable
Components live in src/components/ and never read translations. SiteHeader, PricingTable, AppSidebar — all content arrives via props. They don't know your app's name, your copy, or your locale. That's exactly why they survive every rebrand.
Why the split matters
When you update the gallery’s presentation:
- Keep
src/components/*— the chassis. - Rewrite
src/blocks/*— the content wiring. - Rewrite the translation JSON files that feed the blocks.
The page files themselves stay tiny — src/routes/index.tsx is pure composition, a stack of blocks. Copy and composition changes usually touch blocks and JSON; components change when their presentation needs to. The split is not cosmetic; it's what makes customization a rewrite of intent instead of a fight with someone else's design decisions.