logicspike/docs

Blog Engine

`@vlozi/blog` — AI Integration Prompt

Last Updated: 2026-05-06 Status: Active

Paste-ready prompt for AI coding assistants (Claude / Cursor / Copilot / GPT) to integrate @vlozi/blog into a React or Next.js application without losing styling, breaking SEO, or shipping a half-working integration. Target SDK version: @vlozi/blog@2.1.6+.

NOTE

Every claim in this prompt is grounded in sdk-reference.md. When in doubt, that doc is the ground truth.


1. How to use this document

  1. Copy Section A verbatim into your AI assistant.
  2. Replace YOUR_API_KEY and YOUR_GATEWAY_URL with the real values.
  3. After the agent finishes the integration, run Section B's verification fixture before considering the integration done.
  4. If anything fails, jump to Section D — Troubleshooting.

2. Section A — The integration prompt

Copy everything between the ===== lines into your AI assistant.

=====================================================================
 
## Task
 
Integrate the `@vlozi/blog` SDK (^2.1.6) into my React/Next.js application.
Build a working blog with `/blog` (list), `/blog/[slug]` (post detail),
`/blog/category/[slug]`, `/blog/tag/[slug]`, and an RSS feed.
 
## STEP 1 — Analyze my codebase first (mandatory before writing code)
 
Output your findings BEFORE writing any code so I can verify you understood
my codebase. Cover:
 
1. **Framework**: Next.js App Router? Pages Router? Astro? Vite + React Router?
2. **Design system**: tailwind.config.*, globals.css, CSS-vars file. Is it
   shadcn-style HSL tokens? Tailwind v4 with @theme? Plain CSS vars? List
   my color tokens, fonts, border-radius, spacing.
3. **Component library**: shadcn? MUI? Custom? List Button, Input, Card,
   Skeleton — whatever I have.
4. **Layout**: shared chrome (header, footer, container width, padding).
5. **Typography**: font family, heading sizes, body color, muted color.
6. **Routing**: file-based or programmatic.
7. **Theming**: dark mode — next-themes? data-theme attr? media query?
8. **Build mode**: dev / static export / SSR.
 
## STEP 2 — The four hard rules (violating these breaks the integration)
 
These are the most common failure modes I see in real consumer integrations.
DO NOT skip them.
 
### Rule 1 — Import `@vlozi/blog/styles.css` FIRST
 
```tsx
// app/layout.tsx (Next.js App Router)
import "@vlozi/blog/styles.css";   // ← MUST be first
import "./globals.css";             // ← my own styles second

The SDK's CSS targets .vlz-content and .vlz-* selectors. Importing your globals.css first means your global resets (Tailwind preflight, custom * { margin: 0 }, shadcn's text-base cascading) clobber the SDK's defaults. Symptom: post bodies render as flat unformatted text — no heading sizes, no list markers, no spacing. This is the single most common integration failure.

If your stack reverses the order anyway (some monorepo bundlers do), scope all your overrides under .vlz-content so they have higher specificity than the unscoped SDK rules.

Rule 2 — DO NOT add prose classes to <BlogContent>

The SDK ships its own prose baseline. The Tailwind Typography plugin's prose / prose-lg / dark:prose-invert classes are a parallel stylesheet that fights the SDK's. Result: specificity wars, broken heading rhythm, weird link colors.

❌ Don't:

<article className="prose prose-lg dark:prose-invert">
    <BlogContent html={post.content} />
</article>

✅ Do:

<article>
    <BlogContent html={post.content} />
</article>

Rule 3 — DO NOT style bare <pre> or <iframe> in your global CSS

The SDK portal-mounts <MermaidBlock> into the original <pre> and strips non-allowlisted iframes. Your pre { background: black } rule will ALSO color the mermaid host. Use the hydration data-attributes to scope:

/* Style my code blocks but not mermaid hosts */
.vlz-content pre:not([data-vlz-hydrated]) {
    background: var(--my-code-bg);
}
 
/* Style my non-SDK iframes (allowlisted YouTube embeds keep the SDK
   styling automatically; everything else is stripped before render) */
iframe:not([src*="youtube-nocookie.com"]):not([src*="youtube.com/embed"]) {
    /* whatever you want */
}

Rule 4 — Use slug as the React key, not id

The Post type has NO id field. The public API never returns one.

{posts.map((post) => (
    <li key={post.slug}>{post.title}</li>     // ✅
))}
 
{posts.map((post) => (
    <li key={post.id}>{post.title}</li>       // ❌ TypeScript error
))}

STEP 3 — Install + environment

pnpm add @vlozi/blog
# If your authors publish posts containing mermaid diagrams:
pnpm add mermaid

Required environment variables:

# Server-side use (Server Components, generateMetadata, RSS routes,
# blog-service calls). PREFER THIS — your API key never reaches the
# client bundle.
VLOZI_API_KEY=YOUR_API_KEY
VLOZI_BASE_URL=YOUR_GATEWAY_URL
 
# Only if you also need a browser-side client (interactive search,
# infinite scroll, etc.):
NEXT_PUBLIC_VLOZI_API_KEY=YOUR_API_KEY
NEXT_PUBLIC_VLOZI_BASE_URL=YOUR_GATEWAY_URL

The API key is a public read-only pk_* key — exposing it to the client is safe but not necessary for purely server-rendered blog routes.

STEP 4 — File structure (create these files)

lib/vlozi.ts — shared module-scope client (server-side)

import { VloziClient } from "@vlozi/blog";
 
export const blogClient = new VloziClient({
    apiKey: process.env.VLOZI_API_KEY ?? "",
    baseUrl: process.env.VLOZI_BASE_URL ?? "",
});
 
export const SITE_URL = process.env.NEXT_PUBLIC_SITE_URL ?? "https://example.com";

The constructor does NOT throw on empty values (2.1.5+); the typed VloziConfigError fires on the first API call. So this module imports cleanly during next build even when env vars aren't yet wired.

app/layout.tsx — root layout: SDK CSS first, then yours

import "@vlozi/blog/styles.css";   // FIRST — Rule 1
import "./globals.css";             // SECOND
 
export default function RootLayout({ children }: { children: React.ReactNode }) {
    return <html lang="en"><body>{children}</body></html>;
}

Map your design tokens onto the five --vlz-* knobs in your globals.css AFTER importing the SDK CSS:

/* shadcn-style HSL */
.vlz-content {
    --vlz-accent:        hsl(var(--primary));
    --vlz-muted-fg:      hsl(var(--muted-foreground));
    --vlz-border:        hsl(var(--border));
    --vlz-surface:       hsl(var(--muted) / 0.6);
    --vlz-surface-hover: hsl(var(--muted) / 0.8);
}
 
/* OR Tailwind v4 with @theme */
.vlz-content {
    --vlz-accent:        var(--color-primary);
    --vlz-muted-fg:      var(--color-muted-foreground);
    --vlz-border:        var(--color-border);
    --vlz-surface:       var(--color-muted);
    --vlz-surface-hover: var(--color-accent);
}
module.exports = {
    images: {
        remotePatterns: [
            { protocol: "https", hostname: "media.vlozi.app" },
            // add other hosts your authors actually use
        ],
    },
};

app/blog/page.tsx — list page (server-rendered, zero JS)

import { ServerBlogList } from "@vlozi/blog/server";
import { blogClient } from "@/lib/vlozi";
 
export const dynamic = "force-dynamic";   // or revalidate = 60 for ISR
 
export default function BlogIndexPage() {
    return (
        <main className="my-page-container">
            <h1 className="my-page-title">Blog</h1>
            <ServerBlogList client={blogClient} limit={9} />
        </main>
    );
}

If you want interactive search/filters: build it as a client component using usePosts from @vlozi/blog/react, wrapped in a <VloziProvider> mounted in app/blog/layout.tsx.

app/blog/[slug]/page.tsx — post detail

import type { Metadata } from "next";
import { notFound } from "next/navigation";
import { VloziApiError } from "@vlozi/blog";
import { BlogContent } from "@vlozi/blog/react";
import { generateMetadataForPost, generateStaticParamsForPosts } from "@vlozi/blog/next";
import { blogClient, SITE_URL } from "@/lib/vlozi";
 
type PageProps = { params: Promise<{ slug: string }> };
 
export async function generateStaticParams() {
    return generateStaticParamsForPosts({ client: blogClient });
}
 
export async function generateMetadata({ params }: PageProps): Promise<Metadata> {
    const { slug } = await params;
    return generateMetadataForPost({ client: blogClient, slug, siteUrl: SITE_URL });
}
 
export default async function PostPage({ params }: PageProps) {
    const { slug } = await params;
    let post;
    try {
        post = await blogClient.blog.get(slug);
    } catch (err) {
        if (err instanceof VloziApiError && err.status === 404) notFound();
        throw err;
    }
 
    return (
        <article className="my-post-layout">
            {post.featuredImageUrl && (
                <div className="my-post-banner">
                    {/* eslint-disable-next-line @next/next/no-img-element */}
                    <img src={post.featuredImageUrl} alt={post.title} />
                </div>
            )}
            <header className="my-post-header">
                {post.category && <span className="my-pill">{post.category.name}</span>}
                <h1>{post.title}</h1>
                {post.excerpt && <p className="my-lede">{post.excerpt}</p>}
            </header>
 
            {/* Body — sanitized, hydrated, syntax-highlighted */}
            {post.content && <BlogContent html={post.content} />}
 
            {post.tags && post.tags.length > 0 && (
                <footer className="my-post-tags">
                    {post.tags.map((t) => <span key={t.slug}>#{t.name}</span>)}
                </footer>
            )}
        </article>
    );
}

STEP 5 — When to use <VloziProvider> (client-side hooks/components only)

// app/blog/layout.tsx — wrap ONLY the routes that need hooks
"use client";
 
import { useMemo } from "react";
import { VloziClient } from "@vlozi/blog";
import { VloziProvider } from "@vlozi/blog/react";
 
export default function BlogLayout({ children }: { children: React.ReactNode }) {
    const client = useMemo(
        () => new VloziClient({
            apiKey: process.env.NEXT_PUBLIC_VLOZI_API_KEY!,
            baseUrl: process.env.NEXT_PUBLIC_VLOZI_BASE_URL!,
        }),
        [],
    );
    return (
        <VloziProvider
            client={client}
            config={{
                mermaidTheme: "default",      // or "auto" / "dark" / a function
                syntaxHighlighting: true,      // false to opt out (then ship your own .hljs theme)
                // imageComponent: NextImage,  // optional — upgrades inline body images
            }}
        >
            {children}
        </VloziProvider>
    );
}

Server-side fetching (generateMetadata, generateStaticParams, <ServerBlogList>, <ServerBlogPost>, RSS routes) does NOT need a provider — it uses the module-scope blogClient directly.

Mounting a provider when you only need server-rendering is dead weight.

STEP 6 — Critical gotchas (read once, save 4 hours)

  1. <BlogContent transformHtml={...}> is your escape hatch when the server-rendered HTML has bugs. Use it sparingly and only as a workaround until the upstream fix deploys. Output is still sanitized — not an XSS escape.

  2. YouTube embeds: only youtube-nocookie.com/ and youtube.com/embed/ are allowed by the iframe sanitizer. If you see an empty 16:9 box where a video should be, check the iframe's src.

  3. Mermaid: lazy-loaded only when a post contains a mermaid block. Install mermaid if your authors use it.

  4. <BlogCategoryNav onSelect> receives null for the "All" button. Convert with: onSelect={(slug) => setCategory(slug ?? undefined)}.

  5. post.content is HTML and only on the detail endpointclient.blog.list() returns posts WITHOUT content (excerpt only).

  6. post.id does not exist — the API never returns it. Use post.slug as your React key.

  7. Search debounce: 300ms is the SDK default in <BlogList searchable>. If you build your own search, debounce at 300ms — the API is rate-limited.

STEP 7 — Match my design

  • DO match my color scheme via the --vlz-* knob mapping (see Step 4)
  • DO match my container width, header/footer chrome on the blog pages
  • DO use my existing Card / Button / Skeleton components for custom list pages built with usePosts (instead of the prebuilt <BlogList>/<BlogCard>)
  • DO support my dark mode mechanism via mermaidTheme (function form for next-themes/data-theme integrations)

Output your codebase analysis from Step 1, then implement the integration.

=====================================================================

 
---
 
## 3. Section B — Verification fixture (run AFTER integration)
 
After the agent finishes, **render this test post** and visually confirm
each construct displays correctly. Most styling failures are caught here.
 
### Step B1 — Create a published test post
 
Authoring path: paste the markdown below into your editor (via the
`/dashboard` admin's "Import Markdown" or a direct API call, depending
on your tooling) and publish it.
 
```markdown
# Heading 1 — should be the largest
 
A paragraph with **bold**, *italic*, ***bold-italic***, ~~strikethrough~~,
and `inline code`. A [link](https://example.com).
 
## Heading 2 — should be visibly smaller than H1
 
### Heading 3
 
#### Heading 4
 
A long paragraph to verify body line-height and column rhythm. Lorem ipsum
dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor
incididunt ut labore et dolore magna aliqua.
 
- Bullet list item 1
- Bullet list item 2
- Bullet list item 3
 
1. Ordered list item 1
2. Ordered list item 2
3. Ordered list item 3
 
- [ ] Task item unchecked
- [x] Task item checked
 
> A blockquote with **bold** text inside.
 
`​`​`typescript
const x: string = "syntax-highlighted code block";
function greet(user: User): string {
  return `Hello, ${user.name}!`;
}
`​`​`
 
| Header A | Header B |
| --- | --- |
| Cell 1 | Cell 2 |
| Cell 3 | Cell 4 |
 
![Alt text for inline body image](https://placehold.co/800x400)
 
`​`​`mermaid
flowchart LR
  A[Author] --> B[Editor]
  B --> C[Published]
`​`​`
 
https://www.youtube.com/watch?v=dQw4w9WgXcQ
 
> [!NOTE]
> A callout block — should render with a subtle accent border.

Step B2 — Visual checklist

Visit the rendered page and confirm:

Construct Expected If broken, see...
# H1 is biggest, ## H2 smaller, ### H3 smaller still Each heading has visible margin-top, distinct font-size Section D row 1
**bold** is bolder, *italic* is italicized Inline marks render distinctly Section D row 2
Inline ​code​ has rounded grey background The --vlz-surface token is applied Section D row 1
Bullets visible on <ul>, numbers on <ol> List markers appear, indent visible Section D row 1
Task items show a checkbox Disabled checkboxes, label aligned with checkbox Section D row 3
Blockquote has accent left-border --vlz-accent color visible Section D row 1
Code block tokens are colored Keywords blue, strings purple-ish, comments italic-grey Section D row 4
Table rows have borders, header has subtle bg --vlz-border + --vlz-surface applied Section D row 1
Inline body image renders (or upgrades to your imageComponent) Picture visible with rounded corners Section D row 5
Mermaid block renders as an SVG diagram (NOT raw text) Live flowchart LR rendered Section D row 6
YouTube link on its own line embeds (not 16:9 empty box) iframe with the video Section D row 7
Callout shows accent left-border + tinted background border-color: #3b82f6 etc. Section D row 1

If every row checks out, the integration is correct. Ship it.


4. Section C — Customization recipes (paste as needed)

C1 — Custom <BlogList> with my own card component

Don't use <BlogCard>. Use usePosts() directly and render with my existing
Card component — same image aspect ratio, same typography, same spacing as
the rest of my app. Show: featured image, category pill, title, excerpt
(clamp 2 lines), author + date + reading time, tag pills.

C2 — Custom post page with <BlogContent>

For /blog/[slug] don't use <BlogPost>. Build my own header (matching my
site's article style) and use <BlogContent html={post.content!} /> for
the body. <BlogContent> handles sanitization plus rich-block hydration —
I just provide the layout chrome around it.

C3 — Headless integration (no SDK components)

Don't use any prebuilt SDK components. Use only the hooks (usePosts,
usePost, useCategories, useTags, useArchive, useRelatedPosts, useNeighbors)
and build all UI from my existing components. The only render-side I want
to keep is <BlogContent> for post bodies.

C4 — Dark mode + mermaid via next-themes

"use client";
import { useTheme } from "next-themes";
import { VloziClient } from "@vlozi/blog";
import { VloziProvider } from "@vlozi/blog/react";
 
const client = new VloziClient({ apiKey: ..., baseUrl: ... });
 
export function BlogProvider({ children }: { children: React.ReactNode }) {
    const { resolvedTheme } = useTheme();
    return (
        <VloziProvider
            client={client}
            config={{
                mermaidTheme: () => (resolvedTheme === "dark" ? "dark" : "default"),
            }}
        >
            {children}
        </VloziProvider>
    );
}

C5 — RSS + Sitemap

// app/feed.xml/route.ts
import { generateRSS } from "@vlozi/blog";
import { blogClient, SITE_URL } from "@/lib/vlozi";
 
export async function GET() {
    const xml = await generateRSS({
        client: blogClient,
        siteUrl: SITE_URL,
        title: "My Blog",
        description: "Latest posts",
        limit: 50,
    });
    return new Response(xml, { headers: { "Content-Type": "application/xml; charset=utf-8" } });
}
// app/sitemap.xml/route.ts
import { generateSitemap } from "@vlozi/blog";
import { blogClient, SITE_URL } from "@/lib/vlozi";
 
export async function GET() {
    const xml = await generateSitemap({
        client: blogClient,
        siteUrl: SITE_URL,
        additionalUrls: [{ loc: SITE_URL + "/", priority: 1.0, changefreq: "daily" }],
    });
    return new Response(xml, { headers: { "Content-Type": "application/xml; charset=utf-8" } });
}

C6 — Inline body image upgrade with Next.js Image

"use client";
import Image from "next/image";
import { BlogContent, type VloziImageComponent } from "@vlozi/blog/react";
 
const NextBlogImage: VloziImageComponent = ({ src, alt, width, height }) => (
    <Image
        src={src}
        alt={alt}
        width={width ?? 1200}
        height={height ?? 800}
        style={{ width: "100%", height: "auto" }}
    />
);
 
<BlogContent html={post.content!} imageComponent={NextBlogImage} />;

Per-image opt-out by adding data-vlz-skip-hydrate to a specific <img> in the source HTML.

C7 — Skeleton primitives for custom UIs

import {
    BlogPostSkeleton,
    BlogCardSkeleton,
    BlogListSkeleton,
} from "@vlozi/blog/react";
 
if (loading) return <BlogPostSkeleton />;
// or
<BlogListSkeleton columns={3} count={6} />;

5. Section D — Troubleshooting

# Symptom Cause Fix
1 Headings render as body text. No list markers. Body looks like a wall of unformatted text. Title above <BlogContent> looks fine, but the body inside is flat. Most common: @vlozi/blog/styles.css is imported AFTER your globals.css OR you have a global heading reset (Tailwind preflight, * { margin: 0 }, shadcn text-base cascading) that's overriding the SDK's .vlz-content h1/h2/h3... rules. (a) Move import "@vlozi/blog/styles.css" to the FIRST line of app/layout.tsx. (b) DevTools → inspect a heading → check the Computed tab → if SDK's rule is crossed-out, scope your reset under :not(.vlz-content) or move the import earlier. (c) If using Tailwind: add @layer base for your reset so unscoped SDK CSS still wins.
2 Bold/italic don't render <BlogContent> not used (you're using raw dangerouslySetInnerHTML) — the .vlz-content class isn't on the wrapper, so styles don't apply. Always use <BlogContent html={post.content!} /> for post bodies — it adds the .vlz-content wrapper plus sanitization + hydration.
3 Task list checkboxes are out of alignment with their labels Pre-2.1.6 bug. Upgrade. pnpm add @vlozi/blog@^2.1.6
4 Code blocks render in a single color (no syntax highlighting) Either: (a) syntaxHighlighting: false in provider config; (b) blog-service Worker isn't running 2.1.6 yet (server-side tokenization is part of the Worker deploy, not the npm publish). (a) Remove the false flag or default to true; (b) deploy the blog-service Worker with wrangler deploy.
5 Inline body images render as plain <img> even though I passed imageComponent imageComponent not threaded into <BlogContent> — check your prop. Body-image hydration is only triggered when imageComponent is provided either as a prop or via provider config. Pass imageComponent explicitly: <BlogContent imageComponent={NextImage} />. Or set it on the provider: <VloziProvider config={{ imageComponent: NextImage }}>.
6 Mermaid blocks render as raw flowchart LR ... text inside a beige box Mermaid not installed, OR you're on @vlozi/blog@2.1.5 (regression). (a) pnpm add mermaid; (b) upgrade to @vlozi/blog@^2.1.6.
7 YouTube embed shows as an empty 16:9 box (no video) The renderer emitted a non-allowlisted iframe src (e.g. youtube.com/watch?v=...) and the SDK's sanitizer stripped it. Pre-2.1.6: also strips the wrapper. (a) Deploy the blog-service Worker (URL normalization is in the Worker, not the npm package); (b) for instant fix: use <BlogContent transformHtml={...}> to rewrite watch URLs client-side.
8 Carousels render as a flat stack of images instead of a swipeable carousel Known bug — the blog-service renderer doesn't emit the <div data-type="carousel"> wrapper. The SDK has the carousel runtime but never sees the marker. Tracked in sdk-backlog.md. Until the renderer fix deploys, build your own carousel using <Carousel> from @vlozi/blog/react for posts that need this.
9 TypeScript error: Property id does not exist on type Post Pre-2.1.6 the SDK had id?: string in the type but the API never emitted it. Removed in 2.1.6. Use post.slug as your React key.
10 Build fails with Module not found: 'mermaid' Pre-2.1.6 had a bundler-resolution bug. Upgrade to @vlozi/blog@^2.1.6.
11 next build crashes with apiKey is required Pre-2.1.5 the constructor threw synchronously on empty key. Upgrade to @vlozi/blog@^2.1.5.
12 "useVlozi must be used within a VloziProvider" Component using a hook isn't wrapped in <VloziProvider>. Mount <VloziProvider client={client}> at the route layout level. Server-side fetching does NOT need a provider — only hooks/client-components do.
13 Featured images 404 in production next.config.js images.remotePatterns doesn't include the upload host. Add the actual upload host to remotePatterns.
14 Two different VloziConfig shapes show up in IDE autocomplete Naming collision: @vlozi/blog exports VloziConfig (client config) AND @vlozi/blog/react exports VloziConfig (provider config). TypeScript resolves them by import path automatically. To use both in one file, alias one: import type { VloziConfig as VloziProviderConfig } from "@vlozi/blog/react".
15 pre { background: ... } styles a mermaid host the wrong color Mermaid is portal-mounted into the <pre>. Your global CSS still applies. Scope your <pre> rules: .vlz-content pre:not([data-vlz-hydrated]) { background: ... }.

6. Section E — One-line prompts (copy as-is)

Minimal blog (server-rendered, zero JS, App Router)

Install @vlozi/blog (^2.1.6). Create /blog with a 3-column post grid using
ServerBlogList, and /blog/[slug] using server-side blogClient.blog.get(slug)
+ <BlogContent html={post.content!} />. Use my existing layout, color tokens,
fonts. Use `dynamic = "force-dynamic"` on both routes. Import
"@vlozi/blog/styles.css" FIRST in root layout (before globals.css). Map my
shadcn HSL tokens to --vlz-accent / --vlz-muted-fg / --vlz-border /
--vlz-surface / --vlz-surface-hover under .vlz-content. API key:
YOUR_API_KEY, baseUrl: YOUR_GATEWAY_URL.
Install @vlozi/blog (^2.1.6). Create:
- /blog (client component): debounced search input, BlogCategoryNav (sidebar
  variant), BlogTagNav (cloud), 3-col grid using my own Card component
  + usePosts(), pagination
- /blog/[slug] (server component): hand-rolled header matching my article
  style, <BlogContent html={post.content!} />, related posts via
  useRelatedPosts(), prev/next via useNeighbors()
- /blog/category/[slug], /blog/tag/[slug]: filtered list pages
- app/feed.xml/route.ts using generateRSS
- app/sitemap.xml/route.ts using generateSitemap
 
Map my shadcn HSL tokens to the five --vlz-* knobs under .vlz-content. Mount
BlogProvider only over /blog routes (config: mermaidTheme: "default",
syntaxHighlighting: true). Add `images.remotePatterns` for my media host.
Import "@vlozi/blog/styles.css" FIRST in root layout. API key: YOUR_API_KEY,
baseUrl: YOUR_GATEWAY_URL.

"Latest 3 posts" widget on homepage

Add a "Latest Posts" section to my homepage using server-side
blogClient.blog.list({ limit: 3 }). Render with my homepage card component
(NOT <BlogCard>). Match the card style, spacing, typography of the rest of
my homepage. No provider needed (server-side only). API key: YOUR_API_KEY.

7. Section F — Migration

From @vlozi/blog@2.1.5@vlozi/blog@2.1.6

pnpm add @vlozi/blog@^2.1.6

Breaking changes:

  • Post.id removed — TypeScript will flag key={post.id ?? post.slug} patterns. Replace with key={post.slug}.
  • <BlogContent> no longer attaches Tailwind prose classes (was already changed in 2.1.5; restating). If you depended on it, add className="prose prose-lg dark:prose-invert" yourself OR remove the dependency.

Additive (non-breaking):

  • <BlogContent transformHtml>, <BlogContent imageComponent>
  • <BlogList search> (controlled), <BlogArchive prefetchOnHover>
  • New exports: <BlogPostSkeleton>, <BlogCardSkeleton>, <BlogListSkeleton>
  • VloziImageProps.width / height (optional)

Bug fixes (just upgrade, no code change):

  • Mermaid hydration regression from 2.1.5 fixed
  • YouTube embed handling (renderer + sanitizer + wrapper-strip)
  • Task-list / callout / blockquote <p> margin leak
  • Server-side syntax highlighting (requires blog-service Worker deploy)

From legacy @logicspike/blog

Renamed on 2026-04-12.

pnpm remove @logicspike/blog && pnpm add @vlozi/blog@^2.1.6

Find/replace:

  • @logicspike/blog@vlozi/blog
  • LogicSpikeClientVloziClient
  • LogicSpikeProviderVloziProvider
  • useLogicSpikeuseVlozi
  • LOGICSPIKE_* env-var prefix → VLOZI_*

8. What this prompt deliberately doesn't cover

  • Layout/theme system (<VloziBlogSite>, named layouts) — not yet shipped. Tracked in layouts-vision.md for @vlozi/blog@2.2.0. Don't reference it in current integrations.
  • Hosted blog at <tenant>.vlozi.app/blog — not yet shipped. Tracked in hosted-blog-vision.md. Current integrations are React-SDK only.
  • Multi-provider embeds (Vimeo / Twitter / Spotify / etc.) — not supported. YouTube only.
  • Author pages (client.blog.authors.list/.get) — backend doesn't expose endpoints yet.
  • Draft preview mode — not implemented; published posts only.

When the SDK ships any of the above, this prompt and sdk-reference.md will be updated together.


End of document. Ground truth: sdk-reference.md. Bug queue: sdk-backlog.md.

Blog Engine