Last Updated: 2026-05-01 Status: Draft (idea validation) Service: extension of
apps/blog-service+@vlozi/blogTarget SDK version: builds on@vlozi/blog@2.1.5+(the SDK that ships the.vlz-*class system +--vlz-*token contract) Related:blog-engine-vision.md,books-vision.md,hosted-blog-vision.md,integration-friction-vision.md
The goal: A vlozi customer should go from "I want a blog on my site" to a polished, live blog page in under five minutes, picking from a small catalog of opinionated presets — without ever opening a CSS file.
TL;DR
Today the SDK gives customers React building blocks. They still have to design and compose /blog and /blog/[slug] pages themselves. Most vlozi customers aren't designers — they stall on "what should the blog page look like?" before the first post ships.
Layouts are pre-built, opinionated, named blog presets. Customer picks one, layout decisions disappear, the blog ships.
Three rules that make this work and not become a Wordpress-themes graveyard:
- Cap the catalog at ~6 layouts forever. Each is a maintenance unit (mobile / dark mode / RTL / a11y). Treat new layouts as expensive.
- Customization is design tokens, not CSS overrides. Accent color, font pair, density. Not "the author bio block should be 8 px taller." The token system extends the existing
--vlz-*SDK CSS variables — it's not a parallel theming surface. - Two delivery surfaces from one layout source today: React component (for React/Next.js sites), embed widget (a "latest posts" snippet for homepages — not a full embedded blog). A third surface (hosted blog at
<tenant>.vlozi.app/blog+ custom-domain CNAME) is designed for inhosted-blog-vision.mdand ships post-MVP.
Verdict (my take): Strong fit. Lower risk than books, faster ROI. Ship the first two layouts at blog v1.0 launch (Editorial + Minimal), the embed widget at v1.2. The catalog grows slowly over months — never in batches.
Scope note (2026-05-01): the hosted-blog surface (
<tenant>.vlozi.app/blog+ custom-domain CNAME) is out of layouts MVP and tracked separately inhosted-blog-vision.md. It's a substantial infra build (Cloudflare for SaaS, edge worker, cert provisioning) and shouldn't gate layouts. Layouts ship for React/Next.js customers first; hosted ships when the layouts-on-react surface is proven and we have signal that the non-React audience is real.
1. Purpose (The Why)
1.1 The current friction
A vlozi customer wants a blog on their site today. The path is:
- Install
@vlozi/blog✓ (5 min) - Read the integration guide ✓ (10 min)
- Create
/blogpage in Next.js — decide on a layout. - Create
/blog/[slug]page — decide on a layout. - Style both to match brand — make a hundred small CSS decisions.
- Add nav, footer, dark mode, mobile breakpoints — ship it.
- Realize the layout looks dated, redo it three months later.
Steps 3–6 are where most customers stall. Layout is a decision tax founders aren't qualified to pay. They want "make the blog look good, I'll pick from 5 options."
1.2 The "no one else can do this" angle
Headless CMSes (Sanity, Contentful, Strapi) ship raw APIs and let customers DIY everything. That's correct for enterprise teams with designers; wrong for the founder/creator audience vlozi targets.
Substack ships one opinionated layout, hosted, and that constraint is why it works for non-technical writers. But Substack is locked to its own domain and aesthetic.
Vlozi sits in the gap: Substack-grade preset polish, but on the customer's own domain, with multiple layouts and the rest of the platform underneath.
2. Target Audience (The Who)
| Persona | What they have | What layouts give them |
|---|---|---|
| Indie founder with a Next.js site | An empty /blog route and decision paralysis |
One prop: layout="editorial". Done. |
| Creator on Webflow / Framer / Carrd | A site with no React, can't use the SDK | Out of layouts MVP — covered by the future hosted-blog surface (hosted-blog-vision.md) |
| Pre-launch founder with no website yet | Just a brand and a Notion doc | Out of layouts MVP — covered by the future hosted-blog surface |
| Agency managing 10 client blogs | Per-client custom Next.js builds = unsustainable | Same SDK, different layout= per client, instant brand variation |
| Technical founder who does want full control | Will compose primitives anyway | Layouts are optional. They use the granular components like today. |
2.5 What the customer experiences (storyboard)
The whole product, end-to-end, is five screens. This is what we're building.
Step 1 — Sign up
┌──────────────────────────────────────────────┐
│ Vlozi → Get started │
├──────────────────────────────────────────────┤
│ │
│ Welcome — let's set up your blog. │
│ │
│ Workspace name: [my-startup____________] │
│ Logo (optional): [📁 Choose file ] │
│ │
│ [ Continue → ] │
└──────────────────────────────────────────────┘If a logo is uploaded, the dominant non-white color is extracted server-side and pre-fills the next step's accent token.
Step 2 — Pick a layout
┌──────────────────────────────────────────────────────────────┐
│ Pick a layout — you can change it any time │
├──────────────────────────────────────────────────────────────┤
│ │
│ ┌───────────┐ ┌───────────┐ ┌───────────┐ ┌───────────┐ │
│ │ │ │ │ │ │ │ 01 ▮▮▮ │ │
│ │ [Hero │ │ May 1 │ │ [img][img]│ │ TITLE │ │
│ │ image] │ │ Title │ │ Title │ │ → │ │
│ │ ─Title─ │ │ excerpt │ │ excerpt │ │ │ │
│ │ excerpt │ │ │ │ │ │ 02 ▮▮▮ │ │
│ └───────────┘ └───────────┘ └───────────┘ └───────────┘ │
│ │
│ ● Editorial ○ Minimal ○ Magazine ○ Brutalist │
│ │
│ Best for thought Stripe / Linear Image-heavy │
│ pieces, long-form vibes, no images grid blogs │
│ │
│ [ Continue → ] │
└──────────────────────────────────────────────────────────────┘Live screenshot of each layout (not ASCII in the real product), with one-line "best for" descriptions. Default selection is Editorial.
Step 3 — Brand it (~30 seconds)
┌──────────────────────────────────────────────┐
│ Make it yours │
├──────────────────────────────────────────────┤
│ │
│ Accent color: [ #0066ff ] ▣ │
│ ↳ Auto-detected from your logo │
│ │
│ Heading font: [ Cormorant Garamond ▾ ] │
│ Body font: [ Source Serif Pro ▾ ] │
│ │
│ Density: ○ Compact ● Comfortable │
│ ○ Roomy │
│ │
│ Dark mode: ● Auto ○ Light ○ Dark │
│ │
│ ─── Live preview ─── │
│ ┌────────────────────────────────────────┐ │
│ │ [Preview of the customer's blog with │ │
│ │ their picked layout + live token edit]│ │
│ └────────────────────────────────────────┘ │
│ │
│ [ Continue → ] │
└──────────────────────────────────────────────┘WCAG-AA contrast check runs on every accent change (see §5.3). If the preview will fail contrast, the picker shows a yellow inline warning — never blocks.
Step 4 — Connect your site
┌──────────────────────────────────────────────┐
│ Where will your blog live? │
├──────────────────────────────────────────────┤
│ │
│ ● I have a Next.js / React site │
│ ○ Astro / Remix / other React framework │
│ ○ I'll set it up later │
│ │
│ ─── Run this in your project root ─── │
│ │
│ $ npx @vlozi/blog@latest setup │
│ │
│ ↳ Sets up routes, env vars, image │
│ domains, and the layout you picked. │
│ Takes about 30 seconds. │
│ │
│ [ Done ] │
└──────────────────────────────────────────────┘CLI is what runs the actual integration. If they pick "later," dashboard shows the same npx command in Settings → Blog → Connect site whenever they're ready.
Step 5 — Write the first post
┌──────────────────────────────────────────────┐
│ Welcome to your blog │
├──────────────────────────────────────────────┤
│ │
│ Your blog is live at: │
│ ▸ https://yoursite.com/blog │
│ │
│ ─── Get started ─── │
│ │
│ ┌────────────────────────────────────────┐ │
│ │ ✍ Write your first post │ │
│ │ Start with a blank page │ │
│ └────────────────────────────────────────┘ │
│ │
│ ┌────────────────────────────────────────┐ │
│ │ ✨ Use a starter template │ │
│ │ "Hello world" intro post │ │
│ └────────────────────────────────────────┘ │
│ │
│ ┌────────────────────────────────────────┐ │
│ │ 🤖 AI-assisted draft │ │
│ │ Tell us your topic, get a draft │ │
│ └────────────────────────────────────────┘ │
│ │
└──────────────────────────────────────────────┘Three options to clear the blank-page paralysis (see integration-friction-vision.md Idea #7).
Total time from signup to live blog with first post: < 5 minutes. That's the bar. Layouts MVP closes Steps 1–4; Step 5 is the friction-reduction roadmap.
3. The Two Delivery Surfaces (MVP)
┌────────────────────────────────────────────────────────┐
│ Vlozi Blog Layouts (MVP) │
├──────────────────────────┬─────────────────────────────┤
│ ⚛️ React │ 📎 Embed │
│ │ │
│ For React / │ For homepage / │
│ Next.js sites │ marketing-site widgets │
├──────────────────────────┼─────────────────────────────┤
│ <VloziBlogSite │ <script> + <div> │
│ layout="..." /> │ "Latest 3 posts" │
│ │ "Featured post" │
│ Drop-in route │ Server-rendered HTML │
│ for /blog/* │ in Shadow DOM, hydrated │
└──────────────────────────┴─────────────────────────────┘Future third surface — Hosted Blog (<tenant>.vlozi.app/blog + custom-domain CNAME): tracked in hosted-blog-vision.md. Ships post-MVP when the React/Embed layouts have validated and the non-React audience is signaling real demand. The layout source is designed so it can render through a third surface later without change.
Same layout source, two rendering paths today. Each layout is authored once and rendered in both modes. That's the constraint that keeps the system maintainable — and the constraint that makes adding the hosted surface later cheap.
4. The Layout Catalog
The catalog is named and opinionated. Each layout has a personality and a target audience. The customer picks the one whose vibe matches their brand — they don't customize their way to it.
4.0 Visual catalog at a glance
┌────────────── EDITORIAL ──────────────┐ ┌────────────── MINIMAL ────────────────┐
│ │ │ │
│ ┌─────────────────────────┐ │ │ Latest from our team │
│ │ [hero image] │ │ │ Building tools that respect time. │
│ └─────────────────────────┘ │ │ │
│ │ │ ───────────────────────────── │
│ ─── CATEGORY · 8 MIN READ ─── │ │ May 1 · Engineering │
│ │ │ Building a faster blog SDK │
│ Why we rebuilt the │ │ │
│ editorial experience │ │ ───────────────────────────── │
│ │ │ Apr 28 · Design │
│ A serif lede paragraph that pulls │ │ Six layouts forever │
│ the reader in... │ │ │
│ │ │ ───────────────────────────── │
└───────────────────────────────────────┘ └───────────────────────────────────────┘
thought pieces, long-form, B2B SaaS, dev tools, utilitarian voice
┌────────────── MAGAZINE ───────────────┐ ┌──────────────── BRUTALIST ────────────┐
│ │ │ │
│ ┌───────────────────────────────┐ │ │ ┌─────────────────────────────┐ │
│ │ [ img ][ img ][ img ] │ │ │ │ 01 ENGINEERING — MAY 1 │ │
│ │ ● ○ ○ │ │ │ │ │ │
│ └───────────────────────────────┘ │ │ │ WHY WE REBUILT THE │ │
│ │ │ │ EDITORIAL EXPERIENCE │ │
│ ┌──────┐ ┌──────┐ ┌──────┐ │ │ │ → │ │
│ │[img] │ │[img] │ │[img] │ │ │ └─────────────────────────────┘ │
│ │Title │ │Title │ │Title │ │ │ │
│ └──────┘ └──────┘ └──────┘ │ │ ┌─────────────────────────────┐ │
│ │ │ │ 02 DESIGN — APR 28 │ │
│ ┌──────┐ ┌──────┐ ┌──────┐ │ │ │ SIX LAYOUTS FOREVER │ │
│ │[img] │ │[img] │ │[img] │ │ │ │ → │ │
│ └──────┘ └──────┘ └──────┘ │ │ └─────────────────────────────┘ │
│ │ │ │
└───────────────────────────────────────┘ └───────────────────────────────────────┘
lifestyle, food, travel, visuals technical content, indie, distinctivePick the one whose feeling matches your brand. Then customize colors and fonts on top. You don't compose layouts — you pick them.
The four MVP layouts are visually distinct on purpose: a customer should be able to tell them apart from the gallery alone. If two of them feel too similar in user testing (see §13 risks), one gets dropped.
4.1 Initial catalog (4 layouts)
Editorial — the default
A magazine-grade reading layout. Serif body type, large hero image, single-column post body, generous whitespace. Looks like The Atlantic or NYT Opinion.
INDEX PAGE POST PAGE
┌─────────────────────────────┐ ┌─────────────────────────────┐
│ THE BLOG ☰ │ │ ← Back to blog │
├─────────────────────────────┤ ├─────────────────────────────┤
│ │ │ │
│ Featured Story │ │ ┌───────────────────────┐ │
│ ┌───────────────────────┐ │ │ │ [Hero image — full │ │
│ │ [Hero — full bleed] │ │ │ │ bleed] │ │
│ └───────────────────────┘ │ │ └───────────────────────┘ │
│ │ │ │
│ ─── CATEGORY · 8 MIN ─── │ │ ─── CATEGORY · 5 MIN ─── │
│ │ │ │
│ Why we're rebuilding │ │ Why we rebuilt the │
│ the editorial experience │ │ editorial experience │
│ │ │ │
│ A serif lede that pulls │ │ By Dipanshu · May 1, 2026 │
│ the reader in... │ │ ────────────────────── │
│ │ │ │
│ ─── LATEST ─── │ │ A long-form serif body │
│ ┌──────────┐ ┌──────────┐ │ │ that flows through the │
│ │ [image] │ │ [image] │ │ │ column with classic │
│ │ Article │ │ Article │ │ │ editorial line-height... │
│ │ excerpt │ │ excerpt │ │ │ │
│ └──────────┘ └──────────┘ │ │ > "An indented serif │
│ │ │ > quote stands apart" │
└─────────────────────────────┘ └─────────────────────────────┘- Best for: writers, founders publishing thought pieces, B2B blogs
- Body: serif (Source Serif Pro / equivalent), 18 px, 1.7 line-height
- List page: hero card on top + alternating large/medium cards below
- Hero element: featured image bleeds edge-to-edge
Minimal — clean, modern, defaults-friendly
Sans-serif, dense but not cramped, no images required. Looks like the Stripe or Linear blog.
INDEX PAGE POST PAGE
┌─────────────────────────────┐ ┌─────────────────────────────┐
│ Blog [search] │ │ ← Blog │
├─────────────────────────────┤ ├─────────────────────────────┤
│ │ │ │
│ Latest from our team │ │ Engineering · May 1 │
│ Building tools that │ │ │
│ respect your time. │ │ Building a faster │
│ │ │ blog SDK │
│ ────────────────────── │ │ │
│ May 1 · Engineering │ │ How we got the React │
│ Building a faster │ │ entry from 38 KB → 35 KB │
│ blog SDK │ │ by removing Tailwind. │
│ │ │ │
│ ────────────────────── │ │ ───────── │
│ Apr 28 · Design │ │ │
│ Six layouts forever │ │ Body copy in clean Inter, │
│ │ │ 16 px, tight 1.6 line- │
│ ────────────────────── │ │ height. No serifs, no │
│ Apr 21 · Product │ │ decoration, just signal. │
│ Hosted blogs vs custom │ │ │
│ domains │ │ ## Section heading │
│ │ │ │
│ ────────────────────── │ │ More content... │
│ │ │ │
└─────────────────────────────┘ └─────────────────────────────┘- Best for: SaaS, dev tools, products with utilitarian voice
- Body: Inter / system sans, 16 px, 1.6 line-height
- List page: vertical list of titles + dates + 1-line excerpts (no images required)
- Hero element: latest post title, prominent
Magazine — image-heavy, browse-y
Multi-column grid, image-first cards, dense. Looks like Medium or The Verge.
INDEX PAGE POST PAGE
┌─────────────────────────────┐ ┌─────────────────────────────┐
│ THE MAGAZINE │ │ ← Magazine │
├─────────────────────────────┤ ├─────────────────────────────┤
│ │ │ │
│ ┌─────────────────────┐ │ │ ┌───────────────────────┐ │
│ │ [img1][img2][img3] │ │ │ │ [Featured image — 2:1]│ │
│ │ ● ○ ○ │ │ │ └───────────────────────┘ │
│ └─────────────────────┘ │ │ │
│ │ │ FOOD · 6 MIN │
│ ┌────┐ ┌────┐ ┌────┐ │ │ │
│ │[img]│ │[img]│ │[img]│ │ │ Sourdough on a Tuesday │
│ │Title│ │Title│ │Title│ │ │ │
│ │exc. │ │exc. │ │exc. │ │ │ ┌─────────┐ │
│ └────┘ └────┘ └────┘ │ │ │[avatar] │ Author │
│ │ │ └─────────┘ May 1 │
│ ┌────┐ ┌────┐ ┌────┐ │ │ │
│ │[img]│ │[img]│ │[img]│ │ │ Sans-serif body copy in │
│ │Title│ │Title│ │Title│ │ │ a wider column. Plenty │
│ │exc. │ │exc. │ │exc. │ │ │ of inline images. Pull- │
│ └────┘ └────┘ └────┘ │ │ quotes break up rhythm. │
│ │ │ │
└─────────────────────────────┘ └─────────────────────────────┘- Best for: lifestyle, food, travel, design — anything with strong visuals
- Body: serif headlines + sans body
- List page: 3-column responsive grid, every card has an image
- Hero element: featured carousel of 3 latest posts
Brutalist — the vlozi house style
Hairline 1 px borders, large background numerals (01, 02, 03 rendered as semi-transparent typography behind each post card), monospace tracking labels. Matches the seller-dashboard's own design language. Stands out hard.
INDEX PAGE POST PAGE
┌─────────────────────────────┐ ┌─────────────────────────────┐
│ THE BLOG ▮▮▮ │ │ ← BLOG │
├─────────────────────────────┤ ├─────────────────────────────┤
│ │ │ │
│ ┌───────────────────────┐ │ │ ▮▮▮ 03 │
│ │ 01 ENG — MAY 1 │ │ │ │
│ │ │ │ │ ENGINEERING · MAY 01 │
│ │ WHY WE REBUILT THE │ │ │ │
│ │ EDITORIAL EXPERIENCE │ │ │ WHY WE REBUILT THE │
│ │ │ │ │ EDITORIAL EXPERIENCE │
│ │ → │ │ │ │
│ └───────────────────────┘ │ │ ▮ DIPANSHU · 5 MIN READ │
│ │ │ ───────────────────────── │
│ ┌───────────────────────┐ │ │ │
│ │ 02 DESIGN — APR 28 │ │ │ Body copy in a tabular, │
│ │ │ │ │ monospace-labeled prose │
│ │ SIX LAYOUTS FOREVER │ │ │ block. Hairline rules │
│ │ │ │ │ between sections. │
│ │ → │ │ │ │
│ └───────────────────────┘ │ │ ▮ NEXT ───────────────── │
│ │ │ │
└─────────────────────────────┘ └─────────────────────────────┘- Best for: technical content, indie products, anyone who wants to look like they not every other blog
- Body: sans, 16 px, with monospace section labels and tabular numerals
- List page: numbered grid (
01,02, …) with hairline borders, oversized post-number sitting behind each card title - Hero element: oversized title block with the post number prominent
4.2 Future catalog (v1.6+, max 2 more — forever)
Notion-doc — the structured-content blog
Sans-serif, toggle-able sections, looks like a Notion page. Great for changelog-style or docs-adjacent blogs.
Mono — the developer terminal aesthetic
All monospace, dark-by-default, terminal vibes. Niche but distinctive.
That's it. Six layouts forever. The temptation to add a "Magazine but two columns" or "Editorial but with a sidebar" must be resisted — those become 12 layouts within a year and the catalog falls apart.
4.3 What a layout includes
Each layout ships with all of the following surfaces:
| Surface | Description |
|---|---|
Index page (/blog) |
List/grid of posts with that layout's personality |
Post page (/blog/[slug]) |
Single post reader with that layout's typography and decoration |
Category page (/blog/category/[slug]) |
Filtered list, same chrome as index |
Tag page (/blog/tag/[slug]) |
Same |
Series page (/blog/series/[slug]) |
Series index (once series ships) |
Search results (/blog/search?q=...) |
Search UI in the same chrome |
| 404 / empty states | "No posts yet," "no results" — designed, not generic |
| OpenGraph image template | Layout-matched social-share card, generated per post via next/og (see 4.5) |
| RSS feed | One canonical feed per blog — XML structure is the same across layouts (no styling), but <link> and atom-icon fields are correct for the active layout |
A layout is a complete reading experience, not a single component.
4.4 How body content interacts with a layout
The post body — what <BlogContent> renders inside <div class="vlz-content"> — is shared across all layouts. Layouts theme chrome (header, list, nav, banners, OG image) but inherit the body's structural CSS from the SDK. What a layout can change about the body:
fontBodytoken — forwards to.vlz-content { font-family: ...; }. So a Magazine post's body gets the layout's serif while keeping the same heading structure, paragraph rhythm, and rich-content (mermaid/carousel/code-block) styles as Editorial.densitytoken — forwards to padding/line-height variables on.vlz-content(see 5.4).- Max-width and column rhythm — the layout owns the wrapping
<article>and decides "single 720 px column" vs "wider with aside."
What a layout cannot change about the body:
- The hydrated rich-content blocks (carousel, mermaid, YouTube embed wrappers). These are SDK-owned and identical across layouts.
- The HTML sanitization or the
data-vlz-syntaxsyntax-highlighting palette — those are SDK-owned for security/consistency.
This rule keeps prose CSS from forking per layout and keeps the SDK's hydration contract intact. Magazine and Editorial differ in chrome and the column they wrap the body in — not in how the body itself renders.
4.5 OpenGraph image templates
Each layout ships an og.tsx that produces a layout-matched 1200×630 social card via next/og (ImageResponse). The runtime call passes { layout, post, tokens }; the layout's og.tsx returns the React tree.
| Layout | OG style |
|---|---|
| Editorial | Full-bleed featured image + serif title overlay (bottom-aligned) + small category chip |
| Minimal | Type-only — large heading, smaller dek, hairline + accent dot |
| Magazine | 3-up image grid (latest + this post + one related), bold title at the bottom |
| Brutalist | Hairline-bordered card, oversized post number watermark, monospace category label |
The OG endpoint lives at /blog/og?slug=… (React SDK) and is cached for 24 h with on-publish purge. The hosted-blog surface adds its own equivalent route when it ships. Massive social-share differentiation for ~1 day of work per layout.
5. Customization Model — Design Tokens, Not Overrides
This is the part most "themes" get wrong. The escape valve is too wide; everything becomes overridable; the layout becomes unmaintainable.
Customization is restricted to a fixed set of design tokens — and those tokens extend the existing SDK --vlz-* token contract rather than replacing it.
5.1 Mapping to the existing SDK token system
@vlozi/blog@2.1.5 already ships a token system documented in the SDK README — five CSS custom properties layered over currentColor:
--vlz-* knob |
Purpose |
|---|---|
--vlz-accent |
Link color, syntax keywords, blockquote rule, task checkbox |
--vlz-muted-fg |
Figcaptions, list markers, syntax comments |
--vlz-border |
Tables, <hr>, <details>, card borders |
--vlz-surface |
Code blocks, table headers, <summary>, card surfaces |
--vlz-surface-hover |
Hover state on the above |
Every .vlz-* component class derives its color from one of these. Layout tokens compile down to these variables, not parallel CSS. The mapping:
<VloziBlog
layout="editorial"
tokens={{
// Direct passthrough to existing --vlz-* knobs
accent: "#0066ff", // → --vlz-accent
surface: "rgba(0,0,0,0.04)", // → --vlz-surface
surfaceHover: "rgba(0,0,0,0.07)", // → --vlz-surface-hover
border: "rgba(0,0,0,0.18)", // → --vlz-border
mutedFg: "rgba(0,0,0,0.55)", // → --vlz-muted-fg
// New layout-only tokens
fontHeading: "Cormorant Garamond", // pick from a curated list
fontBody: "Source Serif Pro", // pick from a curated list
density: "comfortable", // see 5.4
radius: "none", // see 5.5
darkMode: "auto", // see 5.6
// Reuses existing provider config — same surface as <VloziProvider config={...}>
mermaidTheme: "auto", // forwards to VloziProvider
syntaxHighlighting: true, // forwards to VloziProvider
}}
/>The SDK's <VloziProvider> is mounted internally by <VloziBlog> with the right config, so consumers don't double-wrap.
5.2 The escape valve — and why it's narrow on purpose
No CSS overrides. No className passthrough on the layout root. No "render prop for the post card." If a customer needs more, they should:
- Pick a different layout.
- Or use the granular SDK components (
<BlogList>,<BlogPost>, hooks) and compose their own pages — those still acceptclassNameand full per-component slots like today.
This is a feature, not a limitation: the layout designer gets to make and own every layout decision below the token level. That's how the layout stays polished forever instead of decaying as customers chip pieces off.
5.3 Token validation
The dashboard runs validation on every token change:
- Color contrast.
accenton the layout'ssurfaceand on the body text color must pass WCAG-AA (4.5:1 for body, 3:1 for large text). If it fails, the dashboard shows an inline warning ("This accent color won't pass contrast on Editorial — pick a darker shade or a different layout"). The token is still saved — we don't block — but the warning is loud and the per-post preview shows the failure highlighted. Mismatched colors are a bigger product-quality risk than mismatched fonts. - Curated font list. Tokens accept any font name, but the dashboard UI shows only fonts vlozi has tested with each layout. If a customer types in a custom font, we render with it but flag a warning: "untested with this layout."
- Token shape. Unknown tokens are dropped with a console warning (in dev) and silently in production — never throw. The token contract is forward-compatible: adding new tokens never breaks old layouts.
5.4 density spec (applies to all layouts identically)
density |
List-card padding | Body font / line-height | Section spacing |
|---|---|---|---|
compact |
12 px | 15 px / 1.55 | 1.25 rem |
comfortable (default) |
20 px | 17 px / 1.65 | 1.75 rem |
roomy |
32 px | 18 px / 1.75 | 2.5 rem |
These map to --vlz-density-pad, --vlz-density-text, --vlz-density-leading, --vlz-density-gap set by <VloziBlog>. Layouts may not reinterpret these tokens — that's how they stay portable.
5.5 radius spec
radius |
Cards | Buttons / inputs | Images / videos in body |
|---|---|---|---|
none |
0 | 0 | 8 px (always — square photos look broken) |
sm |
4 px | 4 px | 8 px |
md (default) |
12 px | 6 px | 8 px |
Tags / pills are always pill-shaped (9999px) regardless of the radius token — that's a stylistic anchor of every layout.
5.6 darkMode spec
darkMode mirrors the existing mermaidTheme surface so consumers configure a single theme model:
| Value | Behavior |
|---|---|
"auto" (default) |
Reads prefers-color-scheme: dark via MediaQueryList. Re-renders when the user toggles their OS theme mid-session. |
"light" |
Force light, regardless of OS preference. |
"dark" |
Force dark. |
() => "light" | "dark" |
Function form for next-themes integration: darkMode: () => resolvedTheme === "dark" ? "dark" : "light" |
When darkMode resolves to "dark", the layout sets --vlz-* to dark-mode defaults (e.g. --vlz-surface: rgba(255,255,255,0.06)) before consumer overrides apply. The SDK's mermaid block + syntax highlighter follow automatically because they read the same vars.
5.7 Tokens in action — visual examples
Same layout (Editorial), different token bags. Every difference comes from the five --vlz-* knobs and the layout-specific font/density/radius tokens. No CSS overrides anywhere.
TOKENS A — defaults TOKENS B — customer override
{ {
accent: "#3b82f6", /* SDK blue */ accent: "#dc2626", /* red */
fontHeading: "Source Serif Pro", fontHeading: "Playfair Display",
fontBody: "Source Serif Pro", fontBody: "Source Serif Pro",
density: "comfortable", density: "roomy",
} }
┌───────────────────────────┐ ┌───────────────────────────┐
│ ─── CATEGORY · 8 MIN ─── │ │ ─── CATEGORY · 8 MIN ─── │
│ │ │ │
│ Why we rebuilt the │ │ Why we rebuilt │
│ editorial experience │ │ the editorial │
│ │ │ experience │
│ A serif lede paragraph │ │ │
│ pulls readers in... │ │ A serif lede paragraph │
│ │ │ pulls readers in, with │
│ [Read more →] │ │ more breathing room. │
│ ↑ blue │ │ │
│ │ │ [Read more →] │
└───────────────────────────┘ │ ↑ red │
│ │
└───────────────────────────┘
tighter rhythm, blue links looser rhythm, larger title,
red accents, display serifSame source code, two completely different feels. That's the leverage of the token system: customer changes ~3 lines of config, the whole blog re-skins.
TOKENS C — dark mode auto TOKENS D — Brutalist + brand
{ {
layout: "editorial", layout: "brutalist",
accent: "#22c55e", accent: "#facc15",
darkMode: "auto", fontHeading: "JetBrains Mono",
} density: "compact",
}
┌───────────────────────────┐
│ ████ CATEGORY · 8 MIN │ ┌───────────────────────────┐
│ (dark surface) │ │ 01 ENG — MAY 1 │
│ │ │ │
│ Why we rebuilt the │ │ WHY WE REBUILT THE │
│ editorial experience │ │ EDITORIAL EXPERIENCE │
│ │ │ │
│ Body text inverts to │ │ → ↑ yellow accent │
│ light-on-dark when OS │ │ mono headings │
│ theme flips. Mermaid + │ │ tight density │
│ syntax follow. │ └───────────────────────────┘
│ │
│ [Read more →] ↑ green │ entirely different layout,
└───────────────────────────┘ same five-token contract
inverts on prefers-color-schemeThe customer never edits CSS. They pick a layout, set a few tokens, the system handles the rest. Theming surface = ~6 inputs. Output coverage = every page in the blog.
5.8 Brand kit (later)
In a future version, customers could save a "brand kit" (logo, accent color, font pair, dark-mode preference) at the workspace level. Every vlozi product (blog, forms, newsletter, chatbot) reads from it. Out of scope for layouts MVP — but the token contract is designed so a brand kit just becomes the default token bag.
6. Surface 1 — React / Next.js Component
For customers with an existing React or Next.js site. The current SDK path, just much higher-level.
6.1 The all-in-one mount
// app/blog/[[...slug]]/page.tsx
import { VloziBlogSite } from "@vlozi/blog/react"
export default function BlogPage() {
return (
<VloziBlogSite
tenant="my-tenant"
apiKey={process.env.NEXT_PUBLIC_VLOZI_API_KEY!}
layout="editorial"
tokens={{ accent: "#0066ff" }}
/>
)
}One catch-all route, one component, the entire blog renders. List, post, category, tag, search, 404 — all handled.
6.2 The granular path (unchanged)
Power users still get the existing components:
import { BlogList, BlogPost, useBlogPosts } from "@vlozi/blog/react"Compose your own pages. Layouts don't replace this — they sit on top.
6.3 SSR / RSC support
Layouts must work in:
- Next.js App Router (RSC + client components mixed)
- Next.js Pages Router (legacy customers)
- Vite/SPA (no SSR)
The all-in-one component handles each via auto-detection. Search uses URL state (?q=…) so server-rendered pages handle search natively without JS — client-side instant search is an opt-in via searchMode="instant" on the layout root.
7. Surface 2 — Embed Widget
For partial embedding on a customer's existing site. This is not "embed the whole blog inside Webflow" — full-blog cases are handled by the React SDK today, and by the future hosted-blog CNAME flow. This is "show 3 latest blog posts on the homepage."
7.1 What it covers
| Widget | Purpose |
|---|---|
| Latest posts strip | Show the 3 latest posts as cards on a homepage. |
| Featured post block | Pin one post to a homepage hero section. |
| Category showcase | "Latest from Engineering" — filtered latest posts by category. |
| Reading list | (Later) personalized recommendations once contact-intelligence ships. |
Each widget gets its own one-line install:
<script src="https://cdn.vlozi.app/embed/v1.js" defer></script>
<div data-vlozi-blog="latest" data-tenant="..." data-count="3" data-layout="editorial"></div>7.2 Hydration strategy
Server-rendered HTML on first byte, hydrated for interactivity. The embed/v1.js script:
- Reads the
data-*attributes from each<div data-vlozi-blog>. - Fetches
cdn.vlozi.app/embed/render?...— which returns pre-rendered HTML for the widget body. - Replaces the empty
<div>content with the HTML, inside a Shadow DOM so the host page's CSS reset can't leak in. - Attaches event listeners (hover prefetch, click-through) on the rendered tree.
Why not pure client-side rendering: that flow shows an empty <div> until JS loads, kills the host page's LCP, and Google PageSpeed flags it. The server-render-then-hydrate flow keeps embedded widgets neutral on Core Web Vitals — important because customers care about their own SEO, not vlozi's.
Shadow DOM contract:
- Embedded widgets do not inherit
--vlz-*from the host page's CSS (Shadow DOM isolation prevents this). - Tokens come from the
data-tokens='{"accent":"#0066ff"}'attribute on the<div>, plus the layout's defaults from the worker. - Fonts: widgets inline a single font via
@font-faceinside the shadow tree. Web-font loading is the widget script's responsibility; host page's font-loading state has no effect.
7.3 What it does NOT cover
- Full blog index. (Use the React SDK today; future: hosted blog.)
- Single-post pages. (Use the React SDK; future: hosted blog.)
- Search. (Use the React SDK; future: hosted blog.)
The embed widget is intentionally a homepage decoration, not a blog-replacement. Trying to do "embed the entire blog as an iframe" is what kills DX (slow, breaks SEO, looks foreign on the host site). We don't do that.
8. Schema Additions
Tiny. The blog content tables don't change.
CREATE TABLE tenant_blog_config (
tenant_id text PRIMARY KEY,
layout text NOT NULL DEFAULT 'editorial',
layout_version text NOT NULL DEFAULT 'v1', -- pins layout=editorial@v1; opt-in upgrade
tokens_json jsonb NOT NULL DEFAULT '{}',
updated_at timestamp NOT NULL DEFAULT now()
);Three columns of real config (layout, layout_version, tokens_json). The layout_version exists from day one even though only v1 will be valid at launch — adding the column later requires a migration on every tenant. The hosted-blog vision adds two more columns (custom_domain, custom_domain_verified_at) when that surface ships.
9. Architecture Sketch
┌─────────────────────┐
│ Dashboard │
│ Layout picker UI │
└──────────┬──────────┘
│ writes
▼
┌─────────────────────┐
│ blog-service │
│ /admin/blog/config │
└──────────┬──────────┘
│ reads
┌────────────┴────────────┐
▼ ▼
┌─────────────────┐ ┌─────────────────┐
│ React SDK │ │ embed worker │
│ @vlozi/blog │ │ cdn.vlozi.app/ │
│ <VloziBlogSite/>│ │ embed/v1.js │
└────────┬────────┘ └────────┬────────┘
│ │
│ customer's │ data-vlozi-blog
│ Next.js app │ div on any site
│ │
▼ ▼
React render Server-rendered HTML
(RSC + client) in Shadow DOM (hydrated)
[Future: hosted blog surface — see hosted-blog-vision.md]One layout source of truth — a TypeScript module per layout that exports the rendering primitives (see section 17 for the layout-author contract). Each surface (React SDK, embed widget, future hosted worker) consumes the same source. New layouts ship to all surfaces simultaneously.
10. SDK Design
// Top-level, all-in-one
import { VloziBlogSite } from "@vlozi/blog/react"
<VloziBlogSite
tenant="my-tenant"
apiKey={...}
layout="editorial" // "editorial" | "minimal" | "magazine" | "brutalist" | …
layoutVersion="v1" // pin a specific version; default = "latest"
tokens={{
accent: "#0066ff",
fontHeading: "...",
fontBody: "...",
density: "comfortable",
radius: "none",
darkMode: "auto",
mermaidTheme: "auto", // mirrors VloziProvider config
syntaxHighlighting: true,
}}
basePath="/blog" // for nested routing
searchMode="server" // "server" (URL-state) | "instant" (client-side)
onPostView={(post) => { /* analytics hook */ }}
/>
// Per-page, if you want to mount specific routes
import { BlogIndexPage, BlogPostPage } from "@vlozi/blog/react"
// Granular, unchanged from today
import { BlogList, BlogPost, useBlogPosts } from "@vlozi/blog/react"The all-in-one path is what we recommend in the docs and what the dashboard's "integration code" panel generates. Granular components stay for power users.
11. MVP Scope
11.0 Visual scope — what 2.2.0 actually ships
┌──────────────────────────────────────┐
│ @vlozi/blog@2.2.0 (this MVP) │
└─────────────────┬────────────────────┘
│
┌──────────────────────────┼─────────────────────────┐
▼ ▼ ▼
┌────────────┐ ┌────────────┐ ┌──────────────┐
│ 2 LAYOUTS │ │ TOKENS │ │ <VloziBlog │
│ │ │ │ │ Site /> │
│ Editorial │ │ accent │ │ │
│ Minimal │ │ surface*4 │ │ One catch- │
│ │ │ font*2 │ │ all route, │
│ Each ships │ │ density │ │ entire blog │
│ all surf: │ │ darkMode │ │ renders │
│ list, post,│ │ │ │ │
│ cat, tag, │ │ Validates │ │ Wraps SDK + │
│ search, │ │ contrast │ │ Provider │
│ 404, OG, │ │ + fonts │ │ + layout │
│ RSS │ │ │ │ │
└─────┬──────┘ └─────┬──────┘ └──────┬───────┘
│ │ │
└─────────────────────────┴──────────────────────────┘
│
▼
┌──────────────────────────────┐
│ DASHBOARD: Settings → Blog │
│ │
│ • Screenshot gallery picker │
│ • Color + font + density UI │
│ • Live token preview │
│ • WCAG-AA contrast warning │
└──────────────────────────────┘
│
▼
┌──────────────────────────────┐
│ TELEMETRY │
│ │
│ • layout_selected │
│ • layout_render │
│ • first_post_published │
└──────────────────────────────┘
EXCLUDED from 2.2.0 (later releases):
✗ Magazine, Brutalist, Notion-doc, Mono layouts (v1.1+)
✗ Embed widget (v1.2)
✗ Hosted blog + custom-domain CNAME (separate doc)
✗ `radius` token, brand kit
✗ A/B test layouts
✗ Per-route component exports11.1 Cut to the bone for blog v1.0 launch:
| Layer | In | Out |
|---|---|---|
| Layouts | Editorial + Minimal | Magazine, Brutalist, Notion-doc, Mono |
| Surfaces | React SDK only (<VloziBlogSite />) |
Embed widget, hosted blog (separate doc) |
| Tokens | accent, surface, surfaceHover, border, mutedFg, fontHeading, fontBody, density, darkMode |
radius, brand kit |
| Token validation | Color contrast warnings, curated font list | Real-time rendering preview (added in L3) |
| Admin | Layout picker in Settings → Blog (already a route), token editor, screenshot gallery of layouts |
Live in-dashboard preview, A/B test layouts |
| SDK | <VloziBlogSite layout=… tokens=… /> |
Per-route components, RSC-specific exports |
| Schema | tenant_blog_config.{layout, layout_version, tokens_json} |
custom_domain column (lives in hosted-blog vision) |
| OG images | Editorial + Minimal templates | The other layouts' OG (added with each layout) |
| Telemetry | blog.layout_selected, blog.layout_render, blog.first_post_published events |
Per-token usage breakdown |
Result at launch: every customer can ship a polished blog with one prop. Two layouts is enough to validate the pattern; we'll know quickly which one is the default people pick — telemetry will tell us within ~4 weeks.
12. Build Order (phased)
| Phase | Scope | Estimate |
|---|---|---|
| L1 — Editorial layout | Index, post, category, tag, search, 404 — all pages, all states, mobile, dark, a11y, OG template. First layout pays the harness cost. | 6–8 days |
| L2 — Minimal layout | Same surfaces, different design. Reuses harness from L1. | 3 days |
| L3 — Token system + dashboard picker | tokens_json, font/color UI in Settings → Blog, contrast validation, screenshot gallery |
3 days |
| L4 — SDK rewrite | <VloziBlogSite /> umbrella + nested route handling, version pinning |
3 days |
| L4.5 — Storybook fixtures | Every layout × every surface × loading/error/empty states. Becomes the layout regression harness. | 1 day |
| L5 — Telemetry hooks | Three events wired through the dashboard + SDK | 0.5 day |
| L6 — Docs | Integration guide rewrite, layout gallery with live demos, copy-paste snippets | 1 day |
| MVP total | ~17–19 working days (~3.5 weeks) | |
| L7 — Brutalist layout (v1.1) | Third option for technical audiences | 3 days |
| L8 — Magazine layout (v1.1) | Visual-heavy option | 3 days |
| L9 — Embed widget (v1.2) | <script> + <div data-vlozi-blog> for "latest posts" — Shadow DOM, server-rendered HTML, hydration |
3–4 days |
| L10 — Notion-doc + Mono (v1.4+) | Two final layouts, only if catalog data justifies | 3 days each |
The hosted-blog surface (<tenant>.vlozi.app/blog + custom-domain CNAME) is its own ~8-day build tracked in hosted-blog-vision.md. It's gated on layouts validating in the React-SDK surface first.
13. Risks & Open Questions
| Risk / Question | Discussion |
|---|---|
| The "let me override the CSS" pressure | Will be constant. Resist firmly. The product is the layout. If a customer needs full control they should use granular SDK components. Document this position publicly so it's not a surprise. |
| Mobile + dark mode + RTL = N × layouts | Every layout × 3 axes = a lot of QA. Cap at 6 layouts forever; treat new layouts as expensive. Each new layout ships only when it has all of: mobile, dark, light, accessibility audit. |
| Layout maintenance over time | A layout from 2026 will look dated by 2028. Versioning: layout="editorial" always means "latest editorial," layout="editorial@v1" pins. Existing customers don't get auto-upgraded — opt-in only via layoutVersion prop. The layout_version schema column exists from day one. |
| Embed widget styling clash | Embedded widgets pick up host-site CSS reset surprises. Solution: render inside a Shadow DOM with scoped styles. Tokens come from data-tokens attribute, not host CSS vars. Standard pattern. |
| Token schema lock-in | The first version of tokens_json becomes a forever-supported contract. Pick the schema carefully; never remove a token, only deprecate. New tokens are additive. Unknown tokens are dropped silently in production with a console warning in dev. |
| Layout discovery in the dashboard | Customers won't know the difference between layouts from a dropdown name. Settings page needs a real layout gallery with screenshots and live previews — not a <select>. Screenshots ship with each layout (/layouts/{id}/cover.png). |
| Color contrast failures in user-picked accent colors | Inline WCAG-AA validation in the dashboard token editor. Warn but don't block. Same warning surfaces in the per-post preview. (Section 5.3.) |
| "Editorial vs Minimal" gallery confusion | Both are restrained sans/serif layouts. Worry: customers can't tell them apart from screenshots. Pre-launch test: show 5 non-designers a side-by-side and ask "which feels more 'magazine' vs 'product'?" — if confusion is >30%, drop Minimal from v1.0 and ship Editorial alone. |
14. Why This Wins
- Removes the largest source of customer drop-off. Layout decisions are where founders stall today. Layouts make the decision a 30-second pick.
- Makes vlozi feel polished from minute one. A new customer's first impression is the integration experience. Two-week-old products rarely look this good without layouts.
- Marketing leverage. Each layout is its own screenshot, its own demo, its own landing-page section. "Pick your aesthetic" is a stronger pitch than "install our SDK."
- Compounds with the other extensions at the chrome level. Books and series share the same layout chrome (header, footer, nav, dark-mode handling, OG template style) but have their own content surfaces (chapter reader for books, series-index page for series). Picking "Brutalist" for your blog at signup means your future book inherits the same visual language without any extra config — a single brand decision flows through the platform.
- Sets up the hosted surface to be cheap when we add it. Because layouts are authored once and consumed by any rendering surface, the future hosted-blog + custom-domain work (see
hosted-blog-vision.md) is mostly an infrastructure project — the layouts themselves don't have to be re-authored.
15. Recommendation
Ship Editorial + Minimal at blog v1.0 launch. Everything else after.
- Now (May 2026): validate this doc internally; pick the two MVP layouts; do a small mood-board exploration of each before coding. Run the "Editorial vs Minimal gallery confusion" test (section 13) with 5 non-designers — drop Minimal if it's confused with Editorial.
- Blog v1.0 (planned launch): include layout picker with Editorial + Minimal. Default = Editorial. Telemetry wired from day one.
- v1.1 (~3 weeks post-launch): add Brutalist + Magazine. Now the catalog is 4. Use telemetry to confirm Editorial is the right default — switch if not.
- v1.2 (~6 weeks post-launch): embed widget for homepages.
- v1.4+: Notion-doc + Mono only if telemetry says yes (e.g. > 20% of customers requesting a layout we don't have, with at least two of those requests pointing at this aesthetic). Otherwise stay at 4.
- Future: the hosted-blog surface ships when (a) the React-SDK layouts have proven traction, and (b) we have signal that the non-React audience is real (Webflow / Framer customer requests, signups checking "I don't have a website yet" in onboarding > some threshold). Tracked separately in
hosted-blog-vision.md.
The catalog never grows past 6. That's the discipline that makes the system maintainable. Every "we should add a fifth column option" request gets answered with: "use the granular SDK components, layouts are picked not built."
This idea earns its slot in the launch because two layouts is a small enough scope to ship at v1.0 without delaying anything, and the catalog can grow on a slow drumbeat as real-world usage tells us which aesthetics resonate.
16. Out of scope (forever)
Pre-empting the obvious "but couldn't we also…" asks. These are intentionally never on the roadmap, and saying so up front prevents drift:
| Out of scope | Why |
|---|---|
| WYSIWYG layout editor | Customers DIY-ing layouts is exactly what we're avoiding. |
| Drag-drop block editor for the blog page | Same. Layouts are picked, not built. |
| AI-generated layouts from a screenshot | Demo magic; unmaintainable and inconsistent. Pick one of six. |
| User-contributed layouts (free) | Six layouts forever. The "premium agency layouts" idea may exist later (see section 17) but never as a free open-contribution surface. |
| Per-post layout overrides | "This one post should use Magazine instead of Editorial." Don't. Confuses readers, breaks RSS, breaks OG cache. |
17. Layout-author contract (sketch)
A layout is a TypeScript module exporting a fixed-shape object. This contract isn't a public surface for v1.0 — it's an internal spec — but having it written down keeps future "premium agency layouts" or "internal layout v2" work coherent.
import type { VloziLayout } from "@vlozi/blog/layouts"
export const editorial: VloziLayout = {
id: "editorial",
version: "v1",
meta: {
name: "Editorial",
tagline: "Magazine-grade reading layout",
bestFor: ["writers", "thought leaders", "B2B blogs"],
cover: "/layouts/editorial/cover.png",
},
// What tokens this layout reads. Tokens not in this list are dropped
// (with a dev warning) when applied to this layout.
tokenSchema: {
accent: { type: "color", default: "#0066ff" },
fontHeading: { type: "font", default: "Cormorant Garamond", curated: ["Cormorant Garamond", "Source Serif Pro", "Lora"] },
fontBody: { type: "font", default: "Source Serif Pro" },
density: { type: "enum", values: ["compact", "comfortable", "roomy"], default: "comfortable" },
darkMode: { type: "enum", values: ["light", "dark", "auto"], default: "auto" },
// ...
},
// The surfaces. Each is a React component.
surfaces: {
index: IndexPage,
post: PostPage,
category: CategoryPage,
tag: TagPage,
series: SeriesPage,
search: SearchPage,
notFound: NotFoundPage,
og: OgImageTemplate, // server-only — ImageResponse via next/og
},
// Storybook fixtures live alongside the layout — required for
// review + layout regression testing.
storybook: () => import("./editorial.stories"),
}The contract is internal to the SDK monorepo. Adding a new layout means:
- Create
packages/blog-sdk/src/react/layouts/{id}/withindex.tsx(the export above), the surface components, anog.tsxtemplate, astories.tsxStorybook file, and acover.pngscreenshot. - Register in the layout registry (
packages/blog-sdk/src/react/layouts/index.ts). - Pass the layout-readiness checklist: mobile QA, dark mode QA, WCAG-AA contrast on default tokens, RTL render, Storybook stories for every surface × loading/error/empty state.
- PR review by the design owner + a code review.
If this contract ever becomes a paid agency-contribution channel (post-v1.6, only if there's clear demand), it becomes the published spec. Until then it stays in-repo so we can refactor it freely.