The SDK comes with a set of pre-built, accessible, and responsive components to help you build your blog quickly. Import them from @vlozi/blog/react.
BlogList
Renders a paginated grid or list of blog posts. Handles loading states, empty states, and errors automatically. Supports filtering by category and tag.
import { BlogList } from "@vlozi/blog/react";
<BlogList
columns={3}
category="tutorials"
tag="react"
showPagination
/>Props
| Prop | Type | Default | Description |
|---|---|---|---|
limit |
number | 10 | Number of posts to fetch per page. |
columns |
1 | 2 | 3 | 4 | 3 | Number of columns in grid mode. |
variant |
"grid" | "list" | "grid" | Layout mode — grid or vertical list. |
category |
string | — | Filter posts by category slug. |
tag |
string | string[] | — | Filter posts by tag slug. Array = OR-match. |
search |
string | — | Controlled search query. Wins over searchable's built-in input. Empty string omits the param. |
searchable |
boolean | false | Render a built-in debounced search input above the grid. Ignored if a controlled search prop is also set. |
searchPlaceholder |
string | "Search posts..." | Placeholder text for the built-in input. |
searchDebounceMs |
number | 300 | Debounce delay for the built-in input. |
sortable |
boolean | false | Render a built-in sort dropdown (date / title, asc / desc). |
showPagination |
boolean | true | Show pagination controls below the list. |
scrollToTopOnPageChange |
boolean | true | Scroll the list back into view when paginating. Set false for modal embedding. |
renderItem |
(post: Post) => ReactNode | — | Custom render function for each post card. |
imageComponent |
VloziImageComponent | — | Custom image component (e.g. Next.js Image) forwarded to every card. |
prefetchOnHover |
boolean | false | Prefetch post details when a card is hovered. |
renderLoading |
() => ReactNode | — | Custom loading state component. |
renderEmpty |
() => ReactNode | — | Custom empty state component. |
renderError |
(error: Error) => ReactNode | — | Custom error state component. |
className |
string | — | Additional CSS classes for the container. |
BlogCard
Renders an individual blog post card with title, excerpt, date, category badge, and tag pills. Supports multiple visual variants.
import { BlogCard } from "@vlozi/blog/react";
<BlogCard
post={post}
variant="featured"
onClick={(p) => router.push(`/blog/${p.slug}`)}
/>Props
| Prop | Type | Default | Description |
|---|---|---|---|
post |
Post | required | The post object to render. |
variant |
"default" | "featured" | "compact" | "default" | Visual style variant. |
renderMeta |
(post: Post) => ReactNode | — | Custom metadata renderer (date, author, etc). |
footer |
ReactNode | — | Custom footer content. |
onClick |
(post: Post) => void | — | Click handler for the card. |
imageComponent |
VloziImageComponent | — | Custom image component (e.g. Next.js Image). |
prefetchOnHover |
boolean | false | Prefetch the full post via VloziClient cache on hover. Requires a mounted VloziProvider. |
className |
string | — | Additional CSS classes. |
BlogPost
Renders a single full blog post including title, date, featured image, and HTML content. Fetches the post by slug automatically.
<BlogPost slug="my-first-post" showFeaturedImage />Props
| Prop | Type | Default | Description |
|---|---|---|---|
slug |
string | required | The post slug to fetch and render. |
showFeaturedImage |
boolean | true | Show the featured image above the content. |
renderTitle |
(title: string) => ReactNode | — | Custom title renderer. |
imageComponent |
VloziImageComponent | — | Custom component for the featured image. |
renderLoading |
Slot | — | Override the loading skeleton. Default: BlogPostSkeleton. |
renderError |
(error: Error) => ReactNode | — | Override the fetch-error UI. Fires on transient errors. |
renderNotFound |
Slot | — | Override the "post not found" UI. Fires on 404. Distinct from renderError. |
className |
string | — | Additional CSS classes. |
BlogCategoryNav
A navigation component that lists all categories. Useful for building a sidebar or filter bar. Fetches categories automatically via useCategories().
import { BlogCategoryNav } from "@vlozi/blog/react";
<BlogCategoryNav
variant="pills"
activeCategory={selectedCategory}
onSelect={(slug) => setSelectedCategory(slug)}
showCounts
/>Props
| Prop | Type | Default | Description |
|---|---|---|---|
variant |
"sidebar" | "tabs" | "pills" | "sidebar" | Visual layout variant. |
activeCategory |
string | — | Currently selected category slug. |
onSelect |
(slug: string | null) => void | — | Callback when a category is clicked. Receives null for "All". |
showCounts |
boolean | true | Show post count next to each category name. |
className |
string | — | Additional CSS classes. |
BlogTagNav
A navigation component that lists all tags. Supports a tag cloud or pill layout. Fetches tags automatically via useTags().
import { BlogTagNav } from "@vlozi/blog/react";
<BlogTagNav
variant="cloud"
activeTag={selectedTag}
onSelect={(slug) => setSelectedTag(slug)}
showCounts
/>Props
| Prop | Type | Default | Description |
|---|---|---|---|
variant |
"pills" | "cloud" | "pills" | Layout variant — pills or tag cloud. |
activeTag |
string | — | Currently selected tag slug. |
onSelect |
(slug: string | null) => void | — | Callback when a tag is clicked. Receives null for "All". |
showCounts |
boolean | true | Show post count next to each tag name. |
className |
string | — | Additional CSS classes. |
BlogInfiniteList
Like BlogList, but loads additional pages automatically as the user scrolls. Built on IntersectionObserver — zero scroll listeners. Use it for long feeds where pagination buttons would be awkward.
import { BlogInfiniteList } from "@vlozi/blog/react";
<BlogInfiniteList
columns={3}
limit={12}
category="tutorials"
rootMargin="400px"
/>Props
| Prop | Type | Default | Description |
|---|---|---|---|
limit |
number | 10 | Posts fetched per page. |
columns |
1 | 2 | 3 | 4 | 3 | Grid column count (responsive). |
category |
string | string[] | — | Filter by category slug. Array = OR-match. |
tag |
string | string[] | — | Filter by tag slug. Array = OR-match. |
search |
string | — | Full-text search. Changing it resets the feed to page 1. |
sort |
"publishedAt" | "title" | "createdAt" | "publishedAt" | Sort field. |
order |
"asc" | "desc" | "desc" | Sort direction. |
rootMargin |
string | "400px" | IntersectionObserver margin — larger = fetch the next page earlier. |
prefetchOnHover |
boolean | — | Prefetch post details when the card is hovered. |
renderItem |
(post: Post) => ReactNode | — | Custom renderer per post (replaces the default BlogCard). |
renderLoading / renderEmpty / renderError |
Slot | — | Override loading / empty / error states. |
imageComponent |
VloziImageComponent | — | Custom image component (e.g. next/image) forwarded to every card. |
className |
string | — | Extra class for the root container. |
BlogArchive
Lists every published post, grouped by year and month. Good for /blog/archive pages. Paginates through the entire post list client-side — suitable for blogs up to ~1000 posts.
import { BlogArchive } from "@vlozi/blog/react";
<BlogArchive
variant="grouped"
postHref={(p) => `/blog/${p.slug}`}
/>Props
| Prop | Type | Default | Description |
|---|---|---|---|
variant |
"grouped" | "flat" | "grouped" | Show year/month headings (grouped) or one continuous list (flat). |
maxPosts |
number | 1000 | Upper bound on posts to include. |
postHref |
(post: Post) => string | /blog/{slug} | Produce the link for each post. |
prefetchOnHover |
boolean | false | Prefetch the full post when an archive link is hovered. Requires a mounted VloziProvider. |
renderGroup |
(group: ArchiveGroup) => ReactNode | — | Custom renderer for each year/month group's body. |
renderLoading / renderEmpty / renderError |
Slot | — | Override loading / empty / error states. |
className |
string | — | Extra class for the root container. |
RelatedPosts
Renders a block of related posts based on category/tag overlap with the current slug. Drop it at the bottom of a post page. Fail-silent by default — on fetch error or when no related posts are found, the block disappears.
import { RelatedPosts } from "@vlozi/blog/react";
<RelatedPosts slug={post.slug} limit={3} columns={3} />Props
| Prop | Type | Default | Description |
|---|---|---|---|
slug |
string | required | Slug of the current post. Related posts are scored against this post's category + tags. |
limit |
number | 3 | Max related posts to show. |
pool |
number | 100 | Pool of recent posts to score against. |
columns |
1 | 2 | 3 | 4 | 3 | Grid column count. |
heading |
ReactNode | "Related posts" | Section heading. Pass "" or null to hide. |
renderItem |
(post: Post) => ReactNode | — | Custom renderer per post. |
prefetchOnHover |
boolean | — | Prefetch post details on hover. |
imageComponent |
VloziImageComponent | — | Custom image component forwarded to each card. |
className |
string | — | Extra class for the root section. |
PrevNextNav
Renders previous / next navigation for a single post, in publication order. Either side may be absent if the current post is the oldest or newest.
import { PrevNextNav } from "@vlozi/blog/react";
<PrevNextNav slug={post.slug} />Props
| Prop | Type | Default | Description |
|---|---|---|---|
slug |
string | required | Slug of the current post. |
pool |
number | 500 | How many recent posts to search when locating neighbors. |
postHref |
(post: Post) => string | /blog/{slug} | Produce the link for each neighbor. |
renderLink |
(post: Post, direction: "previous" | "next") => ReactNode | — | Custom renderer for each neighbor link. |
hideWhenEmpty |
boolean | true | Hide the nav when both neighbors are null. |
className |
string | — | Extra class for the root nav. |
BlogContent
The most important component for custom post pages. Renders the post's sanitized HTML body with prose styling, hydrates rich blocks (mermaid diagrams, carousels, syntax-highlighted code), and optionally upgrades inline body images via your custom image component.
Use this whenever you build your own post layout (instead of the all-in-one BlogPost). Pass post.content as html:
import { BlogContent } from "@vlozi/blog/react";
<article className="my-post-layout">
<header>
<h1>{post.title}</h1>
{post.excerpt && <p>{post.excerpt}</p>}
</header>
{/* Body — sanitized + hydrated + syntax-highlighted */}
{post.content && <BlogContent html={post.content} />}
</article>Props
| Prop | Type | Default | Description |
|---|---|---|---|
html |
string | required | The post's HTML body — typically post.content from client.blog.get(). |
className |
string | — | Extra classes merged with the default .vlz-content wrapper. |
imageComponent |
VloziImageComponent | — | Custom component used to upgrade every inline body <img> tag (e.g. Next.js Image). The original <img> stays in the rendered HTML pre-hydration (good for SEO + no-JS); React replaces it on mount via portal. |
transformHtml |
(html: string) => string | — | Run a transform on the raw HTML before sanitization. Escape hatch for patching server-side bugs without forking. Output is still sanitized — not an XSS escape. |
Inline body image upgrade
Pass imageComponent to upgrade every <img> in the post body to your custom component (e.g. Next.js Image for automatic format conversion + responsive sources). Skipped for images inside hydrated mermaid/carousel hosts, inside <picture> elements, or marked data-vlz-skip-hydrate.
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} />transformHtml escape hatch
Sometimes the server-rendered HTML has bugs the SDK can't fix on its own. transformHtml lets you patch the HTML before it's rendered, without forking the SDK. Common use: rewriting URLs, fixing class names, stripping unwanted markers.
<BlogContent
html={post.content}
transformHtml={(html) =>
html.replace(
/youtube\.com\/watch\?v=([\w-]+)/g,
'youtube-nocookie.com/embed/$1',
)
}
/>IMPORTANT
The transform runs before sanitization. Anything it produces is still subject to the SDK's defense-in-depth sanitizer — script injections still get stripped. transformHtml is not an XSS escape hatch.
Skeleton primitives
Loading skeletons used internally by BlogPost and BlogList are exported for consumers building custom UIs. Use them when you want the SDK's exact loading visuals without writing your own.
import {
BlogPostSkeleton,
BlogCardSkeleton,
BlogListSkeleton,
} from "@vlozi/blog/react";
// Article-page skeleton — banner + title + meta + 5 body lines
if (loading) return <BlogPostSkeleton />;
// Single card skeleton — image + meta + title + 2 excerpt lines
<BlogCardSkeleton />
// Grid of card skeletons sized to match a BlogList(columns={N})
<BlogListSkeleton columns={3} count={6} />Props
| Component | Props |
|---|---|
BlogPostSkeleton |
className?: string |
BlogCardSkeleton |
className?: string |
BlogListSkeleton |
columns?: 1 | 2 | 3 | 4 (default 3), count?: number (default columns × 2), className?: string |
MermaidBlock and Carousel (standalone)
The mermaid + carousel components are also exported for direct use outside post bodies — for example, when you have a static diagram to render somewhere else in your app.
import { MermaidBlock, Carousel, type CarouselSlide } from "@vlozi/blog/react";
<MermaidBlock source="graph TD; A-->B; B-->C" theme="default" />
const slides: CarouselSlide[] = [
{ src: "/a.jpg", alt: "First", caption: "Optional caption" },
{ src: "/b.jpg", alt: "Second" },
];
<Carousel slides={slides} />Customizing styles
All components accept a className prop that merges with the default styles. The SDK's prose stylesheet derives every color from five --vlz-* custom properties — set these once on .vlz-content to theme the entire blog without writing rule-by-rule overrides.
/* Map your design tokens onto the SDK's --vlz-* knobs */
.vlz-content {
--vlz-accent: var(--my-primary);
--vlz-muted-fg: var(--my-muted-foreground);
--vlz-border: var(--my-border);
--vlz-surface: var(--my-surface);
--vlz-surface-hover: var(--my-surface-hover);
}<BlogList className="gap-8 my-10" />
<BlogCategoryNav className="mb-6" />