@wrytze/react
v0.2.1
Published
React components for displaying Wrytze blog content
Maintainers
Readme
@wrytze/react
React components for displaying Wrytze blog content.
Features
- 20+ pre-built components -- blog cards, lists, pagination, search, filtering, table of contents, and more
- React hooks for data fetching --
useBlogs,useBlog,useCategories,useTagswith loading and error states - Next.js adapter -- automatic
next/imageoptimization and URL-synced search/pagination - SEO metadata helpers -- generate Next.js Metadata objects and JSON-LD structured data
- Tailwind CSS styling -- all components use Tailwind classes with a
cn()utility for overrides - TypeScript-first -- full type definitions for all components, hooks, and helpers
Installation
npm install @wrytze/react @wrytze/sdkpnpm add @wrytze/react @wrytze/sdkyarn add @wrytze/react @wrytze/sdk@wrytze/sdk is a required dependency -- it provides the WrytzeClient used to fetch data from the Wrytze API.
Quick Start
import { WrytzeClient } from '@wrytze/sdk'
import { WrytzeProvider, useBlogs, BlogCard } from '@wrytze/react'
const client = new WrytzeClient({
apiKey: 'wrz_sk_...',
websiteId: 'your-website-id',
})
function BlogList() {
const { data: blogs, isLoading, error } = useBlogs(client)
if (isLoading) return <p>Loading...</p>
if (error) return <p>Error: {error.message}</p>
return (
<div className="grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-3">
{blogs?.map((blog) => (
<BlogCard key={blog.id} blog={blog} basePath="/blog" />
))}
</div>
)
}
export default function App() {
return (
<WrytzeProvider client={client}>
<BlogList />
</WrytzeProvider>
)
}Entry Points
| Import | Contents | When to use |
| -------------------------- | ---------------------------------------------------- | ------------------------------------------ |
| @wrytze/react | Core components, hooks, context provider, utilities | Any React app (Vite, Remix, etc.) |
| @wrytze/react/next | Next.js-optimized overrides + page templates | Next.js apps (uses next/image, next/navigation) |
| @wrytze/react/metadata | generateBlogListMetadata, generateBlogPostMetadata | Next.js server components for SEO metadata |
The @wrytze/react and @wrytze/react/next entry points include a "use client" banner. The @wrytze/react/metadata entry point does not include one, so it is safe to use in server components.
Hooks
All hooks take a WrytzeClient instance as the first argument and return reactive state with isLoading and error fields.
useBlogs
Fetches a paginated list of blogs.
import { WrytzeClient } from '@wrytze/sdk'
import { useBlogs } from '@wrytze/react'
function RecentPosts({ client }: { client: WrytzeClient }) {
const { data, pagination, error, isLoading } = useBlogs(client, {
page: 1,
limit: 10,
category: 'engineering',
})
if (isLoading) return <p>Loading...</p>
if (error) return <p>{error.message}</p>
return (
<div>
{data?.map((blog) => <p key={blog.id}>{blog.title}</p>)}
<p>Page {pagination?.page} of {pagination?.pages}</p>
</div>
)
}Parameters: client: WrytzeClient, params?: ListBlogsParams (page, limit, category, tag, search, websiteId)
Returns: { data: Blog[] | null, pagination: Pagination | null, error: WrytzeError | null, isLoading: boolean }
useBlog
Fetches a single blog by ID or slug.
import { useBlog } from '@wrytze/react'
// By ID
const { data, error, isLoading } = useBlog(client, { id: '550e8400-...' })
// By slug
const { data, error, isLoading } = useBlog(client, { slug: 'my-first-post' })Parameters: client: WrytzeClient, identifier: { id: string } | { slug: string }
Returns: { data: BlogDetail | null, error: WrytzeError | null, isLoading: boolean }
useCategories
Fetches all categories.
import { useCategories } from '@wrytze/react'
const { data: categories, error, isLoading } = useCategories(client)Parameters: client: WrytzeClient, params?: ListResourceParams (websiteId)
Returns: { data: Category[] | null, error: WrytzeError | null, isLoading: boolean }
useTags
Fetches all tags.
import { useTags } from '@wrytze/react'
const { data: tags, error, isLoading } = useTags(client)Parameters: client: WrytzeClient, params?: ListResourceParams (websiteId)
Returns: { data: Tag[] | null, error: WrytzeError | null, isLoading: boolean }
Components
All components accept a className prop for styling overrides via the cn() utility (clsx + tailwind-merge).
Layout
| Component | Description |
| -------------- | ----------------------------------------------------------------------------------------------- |
| BlogList | Full blog listing with search, category filter, card grid, and pagination. Works in data mode (pass blogs + pagination) or client mode (pass a WrytzeClient). |
| BlogHeader | Blog post header with featured image, categories, and title. |
| BlogArticle | Complete blog post view with header, meta, content, and tags. Supports data mode or client mode. |
Cards
| Component | Description |
| ---------- | ------------------------------------------------------------------------------------------- |
| BlogCard | Blog preview card with featured image, category badges, title, excerpt, and author footer. |
Content
| Component | Description |
| ---------------- | ------------------------------------------------------ |
| BlogContent | Renders blog contentHtml inside a prose container. |
| BlogImage | Responsive image with aspect ratio. Accepts an optional imageComponent prop for custom image renderers. |
| BlogMeta | Displays published date, reading time, and word count. |
| BlogCategories | Renders category links as badges. |
| BlogTags | Renders tag links as badges. |
Navigation
| Component | Description |
| ---------------- | --------------------------------------------------------- |
| BlogPagination | Page controls driven by a Pagination object. |
| BlogSearch | Search input with debounced onSearch callback. |
| BlogFilter | Category filter bar with active state highlighting. |
Extras
| Component | Description |
| ----------------- | ----------------------------------------------------------------------------- |
| TableOfContents | Sticky sidebar TOC generated from blog.tableOfContents. |
| ReadingProgress | Top-of-page progress bar that tracks scroll position. |
| ShareButtons | Social sharing buttons (supports vertical layout via vertical prop). |
| RelatedPosts | Grid of related blog cards from blog.relatedBlogs. |
| AuthorCard | Author avatar, name, publish date, reading time, and word count. |
| PostNavigation | Previous/next post links at the bottom of a blog post. |
State
| Component | Description |
| -------------- | ----------------------------------------------------------------------- |
| BlogSkeleton | Loading skeleton with variant prop ("list" or "article"). |
| BlogError | Error display with an optional onRetry callback. |
Next.js Integration
Using the Next.js adapter
Import from @wrytze/react/next instead of @wrytze/react. This gives you the same components, but with three Next.js-optimized overrides:
BlogImage-- automatically usesnext/imagefor optimizationBlogList-- syncs search, category, and page state to URL search paramsBlogSearch-- reads and writes thesearchquery parameter
// app/blog/page.tsx
import { WrytzeClient } from '@wrytze/sdk'
import { BlogListPage } from '@wrytze/react/next'
const client = new WrytzeClient({
apiKey: process.env.WRYTZE_API_KEY!,
websiteId: process.env.WRYTZE_WEBSITE_ID!,
})
export default async function BlogPage({
searchParams,
}: {
searchParams: Promise<{ page?: string; category?: string; search?: string }>
}) {
const params = await searchParams
const { data: blogs, pagination } = await client.blogs.list({
page: params.page ? Number(params.page) : 1,
category: params.category,
search: params.search,
})
const { data: categories } = await client.categories.list()
return (
<BlogListPage
blogs={blogs}
pagination={pagination}
categories={categories}
basePath="/blog"
title="Our Blog"
description="Insights, tutorials, and updates."
/>
)
}Page templates
The /next entry point includes two page-level templates that compose multiple components:
BlogListPage -- Full blog listing page with header, category filter, search, card grid, pagination, and empty state.
BlogPostPage -- Full blog post page with reading progress bar, featured image, categories, title, excerpt, author card, share buttons, table of contents sidebar, content, tags, related posts, and previous/next navigation.
// app/blog/[slug]/page.tsx
import { WrytzeClient } from '@wrytze/sdk'
import { BlogPostPage } from '@wrytze/react/next'
const client = new WrytzeClient({
apiKey: process.env.WRYTZE_API_KEY!,
websiteId: process.env.WRYTZE_WEBSITE_ID!,
})
export default async function PostPage({
params,
}: {
params: Promise<{ slug: string }>
}) {
const { slug } = await params
const { data: blog } = await client.blogs.getBySlug(slug)
return (
<BlogPostPage
blog={blog}
basePath="/blog"
prev={{ title: 'Previous Post', slug: 'previous-post' }}
next={{ title: 'Next Post', slug: 'next-post' }}
/>
)
}SEO metadata
Import from @wrytze/react/metadata for server-safe metadata generation (no "use client" banner).
// app/blog/layout.tsx
import { generateBlogListMetadata } from '@wrytze/react/metadata'
export const metadata = generateBlogListMetadata({
title: 'Blog',
description: 'Browse our latest blog posts',
baseUrl: 'https://example.com/blog',
})// app/blog/[slug]/page.tsx
import type { Metadata } from 'next'
import { WrytzeClient } from '@wrytze/sdk'
import { generateBlogPostMetadata } from '@wrytze/react/metadata'
const client = new WrytzeClient({
apiKey: process.env.WRYTZE_API_KEY!,
websiteId: process.env.WRYTZE_WEBSITE_ID!,
})
export async function generateMetadata({
params,
}: {
params: Promise<{ slug: string }>
}): Promise<Metadata> {
const { slug } = await params
const { data: blog } = await client.blogs.getBySlug(slug)
return generateBlogPostMetadata(blog, {
siteName: 'My Site',
baseUrl: 'https://example.com',
})
}generateBlogPostMetadata returns OpenGraph tags, Twitter card tags, and JSON-LD Article structured data.
Utilities
| Export | Description |
| -------------- | -------------------------------------------------------------------- |
| cn(...inputs) | Merges class names using clsx + tailwind-merge. |
| formatDate(dateString) | Formats an ISO date string to "January 1, 2026" format. |
| formatNumber(num) | Formats a number with locale-aware separators (1,234). |
Documentation
Full API documentation, integration guides, and code templates are available at docs.wrytze.com.
License
MIT
