Now shipping: the Harbour block system.
Explore the blocksThe default variant: opening copy and calls to action, over a full-bleed textured band carrying a card that shows an agent mid-instruction.
Harbour is a starter marketing site. Your developers keep working in git; your team edits through the agent they already use.
Same Block, different variant, set through a literal prop rather than a new component.
Swap the tokens, write the copy in MDX, and let the build tell you when something is wrong.
Each Block lives inside its own <BlockSpec> wrapper in this file's raw MDX source, closed like this:
<BlockSpec name="CTA">
<CTA title="Example" />
</BlockSpec>
How it compares
A quick view of what changes when content and layout live in the same system.
What you get
Six pieces, each one doing a single job well.
Compose pages from a growing library of pre-built sections instead of hand-rolling layout for every client.
Wire pages to Markdown, Sanity, or a JSON file. The block components never know which one is behind them.
Rebrand a client site by editing a handful of CSS variables, not hunting hex codes through the codebase.
Every block validates its props at build time, so a bad edit fails the build, not the client demo.
Sections render on the server. A block only ships a bundle when it actually needs one.
Clone the repo, swap the tokens and copy, push. Most rebuilds go live the same day.
Real content
Preview a block and it already reads like a finished page. Swap the copy when the client is ready, not before.

What you get
Compose pages from a library of finished sections instead of hand-rolling a layout for every client.
Browse the blocksPages are plain-text MDX, so a change is diffable, reviewable as a pull request, revertible, and editable by an agent.
Read how it worksRebrand a site by editing a handful of CSS variables instead of hunting hex codes through components.
Open the style guideIn place
An agent's change arrives as a pull request with its own preview, so the client approves the page itself.
In practice

A five-page site for a three-person studio, live two days after the kickoff call.

Pricing, changelog, and docs pulled from three different content sources.

Same block tree, new tokens: the whole site retheme took an afternoon.
Wallpapers
A wide crop for full-bleed heroes, square crops for everything smaller.








Unlike every other Block on this page, the cards above are not props: eyebrow, title, and count
are the only literal values this section carries. The posts themselves come straight from
content/blog/, sorted newest first, the same way /blog itself does, so a card can never show a
stale title. Each card's picture is that post's own cover in its frontmatter; a post without one
gets the site's texture instead. Add category="Guides" to show only one category's posts.
Neither is required. Block components read from a shared content shape, and the shape ships with a Markdown adapter and a Sanity adapter already wired. Point the config at whichever one matches the client's comfort level. The sections themselves never know which backend is behind them.
Most agencies using this go from clone to a live client preview in an afternoon: swap the token file, drop in the client's copy and logo, pick block variants per section. The parts that used to take a week (layout, spacing, responsive behavior) are already decided.
You build it once, following the same props-and-schema pattern as every other block, and it slots into the registry alongside the built-ins. It is an addition to the template rather than a fork of it, so the next client project gets it for free too.
No. The registry is just a mapping from a type string to a component. Whatever's supplying that type (Sanity, a JSON file, a headless CMS you bolt on later), the registry doesn't care, as long as the props match the schema.
Yes. It ships MIT-licensed specifically so agencies can rebrand, resell, and reuse it across client engagements without asking permission or crediting the template anywhere client-facing.
Not yet natively: routing and copy are single-locale today. Multi-language support is on the roadmap, but if a client needs it now, the content layer is thin enough that most teams wire in next-intl themselves in under a day.
From the agencies
We reskinned two sites in a single afternoon and the client never noticed a freeze. Nothing broke.
Rhea SandovalPartnerThe block registry is the first thing I show new hires. They're shipping client sections in their first week, not their first month.
Casey OkaforFounderThe client edits their own copy now. I haven't been in a “can you change this word” thread since March.
Jordan EllisCreative DirectorA bad edit fails the build, not the demo. That's the whole pitch, and it holds.
Morgan ValeLead EngineerFrom the agencies
Nothing ships client JS unless a block actually needs it. Our Lighthouse scores stopped being a monthly fire drill.
Morgan ValeLead Engineer, Attic DigitalPowering marketing teams at
The team
Six people across design, engineering and operations.
Design Lead
Engineering Lead
Product Manager
Front-End Engineer
Design Systems
Operations
By the numbers
/ 8 HR DAY
Newsletter
One email a month, unsubscribe anytime.
Pricing
Start free, upgrade the day you take on a second client.
Free
For a single portfolio or personal project.
$249one-time
For agencies shipping client sites every month.
Custom
For teams running a full client roster.
src/components/ui/button.tsx · 6 variants × 9 sizes
| variant \ size | marketing | default | lg | sm | xs | icon | icon-lg | icon-sm | icon-xs |
|---|---|---|---|---|---|---|---|---|---|
| default | |||||||||
| outline | |||||||||
| secondary | |||||||||
| ghost | |||||||||
| destructive | |||||||||
| link |
Geist Sans (--font-sans) · Geist Mono (--font-mono) · 10 roles
src/components/blocks/section-header.tsx · eyebrow, meta, split
eyebrow + meta
By the numbers
One supporting sentence, never two paragraphs.
SectionHeaderSplit: lede opposite the title
Pricing
Start free, upgrade the day you take on a second client.
src/components/texture/grid-field.tsx · server SVG + canvas · 2 presets
dense: animated, pointer={true}; move the cursor over it
coarse: animate={false}, ships no client JS