For complete control over the UI, use the React hooks to fetch data and build your own components. Import them from @vlozi/blog/react.
usePosts
Fetches a paginated list of blog posts with optional filtering, searching, and sorting.
const {
data, // PaginatedResponse<Post> | null
loading, // boolean
error, // Error | null
refetch, // () => Promise<void>
page, // current page number
totalPages, // total number of pages
hasNextPage, // boolean
hasPrevPage, // boolean
} = usePosts({
page: 1,
limit: 10,
category: "tutorials", // filter by category slug
tag: "react", // filter by tag slug
search: "getting started", // search title & excerpt
sort: "publishedAt", // "publishedAt" | "title" | "createdAt"
order: "desc", // "asc" | "desc"
});Parameters
All parameters are optional:
| Param | Type | Default | Description |
|---|---|---|---|
page |
number | 1 | Page number for pagination. |
limit |
number | 10 | Items per page (max 100). |
category |
string | — | Filter by category slug. |
tag |
string | — | Filter by tag slug. |
search |
string | — | Search title and excerpt (case-insensitive). |
sort |
"publishedAt" | "title" | "createdAt" | "publishedAt" | Field to sort by. |
order |
"asc" | "desc" | "desc" | Sort direction. |
usePost
Fetches a single post by its slug, including full HTML content. Useful for detail pages.
const { data: post, loading, error } = usePost("my-first-post");
// post.title, post.content (HTML), post.category, post.tags, etc.useCategories
Fetches all categories with their published post counts. Categories are returned in a flat array (not paginated).
const { data: categories, loading, error, refetch } = useCategories();
// categories = [{ name: "Tutorials", slug: "tutorials", postCount: 12 }, ...]useTags
Fetches all tags with their published post counts. Tags are returned in a flat array (not paginated).
const { data: tags, loading, error, refetch } = useTags();
// tags = [{ name: "React", slug: "react", postCount: 8 }, ...]useArchive
Fetches every published post, grouped by year and month. Paginates through the post list client-side — suitable for blogs up to ~1000 posts.
const { data, loading, error, refetch } = useArchive({ maxPosts: 1000 });
// data = {
// groups: [
// { year: 2026, month: 4, monthName: "April", posts: [...], count: 5 },
// { year: 2026, month: 3, monthName: "March", posts: [...], count: 8 },
// ...
// ],
// totalPosts: 137,
// }Parameters
| Param | Type | Default | Description |
|---|---|---|---|
maxPosts |
number | 1000 | Upper bound on posts to include across all pages. |
useRelatedPosts
Finds posts related to a given slug by category/tag overlap scoring. Perfect for a "You might also like…" block at the bottom of a post page.
const { data: related, loading, error } = useRelatedPosts(post.slug, {
limit: 3, // how many related posts to return
pool: 100, // how many recent posts to score against
});
// related = Post[] — already sorted by relevance.Parameters
| Param | Type | Default | Description |
|---|---|---|---|
slug |
string | required | Slug of the current post. Scoring is done relative to this post's category + tags. |
options.limit |
number | 3 | Max related posts to return. |
options.pool |
number | 100 | Pool of recent posts to score against. |
useNeighbors
Returns the previous (older) and next (newer) posts adjacent to a given slug in publication order. Either side may be null when the current post is the oldest or newest.
const { data, loading } = useNeighbors(post.slug, { pool: 500 });
// data = {
// previous: { slug, title, publishedAt, ... } | null,
// next: { slug, title, publishedAt, ... } | null,
// }Parameters
| Param | Type | Default | Description |
|---|---|---|---|
slug |
string | required | Slug of the current post. |
options.pool |
number | 500 | How many recent posts to fetch when searching for neighbors. |
useVlozi (advanced)
Returns the underlying VloziClient from context. Use this when the built-in hooks don't cover your use case — e.g. calling a client method imperatively, prefetching on hover, or wiring the client into your own data layer.
import { useVlozi } from "@vlozi/blog/react";
function PrefetchOnHover({ slug, children }: { slug: string; children: ReactNode }) {
const client = useVlozi();
return (
<div onMouseEnter={() => client.blog.get(slug)}>
{children}
</div>
);
}Must be called inside a <VloziProvider> subtree — throws a clear error otherwise.
Combined example: blog with filters
A practical example combining multiple hooks to build a filterable blog page:
"use client";
import { useState } from "react";
import { usePosts, useCategories, useTags } from "@vlozi/blog/react";
export default function BlogPage() {
const [category, setCategory] = useState<string>();
const [tag, setTag] = useState<string>();
const [search, setSearch] = useState("");
const { data: categories } = useCategories();
const { data: tags } = useTags();
const { data, loading } = usePosts({ category, tag, search: search || undefined });
return (
<div>
<input
placeholder="Search posts…"
value={search}
onChange={(e) => setSearch(e.target.value)}
/>
{/* Category pills */}
{categories?.map((cat) => (
<button key={cat.slug} onClick={() => setCategory(cat.slug)}>
{cat.name} ({cat.postCount})
</button>
))}
{/* Tag pills */}
{tags?.map((t) => (
<button key={t.slug} onClick={() => setTag(t.slug)}>
#{t.name}
</button>
))}
{/* Post list */}
{loading ? <p>Loading…</p> : (
data?.data.map((post) => (
<article key={post.slug}>
<h2>{post.title}</h2>
{post.category && <span>{post.category.name}</span>}
{post.tags?.map((t) => <span key={t.slug}>#{t.name}</span>)}
<p>{post.excerpt}</p>
</article>
))
)}
</div>
);
}Headless usage
These hooks are "headless" — they don't render any markup. They simply provide the data and state management, giving you the freedom to design the UI exactly how you want it.