Keep an already-built site showing current content, without rebuilding it.
The problem
If your site renders blog content at build time — generateStaticParams,
output: "export", or any static host — that content is frozen the moment the
build finishes. Fix a typo in the Vlozi dashboard and your live site keeps
showing the old text until someone rebuilds.
Live content closes that gap in the browser. Each rendered surface carries the content version it was built from; on page view it asks Vlozi whether that version is still current, and only if the answer is no does it fetch and swap in fresh content.
The check is a single ~30-byte response, cached for 10 seconds by your reader's browser and at our edge, and it happens during idle time. In the overwhelmingly common case — nothing changed since your build — that's all it costs.
Turn it on
One line, in a server module your root layout imports:
// app/layout.tsx
import { setVloziLiveDefaults } from "@vlozi/blog/server";
setVloziLiveDefaults({
apiKey: process.env.NEXT_PUBLIC_VLOZI_KEY!,
enabled: true,
});Every @vlozi/blog/server component in the app picks it up. Individual
components can override:
<ServerBlogList client={vlozi} live={false} /> {/* opt out */}
<ServerBlogPost client={vlozi} slug={slug} live /> {/* opt in */}
<ServerBlogList client={vlozi} live={{ ttlMs: 30_000 }} /> {/* tune */}Why it needs its own key
The check runs in your reader's browser, so the key is embedded in your HTML
where anyone can read it. That's what publishable keys (pk_*) are for —
they're read-only and can be locked to your domain.
The SDK will not fall back to the key your server client uses, and refuses any
key that isn't pk_*. @vlozi/blog/server exists so credentials stay on the
server; quietly publishing one would take that decision away from you.
Create a publishable key in Settings → API keys, and set its allowed domains to your site's origin.
NOTE
Live content is off by default. Turning it on adds ~3.5 KB of client JavaScript and puts that key in your HTML — two things worth choosing rather than inheriting. With it off, your pages render byte-for-byte as before and load no extra JavaScript at all.
What it does and doesn't cover
| Change | Covered |
|---|---|
| Edit a published post | ✅ within ~30s |
| Unpublish or delete a post | ✅ — the page switches to not-found and gets noindex |
| Rename a category or tag | ✅ |
| Publish a brand-new post | ❌ — see below |
| Search-engine indexing | ❌ — crawlers read your built HTML |
New posts need a rebuild. On a static export the page for a new post doesn't exist as a file, and no amount of client-side JavaScript can create one — the list will update and then link to a URL that 404s. Pair live content with a deploy hook (Dashboard → Blog → Settings → Site updates) so new posts trigger a rebuild.
SEO is unaffected. Search engines index the HTML your build produced. Live content updates what a human sees; it doesn't change what was crawled. If fresh HTML matters for you, use ISR with a webhook, or a deploy hook.
When it's automatically disabled
The feature declines to run rather than doing something surprising. It's off if:
- No publishable key was supplied, or the key isn't
pk_* - Your backend doesn't provide the content-version endpoint
- You passed a
renderItem,renderorhrefForprop — those are functions and can't cross into the browser, so a refresh would re-render the surface with default markup and visibly restyle your page. UsehrefPatterninstead ofhrefForto keep both a custom link and live updates.
At runtime it also gives up quietly — and leaves your page exactly as rendered — when the reader is offline, the key has been rotated, the request is rate-limited, or anything else goes wrong. A stale page beats a broken one.
Custom links with live refresh
{/* hrefFor is a function — disables live refresh */}
<ServerBlogCategoryList client={vlozi} hrefFor={(c) => `/blog/category/${c.slug}`} />
{/* hrefPattern is a string — survives into the browser */}
<ServerBlogCategoryList client={vlozi} hrefPattern="/blog/category/{slug}" live />Styling the swap
The wrapper carries data-vlozi-live, which is "initial" before a refresh and
"fresh" after, so you can add your own transition:
[data-vlozi-live="fresh"] {
animation: vlozi-fade 200ms ease-out;
}
@media (prefers-reduced-motion: reduce) {
[data-vlozi-live="fresh"] { animation: none; }
}The SDK doesn't animate the swap itself — that's a motion decision that belongs in your design, not ours. It does hold the container's height steady across the swap so nothing below it jumps.