React SDK

Components

Drop-in React components for lists, cards, navigation, and post bodies.

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.

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

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" />
Blog · React SDKEdit on GitHub