React SDK

Examples

End-to-end recipes for Next.js — server components, SSG, search, and image upgrades.

Practical recipes that combine the SDK's components and hooks into complete pages.

Next.js App Router (server components)

The most efficient way to use Vlozi with Next.js is to fetch data on the server. This improves SEO and initial load performance.

// app/blog/[slug]/page.tsx
import { vlozi } from "@/lib/vlozi";
import { BlogContent } from "@vlozi/blog/react";
import { notFound } from "next/navigation";
 
export async function generateMetadata({ params }) {
  const post = await vlozi.blog.get(params.slug).catch(() => null);
  if (!post) return {};
 
  return {
    title: post.seoTitle || post.title,
    description: post.seoDescription || post.excerpt,
  };
}
 
export default async function BlogPostPage({ params }) {
  const post = await vlozi.blog.get(params.slug).catch(() => null);
 
  if (!post) {
    notFound();
  }
 
  return (
    <article className="container max-w-3xl py-10">
      <h1 className="text-4xl font-bold">{post.title}</h1>
 
      {/* Category & Tags */}
      <div className="flex items-center gap-3 mt-4 text-sm text-muted-foreground">
        <time>{new Date(post.publishedAt).toLocaleDateString()}</time>
        {post.category && (
          <span className="bg-primary/10 text-primary px-2 py-0.5 rounded-full text-xs">
            {post.category.name}
          </span>
        )}
      </div>
      {post.tags && post.tags.length > 0 && (
        <div className="flex gap-2 mt-2">
          {post.tags.map((tag) => (
            <span key={tag.slug} className="text-xs bg-muted px-2 py-0.5 rounded-full">
              #{tag.name}
            </span>
          ))}
        </div>
      )}
 
      {/* Body — BlogContent applies the SDK's prose baseline, sanitizes,
          and progressively hydrates mermaid / carousels / images.
          Do NOT wrap in <article className="prose"> — see Quickstart Rule 2. */}
      <BlogContent html={post.content} className="mt-8" />
    </article>
  );
}

Static site generation (SSG)

If you use generateStaticParams, you can build your blog as a static site.

export async function generateStaticParams() {
  const posts = await vlozi.blog.list({ limit: 100 });
 
  return posts.data.map((post) => ({
    slug: post.slug,
  }));
}

Category page

Build a page that shows all posts in a specific category:

// app/blog/category/[slug]/page.tsx
import { vlozi } from "@/lib/vlozi";
 
export default async function CategoryPage({ params }) {
  const [posts, categories] = await Promise.all([
    vlozi.blog.list({ category: params.slug, limit: 20 }),
    vlozi.blog.categories.list(),
  ]);
 
  const currentCategory = categories.data.find(
    (c) => c.slug === params.slug
  );
 
  return (
    <div className="container py-10">
      <h1 className="text-3xl font-bold mb-2">
        {currentCategory?.name || params.slug}
      </h1>
      <p className="text-muted-foreground mb-8">
        {posts.meta.total} posts in this category
      </p>
 
      {posts.data.map((post) => (
        <article key={post.slug} className="mb-6">
          <h2>{post.title}</h2>
          <p>{post.excerpt}</p>
        </article>
      ))}
    </div>
  );
}

Search page

Build a client-side search experience with debounced input:

"use client";
 
import { useState, useEffect, useRef } from "react";
import { usePosts } from "@vlozi/blog/react";
 
export default function SearchPage() {
  const [query, setQuery] = useState("");
  const [debouncedQuery, setDebouncedQuery] = useState<string>();
  const timer = useRef<ReturnType<typeof setTimeout>>(undefined);
 
  useEffect(() => {
    timer.current = setTimeout(() => {
      setDebouncedQuery(query.trim() || undefined);
    }, 300);
    return () => clearTimeout(timer.current);
  }, [query]);
 
  const { data, loading } = usePosts({
    search: debouncedQuery,
    sort: "title",
    order: "asc",
  });
 
  return (
    <div className="container py-10">
      <input
        type="text"
        value={query}
        onChange={(e) => setQuery(e.target.value)}
        placeholder="Search blog posts…"
        className="w-full border rounded-lg px-4 py-2 mb-6"
      />
 
      {loading && <p>Searching…</p>}
 
      {data?.data.map((post) => (
        <a key={post.slug} href={`/blog/${post.slug}`}>
          <h3>{post.title}</h3>
          <p>{post.excerpt}</p>
        </a>
      ))}
 
      {!loading && data?.data.length === 0 && (
        <p>No posts found for "{query}"</p>
      )}
    </div>
  );
}

Upgrade body images to Next.js <Image>

Pass an imageComponent prop to <BlogContent> and every <img> tag in the post body is portal-mounted with your component. Featured images on cards/lists also accept the same prop. Your component receives src, alt, plus the original width/height when the editor recorded them — perfect for Next.js's required intrinsic sizing.

// components/SmartImage.tsx
"use client";
 
import NextImage from "next/image";
import type { VloziImageComponent } from "@vlozi/blog/react";
 
export const SmartImage: VloziImageComponent = ({ src, alt, width, height, className }) => (
  <NextImage
    src={src}
    alt={alt ?? ""}
    width={width ?? 1280}
    height={height ?? 720}
    sizes="(max-width: 768px) 100vw, 768px"
    className={className}
  />
);
 
// app/blog/[slug]/page.tsx — pass it through to BlogContent
import { BlogContent } from "@vlozi/blog/react";
import { SmartImage } from "@/components/SmartImage";
 
<BlogContent html={post.content} imageComponent={SmartImage} />

The same imageComponent prop works on <BlogList>, <BlogCard>, <BlogPost>, <BlogInfiniteList>, and <RelatedPosts>, so you can wire one component for the whole module.

Rewrite HTML before render (transformHtml escape hatch)

When you need to inject ads after the third paragraph, swap a deprecated shortcode, or strip a class your CMS leaves behind, use transformHtml. It runs after the SDK's sanitizer and before hydration, so anything you add still passes through the same defense-in-depth pipeline.

<BlogContent
  html={post.content}
  transformHtml={(html) => {
    // Inject a CTA after the first <h2>
    return html.replace(
      /<h2/,
      '<aside class="cta">Subscribe to our newsletter →</aside><h2'
    );
  }}
/>

WARNING

The transform runs on every render. Memoize your transformer (useCallback) when the input HTML is stable to avoid re-parsing.

Polished loading with skeleton primitives

Replace ad-hoc <p>Loading…</p> with the SDK's shape-matched skeletons. They share the same surfaces and spacing as the real components, so the layout doesn't reflow on data arrival.

import {
  BlogList,
  BlogPost,
  BlogListSkeleton,
  BlogPostSkeleton,
} from "@vlozi/blog/react";
 
// List page
<BlogList columns={3} renderLoading={() => <BlogListSkeleton columns={3} count={6} />} />
 
// Detail page
<BlogPost
  slug={params.slug}
  renderLoading={() => <BlogPostSkeleton showFeaturedImage />}
  renderNotFound={() => <p>Post not found</p>}
/>

Blog with sidebar navigation

Combine BlogCategoryNav, BlogTagNav, and BlogList for a complete blog layout:

"use client";
 
import { useState } from "react";
import { BlogList, BlogCategoryNav, BlogTagNav } from "@vlozi/blog/react";
 
export default function BlogWithSidebar() {
  const [category, setCategory] = useState<string | null>(null);
  const [tag, setTag] = useState<string | null>(null);
 
  return (
    <div className="flex gap-8">
      {/* Sidebar */}
      <aside className="w-64 shrink-0">
        <h3 className="font-bold mb-3">Categories</h3>
        <BlogCategoryNav
          variant="sidebar"
          activeCategory={category ?? undefined}
          onSelect={setCategory}
          showCounts
        />
 
        <h3 className="font-bold mt-8 mb-3">Tags</h3>
        <BlogTagNav
          variant="cloud"
          activeTag={tag ?? undefined}
          onSelect={setTag}
          showCounts
        />
      </aside>
 
      {/* Main Content */}
      <main className="flex-1">
        <BlogList
          category={category ?? undefined}
          tag={tag ?? undefined}
          columns={2}
          showPagination
        />
      </main>
    </div>
  );
}
Blog · React SDKEdit on GitHub