React Components
Pre-built React components for displaying Wrytze blog content with dark mode, loading states, and Next.js optimization.
@wrytze/react provides 20 ready-to-use components for blog listing and article pages, plus 2 page-level components that render complete blog pages with a single import. Components support both data mode (you fetch, you render) and client mode (components fetch automatically). All components ship with dark mode, animated loading skeletons, and accessible markup.
Three entry points are available:
@wrytze/react— pure React, works in any React 18+ project@wrytze/react/next— same API, but swaps innext/imagefor optimized images, syncs page/category/search state to the URL, and adds page-level components (BlogListPage,BlogPostPage)@wrytze/react/metadata— server-safe metadata helpers for Next.js (generateBlogListMetadata,generateBlogPostMetadata)
Installation
npm install @wrytze/reactPeer dependencies
| Package | Version | Required |
|---|---|---|
react | >= 18 | Yes |
react-dom | >= 18 | Yes |
@wrytze/sdk | any | Yes |
next | >= 14 | Only for @wrytze/react/next |
Quick start
Server component (data mode)
Fetch data on the server and pass it directly to BlogList. No client-side JavaScript is required for the initial render.
// app/blog/page.tsx
import { WrytzeClient } from "@wrytze/sdk";
import { BlogList } from "@wrytze/react";
const wrytze = new WrytzeClient({
apiKey: process.env.WRYTZE_API_KEY!,
websiteId: process.env.WRYTZE_WEBSITE_ID,
});
export default async function BlogPage() {
const { data: blogs, pagination } = await wrytze.blogs.list({ limit: 9 });
const { data: categories } = await wrytze.categories.list();
return (
<main className="mx-auto max-w-6xl px-4 py-12">
<h1 className="mb-8 text-4xl font-bold">Blog</h1>
<BlogList blogs={blogs} pagination={pagination} categories={categories} />
</main>
);
}Client component (client mode)
Wrap your tree with WrytzeProvider and let BlogList fetch its own data. Useful when you need interactivity without a server component.
// app/blog/page.tsx
"use client";
import { WrytzeClient } from "@wrytze/sdk";
import { WrytzeProvider, BlogList } from "@wrytze/react";
const client = new WrytzeClient({
apiKey: process.env.NEXT_PUBLIC_WRYTZE_API_KEY!,
websiteId: process.env.NEXT_PUBLIC_WRYTZE_WEBSITE_ID,
});
export default function BlogPage() {
return (
<WrytzeProvider client={client}>
<main className="mx-auto max-w-6xl px-4 py-12">
<h1 className="mb-8 text-4xl font-bold">Blog</h1>
<BlogList />
</main>
</WrytzeProvider>
);
}In client mode the API key is visible in the browser. Only use client mode with a public read-only API key or a backend proxy. The server component pattern above keeps the key server-side.
WrytzeProvider
WrytzeProvider makes a WrytzeClient available to all descendant components via React context. It is only required when using client mode components.
import { WrytzeProvider } from "@wrytze/react";
import { WrytzeClient } from "@wrytze/sdk";
const client = new WrytzeClient({ apiKey: "..." });
<WrytzeProvider client={client}>
{/* BlogList, BlogArticle, hooks — all work here */}
</WrytzeProvider>Props
| Prop | Type | Required | Description |
|---|---|---|---|
client | WrytzeClient | Yes | The SDK client instance to provide to children |
children | ReactNode | Yes | Component tree that will have access to the client |
Context hooks
Two hooks read the client from context. Import them from @wrytze/react:
import { useWrytzeClient, useRequiredWrytzeClient } from "@wrytze/react";useWrytzeClient(): WrytzeClient | null
Returns the client if a WrytzeProvider is present in the tree, or null if not. Use this when the client is optional.
useRequiredWrytzeClient(): WrytzeClient
Returns the client or throws if called outside a WrytzeProvider. Use this inside custom components that always require a client.
Page-level components
These components render an entire page layout — header, filters, content, and pagination — with a single import. Available from @wrytze/react/next only.
BlogListPage
Renders a complete blog listing page with a page title, category filters, search input, responsive card grid, empty state, and pagination. Syncs all filter state to URL search params.
import { BlogListPage } from "@wrytze/react/next";
<BlogListPage
blogs={blogs}
pagination={pagination}
categories={categories}
title="Our Blog"
description="Thoughts, guides, and updates from our team."
/>Props
| Prop | Type | Default | Description |
|---|---|---|---|
blogs | Blog[] | — | Array of blogs to render |
pagination | Pagination | — | Pagination metadata |
categories | { name: string; slug: string }[] | — | Categories for the filter bar |
basePath | string | "/blog" | URL prefix for blog card links |
title | string | "Blog" | Page heading rendered as <h1> |
description | string | — | Optional subtitle below the heading |
className | string | — | Additional CSS classes on the root element |
BlogListPage reads ?page=, ?category=, and ?search= from the URL on mount and pushes updates when the user interacts with filters or pagination. Wrap it in a <Suspense> boundary when used inside a server component page.
BlogPostPage
Renders a complete blog post page with a reading progress bar, featured image, categories, title, excerpt, author card, table of contents sidebar, HTML content, tags, share buttons, related posts, and previous/next navigation.
import { BlogPostPage } from "@wrytze/react/next";
<BlogPostPage
blog={blogDetail}
prev={{ title: "Previous Post", slug: "previous-post" }}
next={{ title: "Next Post", slug: "next-post" }}
/>Props
| Prop | Type | Default | Description |
|---|---|---|---|
blog | BlogDetail | — | Full blog post object (must include contentHtml and tableOfContents) |
basePath | string | "/blog" | URL prefix for category, tag, and navigation links |
prev | { title: string; slug: string } | null | — | Previous post for navigation |
next | { title: string; slug: string } | null | — | Next post for navigation |
className | string | — | Additional CSS classes on the root element |
The page layout includes:
- Reading progress bar fixed at the top of the viewport
- Featured image with 2:1 aspect ratio and
next/imageoptimization - Share buttons positioned to the right of the title on desktop, inline on mobile
- Table of contents sidebar on desktop (left of content) when
tableOfContentshas entries - Related posts grid rendered when
blog.relatedBlogshas entries - Previous/Next navigation rendered when
prevornextis provided
Hooks
All four hooks are client-side only ("use client"). They handle cancellation on unmount and deduplicate requests when params have not changed.
useBlogs
Fetches a paginated list of blogs.
import { useBlogs } from "@wrytze/react";
const { data, pagination, error, isLoading } = useBlogs(client, params);Signature
function useBlogs(
client: WrytzeClient,
params?: ListBlogsParams
): UseBlogsResultParameters
| Parameter | Type | Description |
|---|---|---|
client | WrytzeClient | An initialized SDK client |
params | ListBlogsParams | Optional filter/pagination params (see below) |
ListBlogsParams fields — all optional:
| Field | Type | Default | Description |
|---|---|---|---|
page | number | 1 | Page number |
limit | number | 20 | Results per page (max 100) |
category | string | — | Filter by category slug |
tag | string | — | Filter by tag slug |
search | string | — | Search by title (case-insensitive) |
websiteId | string | — | Scope to a specific website |
Return type
| Field | Type | Description |
|---|---|---|
data | Blog[] | null | Blog list, or null while loading |
pagination | Pagination | null | Pagination metadata, or null while loading |
error | WrytzeError | null | Error if the request failed |
isLoading | boolean | true during the initial fetch |
useBlog
Fetches a single blog post by ID or slug.
import { useBlog } from "@wrytze/react";
const { data, error, isLoading } = useBlog(client, { slug: "my-post" });
// or
const { data, error, isLoading } = useBlog(client, { id: "cm5abc123" });Signature
type BlogIdentifier = { id: string } | { slug: string }
function useBlog(
client: WrytzeClient,
identifier: BlogIdentifier
): UseBlogResultParameters
| Parameter | Type | Description |
|---|---|---|
client | WrytzeClient | An initialized SDK client |
identifier | BlogIdentifier | Either { id: string } or { slug: string } |
Return type
| Field | Type | Description |
|---|---|---|
data | BlogDetail | null | Full blog post with contentHtml, or null while loading |
error | WrytzeError | null | Error if the request failed |
isLoading | boolean | true during the initial fetch |
useCategories
Fetches all categories for the organization.
import { useCategories } from "@wrytze/react";
const { data, error, isLoading } = useCategories(client);Signature
function useCategories(
client: WrytzeClient,
params?: ListResourceParams
): UseCategoriesResultParameters
| Parameter | Type | Description |
|---|---|---|
client | WrytzeClient | An initialized SDK client |
params | ListResourceParams | Optional { websiteId?: string } to scope results |
Return type
| Field | Type | Description |
|---|---|---|
data | Category[] | null | Category list, or null while loading |
error | WrytzeError | null | Error if the request failed |
isLoading | boolean | true during the initial fetch |
useTags
Fetches all tags for the organization.
import { useTags } from "@wrytze/react";
const { data, error, isLoading } = useTags(client);Signature
function useTags(
client: WrytzeClient,
params?: ListResourceParams
): UseTagsResultParameters
| Parameter | Type | Description |
|---|---|---|
client | WrytzeClient | An initialized SDK client |
params | ListResourceParams | Optional { websiteId?: string } to scope results |
Return type
| Field | Type | Description |
|---|---|---|
data | Tag[] | null | Tag list, or null while loading |
error | WrytzeError | null | Error if the request failed |
isLoading | boolean | true during the initial fetch |
Composite components
These two components are the primary building blocks. Each supports both data mode and client mode.
BlogList
Renders a filter bar, search input, a responsive blog card grid, and pagination controls.
import { BlogList } from "@wrytze/react";
// Data mode — you own the data
<BlogList blogs={blogs} pagination={pagination} categories={categories} />
// Client mode — component fetches automatically
<BlogList websiteId="ws_abc123" />Props
BlogList accepts either data mode props or client mode props, plus shared props.
Data mode props (mutually exclusive with client mode):
| Prop | Type | Required | Description |
|---|---|---|---|
blogs | Blog[] | Yes | Array of blogs to render |
pagination | Pagination | Yes | Pagination metadata for the current page |
categories | { name: string; slug: string }[] | No | Categories for the filter bar |
Client mode props (mutually exclusive with data mode):
| Prop | Type | Required | Description |
|---|---|---|---|
client | WrytzeClient | No | Client instance; falls back to WrytzeProvider context |
websiteId | string | No | Website ID to scope fetched content |
Shared props (available in both modes):
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | — | Additional CSS classes on the root element |
basePath | string | "/blog" | URL prefix for blog card links (e.g. "/articles") |
imageComponent | ComponentType | — | Custom image component (see image customization) |
onPageChange | (page: number) => void | — | Called when the user navigates to a new page |
onCategoryChange | (category: string | undefined) => void | — | Called when the user selects a category filter |
onSearch | (query: string) => void | — | Called 400 ms after the user stops typing in the search box |
In client mode, BlogList manages page, category, and search state internally. Providing onPageChange, onCategoryChange, or onSearch callbacks lets you react to those changes externally (e.g. to sync URL params manually when not using the /next adapter).
BlogArticle
Renders a full blog post: header image, category badges, title, publication meta, HTML content, and tag list.
import { BlogArticle } from "@wrytze/react";
// Data mode
<BlogArticle blog={blogDetail} />
// Client mode — fetch by slug
<BlogArticle slug="building-a-modern-blog" />
// Client mode — fetch by ID
<BlogArticle id="cm5abc123def456" client={client} />Props
Data mode props (mutually exclusive with client mode):
| Prop | Type | Required | Description |
|---|---|---|---|
blog | BlogDetail | Yes | Full blog post object (must include contentHtml) |
Client mode props (mutually exclusive with data mode):
| Prop | Type | Required | Description |
|---|---|---|---|
client | WrytzeClient | No | Client instance; falls back to WrytzeProvider context |
slug | string | No | Blog slug to fetch (one of slug or id is required) |
id | string | No | Blog ID to fetch (one of slug or id is required) |
Shared props (available in both modes):
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | — | Additional CSS classes on the root <article> element |
basePath | string | "/blog" | URL prefix used for category and tag links |
imageComponent | ComponentType | — | Custom image component for the featured image |
Presentational components
These components have no data-fetching logic. They render the data you pass them and accept className for styling overrides.
BlogCard
A card component for a single blog post in a grid layout. Renders the featured image, category badges, title, excerpt, publish date, and reading time.
import { BlogCard } from "@wrytze/react";
<BlogCard blog={blog} basePath="/blog" />| Prop | Type | Default | Description |
|---|---|---|---|
blog | Blog | — | Blog object from the list API |
basePath | string | "/blog" | URL prefix for the card link |
className | string | — | Additional CSS classes |
imageComponent | ComponentType | — | Custom image component |
BlogHeader
Renders the top section of an article: featured image, category badge links, and the <h1> title.
import { BlogHeader } from "@wrytze/react";
<BlogHeader
title={blog.title}
featuredImageUrl={blog.featuredImageUrl}
featuredImageAlt={blog.featuredImageAlt}
categories={blog.categories}
/>| Prop | Type | Default | Description |
|---|---|---|---|
title | string | — | The blog post title, rendered as <h1> |
featuredImageUrl | string | null | — | URL of the featured image; omit to hide the image |
featuredImageAlt | string | null | — | Alt text for the featured image |
categories | { name: string; slug: string }[] | — | Category badges shown above the title |
basePath | string | "/blog" | URL prefix for category links |
className | string | — | Additional CSS classes on the <header> element |
imageComponent | ComponentType | — | Custom image component |
BlogImage
A wrapper around either <img> or a custom image component (e.g. next/image). Adds aspect-ratio container and rounded corners.
import { BlogImage } from "@wrytze/react";
<BlogImage
src="https://cdn.example.com/cover.jpg"
alt="Cover image"
aspectRatio="aspect-[16/9]"
priority
/>| Prop | Type | Default | Description |
|---|---|---|---|
src | string | — | Image URL |
alt | string | — | Alt text |
className | string | — | Additional CSS classes on the container element |
aspectRatio | string | "aspect-[2/1]" | Tailwind aspect-ratio class (e.g. "aspect-[16/9]") |
priority | boolean | false | When true, skips lazy loading (use for above-the-fold images) |
sizes | string | "(max-width: 768px) 100vw, (max-width: 1200px) 66vw, 800px" | Responsive sizes hint passed to the image component |
imageComponent | ComponentType | — | Custom image component |
BlogContent
Renders blog HTML content inside a Tailwind Typography prose container. The Wrytze API returns pre-sanitized HTML, so it is safe to inject directly.
import { BlogContent } from "@wrytze/react";
<BlogContent contentHtml={blog.contentHtml} />| Prop | Type | Default | Description |
|---|---|---|---|
contentHtml | string | — | Sanitized HTML string from the Wrytze API |
className | string | — | Additional CSS classes merged into the prose container |
If you pass HTML from a source other than the Wrytze API, sanitize it first with a library like DOMPurify before passing it to BlogContent.
BlogMeta
Displays publication date, reading time, and word count as an inline row of metadata.
import { BlogMeta } from "@wrytze/react";
<BlogMeta
publishedAt={blog.publishedAt}
readingTimeMinutes={blog.readingTimeMinutes}
wordCount={blog.wordCount}
/>| Prop | Type | Default | Description |
|---|---|---|---|
publishedAt | string | — | ISO 8601 timestamp string |
readingTimeMinutes | number | — | Estimated reading time in minutes |
wordCount | number | — | Total word count |
className | string | — | Additional CSS classes |
BlogCategories
Renders a row of pill-shaped category links that filter the blog list.
import { BlogCategories } from "@wrytze/react";
<BlogCategories categories={blog.categories} basePath="/blog" />| Prop | Type | Default | Description |
|---|---|---|---|
categories | { name: string; slug: string }[] | — | Categories to render |
basePath | string | "/blog" | URL prefix; each link goes to {basePath}?category={slug} |
className | string | — | Additional CSS classes |
Returns null when the categories array is empty.
BlogTags
Renders a row of tag links prefixed with #.
import { BlogTags } from "@wrytze/react";
<BlogTags tags={blog.tags} basePath="/blog" />| Prop | Type | Default | Description |
|---|---|---|---|
tags | { name: string; slug: string }[] | — | Tags to render |
basePath | string | "/blog" | URL prefix; each link goes to {basePath}?tag={slug} |
className | string | — | Additional CSS classes on the container |
Returns null when the tags array is empty.
BlogPagination
A pagination control with Previous/Next buttons and numbered page buttons. Collapses to a "Page X of Y" label on small screens.
import { BlogPagination } from "@wrytze/react";
<BlogPagination
pagination={pagination}
onPageChange={(page) => setCurrentPage(page)}
/>| Prop | Type | Default | Description |
|---|---|---|---|
pagination | Pagination | — | Pagination metadata from the API |
onPageChange | (page: number) => void | — | Called when a page button is clicked; when omitted, buttons render as <a href="?page=N"> links |
className | string | — | Additional CSS classes |
Returns null when pagination.pages <= 1.
BlogSearch
A debounced search input. Calls onSearch 400 ms after the user stops typing.
import { BlogSearch } from "@wrytze/react";
<BlogSearch
onSearch={(query) => setSearch(query)}
placeholder="Search posts..."
/>| Prop | Type | Default | Description |
|---|---|---|---|
onSearch | (query: string) => void | — | Called with the current input value after the debounce delay |
defaultValue | string | "" | Initial input value |
placeholder | string | "Search posts..." | Input placeholder text |
className | string | — | Additional CSS classes on the wrapper element |
BlogFilter
A horizontally scrollable row of category filter pills. Includes an "All" pill that clears the active filter.
import { BlogFilter } from "@wrytze/react";
<BlogFilter
categories={categories}
activeCategory={currentCategory}
onCategoryChange={(slug) => setCategory(slug)}
/>| Prop | Type | Default | Description |
|---|---|---|---|
categories | { name: string; slug: string }[] | — | Categories to display as filter pills |
activeCategory | string | — | Slug of the currently active category |
onCategoryChange | (slug: string | undefined) => void | — | Called with the selected slug, or undefined when "All" is clicked |
className | string | — | Additional CSS classes |
BlogSkeleton
An animated placeholder that matches the layout of BlogList or BlogArticle. Render it while data is loading to prevent layout shift.
import { BlogSkeleton } from "@wrytze/react";
// Show a 6-card grid placeholder
<BlogSkeleton variant="list" count={6} />
// Show an article placeholder
<BlogSkeleton variant="article" />| Prop | Type | Default | Description |
|---|---|---|---|
variant | "list" | "article" | "list" | Layout to mimic — card grid or full article |
count | number | 6 | Number of card skeletons to render in "list" variant |
className | string | — | Additional CSS classes |
BlogError
An error state UI with an optional retry button.
import { BlogError } from "@wrytze/react";
<BlogError
message="Failed to load posts."
onRetry={() => refetch()}
/>| Prop | Type | Default | Description |
|---|---|---|---|
message | string | "Something went wrong while loading the content." | Error message shown below the icon |
onRetry | () => void | — | When provided, a "Try again" button is rendered |
className | string | — | Additional CSS classes |
TableOfContents
A sidebar navigation component that lists linked headings extracted from the blog content. Includes scroll-spy to highlight the currently visible section.
import { TableOfContents } from "@wrytze/react";
<TableOfContents items={blog.tableOfContents} />| Prop | Type | Default | Description |
|---|---|---|---|
items | TocItem[] | — | Array of heading items from BlogDetail.tableOfContents |
className | string | — | Additional CSS classes |
Each TocItem has the shape { id: string; text: string; level: 2 | 3 }. level: 3 items are indented to indicate sub-sections. Returns null when the items array is empty.
ReadingProgress
A fixed progress bar at the top of the viewport that shows how far the user has scrolled through the page.
import { ReadingProgress } from "@wrytze/react";
<ReadingProgress />| Prop | Type | Default | Description |
|---|---|---|---|
className | string | — | Additional CSS classes on the container |
The bar uses bg-primary and updates on scroll via a passive event listener.
ShareButtons
Share buttons for copying the page link and sharing to Twitter/X and LinkedIn. Supports horizontal (inline) and vertical (sidebar) layouts.
import { ShareButtons } from "@wrytze/react";
// Horizontal layout (below content)
<ShareButtons url="https://example.com/blog/post" title="My Post" />
// Vertical layout (sidebar)
<ShareButtons url="https://example.com/blog/post" title="My Post" vertical />| Prop | Type | Default | Description |
|---|---|---|---|
url | string | — | The page URL to share |
title | string | — | The page title for share text |
className | string | — | Additional CSS classes |
vertical | boolean | false | When true, renders as a vertical column of icon-only circular buttons for sidebar placement |
AuthorCard
Displays the author's avatar (or initial fallback), name, publication date, reading time, and word count in a compact inline row.
import { AuthorCard } from "@wrytze/react";
<AuthorCard
author={blog.author}
publishedAt={blog.publishedAt}
readingTimeMinutes={blog.readingTimeMinutes}
wordCount={blog.wordCount}
/>| Prop | Type | Default | Description |
|---|---|---|---|
author | Author | — | Author object with name and optional image URL |
publishedAt | string | — | ISO 8601 timestamp |
readingTimeMinutes | number | — | Estimated reading time in minutes |
wordCount | number | — | Total word count |
className | string | — | Additional CSS classes |
RelatedPosts
A responsive grid of related blog post cards with featured images, titles, excerpts, and author info.
import { RelatedPosts } from "@wrytze/react";
<RelatedPosts
blogs={blog.relatedBlogs}
basePath="/blog"
imageComponent={NextImage}
/>| Prop | Type | Default | Description |
|---|---|---|---|
blogs | RelatedBlog[] | — | Array of related blog objects |
basePath | string | "/blog" | URL prefix for post links |
className | string | — | Additional CSS classes |
imageComponent | ComponentType | — | Custom image component (e.g. next/image) |
Returns null when the blogs array is empty. Renders a sm:grid-cols-2 lg:grid-cols-3 grid.
PostNavigation
Previous/Next post navigation links displayed as styled cards in a two-column grid.
import { PostNavigation } from "@wrytze/react";
<PostNavigation
prev={{ title: "Getting Started with AI", slug: "getting-started-ai" }}
next={{ title: "Advanced Techniques", slug: "advanced-techniques" }}
basePath="/blog"
/>| Prop | Type | Default | Description |
|---|---|---|---|
prev | { title: string; slug: string } | null | — | Previous post info |
next | { title: string; slug: string } | null | — | Next post info |
basePath | string | "/blog" | URL prefix for navigation links |
className | string | — | Additional CSS classes |
Returns null when both prev and next are null or undefined.
Next.js adapter
Import from @wrytze/react/next to get Next.js-optimized components and two page-level components. Every other component and hook is re-exported unchanged.
import {
BlogImage, // uses next/image automatically
BlogList, // syncs page/category/search to URL params
BlogSearch, // syncs search query to URL params
BlogListPage, // complete blog listing page with filters + grid
BlogPostPage, // complete blog post page with TOC + share + related
} from "@wrytze/react/next";What changes
| Component | Core behavior | /next behavior |
|---|---|---|
BlogImage | Renders <img> | Uses next/image for automatic optimization, lazy loading, and WebP conversion |
BlogList | Manages state internally | Also pushes ?page=, ?category=, ?search= to the URL via useRouter + useSearchParams |
BlogSearch | Calls onSearch on debounce | Also reads ?search= from URL as defaultValue and pushes updates to URL |
BlogListPage | Not available | Full listing page: title, filters, card grid, pagination, URL sync |
BlogPostPage | Not available | Full post page: reading progress, TOC, share buttons, related posts, navigation |
Example: server-side URL reading
Because /next syncs state to the URL, you can read the initial filter values from searchParams in your server component and pass them to your SDK call:
// app/blog/page.tsx
import { WrytzeClient } from "@wrytze/sdk";
import { BlogList } from "@wrytze/react/next";
const wrytze = new WrytzeClient({ apiKey: process.env.WRYTZE_API_KEY! });
export default async function BlogPage({
searchParams,
}: {
searchParams: { page?: string; category?: string; search?: string };
}) {
const page = Number(searchParams.page) || 1;
const { data: blogs, pagination } = await wrytze.blogs.list({
page,
category: searchParams.category,
search: searchParams.search,
});
const { data: categories } = await wrytze.categories.list();
return (
<BlogList
blogs={blogs}
pagination={pagination}
categories={categories}
/>
);
}BlogList from /next wraps the pagination and filter handlers to push URL updates, making the back button work and enabling direct URL sharing.
BlogList and BlogSearch from @wrytze/react/next must be used inside a Next.js <Suspense> boundary when placed in a server component page, because they call useSearchParams() internally.
Image customization
The imageComponent prop accepts any component that matches the next/image interface:
ComponentType<{
src: string;
alt: string;
fill: boolean;
priority?: boolean;
sizes?: string;
className?: string;
}>Pass it to BlogCard, BlogHeader, BlogImage, BlogList, or BlogArticle:
import NextImage from "next/image";
<BlogList
blogs={blogs}
pagination={pagination}
imageComponent={NextImage}
/>When using @wrytze/react/next, this is handled automatically — you do not need to pass imageComponent unless you want to override the default next/image.
Utilities
Three utility functions are exported from @wrytze/react:
cn
Merges Tailwind CSS class names using clsx and tailwind-merge. Useful when extending component styles.
import { cn } from "@wrytze/react";
function cn(...inputs: ClassValue[]): string<BlogCard
blog={blog}
className={cn("ring-2 ring-blue-500", isActive && "ring-offset-2")}
/>formatDate
Formats an ISO 8601 date string into a human-readable locale string using Intl.DateTimeFormat with en-US locale.
import { formatDate } from "@wrytze/react";
function formatDate(dateString: string): stringformatDate("2024-03-15T10:00:00Z"); // "March 15, 2024"formatNumber
Formats a number with locale-aware thousands separators using Intl.NumberFormat with en-US locale.
import { formatNumber } from "@wrytze/react";
function formatNumber(num: number): stringformatNumber(12500); // "12,500"Metadata utilities
Import from @wrytze/react/metadata for server-safe metadata helpers. These functions are not marked "use client", so they work in server components and generateMetadata functions.
import {
generateBlogListMetadata,
generateBlogPostMetadata,
} from "@wrytze/react/metadata";generateBlogListMetadata
Generates Next.js Metadata for a blog listing page.
// app/blog/page.tsx
import type { Metadata } from "next";
import { generateBlogListMetadata } from "@wrytze/react/metadata";
export function generateMetadata(): Metadata {
return generateBlogListMetadata({
title: "Our Blog",
description: "Thoughts, guides, and updates from our team.",
baseUrl: "https://example.com/blog",
});
}| Option | Type | Default | Description |
|---|---|---|---|
title | string | "Blog" | Page title |
description | string | "Browse our latest blog posts" | Meta description |
baseUrl | string | — | Canonical URL for the page |
generateBlogPostMetadata
Generates Next.js Metadata for a blog post page, including OpenGraph, Twitter Card, and JSON-LD structured data.
// app/blog/[slug]/page.tsx
import type { Metadata } from "next";
import { generateBlogPostMetadata } from "@wrytze/react/metadata";
import { wrytze } from "@/lib/wrytze";
export async function generateMetadata({
params,
}: {
params: Promise<{ slug: string }>;
}): Promise<Metadata> {
const { slug } = await params;
const { data: blog } = await wrytze.blogs.getBySlug(slug);
if (!blog) return { title: "Post not found" };
return generateBlogPostMetadata(blog, {
siteName: "My Site",
baseUrl: "https://example.com",
});
}| Option | Type | Default | Description |
|---|---|---|---|
siteName | string | "Blog" | Used in OpenGraph siteName and JSON-LD publisher |
baseUrl | string | — | Used to construct the canonical article URL |
The returned metadata includes title, description, openGraph (article type with published time, authors, tags, image), twitter (summary_large_image card), and JSON-LD Article structured data.
Styling
All components use Tailwind CSS utility classes. To use them in your project:
1. Install and configure Tailwind CSS v4 in your project.
2. Install @tailwindcss/typography — required for BlogContent prose styles:
npm install @tailwindcss/typographyThen add it to your CSS entry point:
@import "tailwindcss";
@import "@wrytze/react/tailwind.css";
@plugin "@tailwindcss/typography";The @wrytze/react/tailwind.css import registers all Tailwind classes used by the components via @source inline(...), so Tailwind v4 generates the correct styles automatically — no manual @source path needed.
3. Dark mode works via Tailwind's dark: variant. All components include dark: classes, so your standard dark mode configuration is all that is needed.
4. Customization — every component accepts a className prop. Classes are merged with tailwind-merge, so your overrides always win:
// Override card background and remove the border
<BlogCard
blog={blog}
className="border-0 bg-gray-50 dark:bg-gray-900"
/>TypeScript types
All props interfaces and hook result types are exported and available for import:
import type {
// Context
WrytzeProviderProps,
// Hook results
UseBlogsResult,
UseBlogResult,
BlogIdentifier,
UseCategoriesResult,
UseTagsResult,
// Component props
BlogListProps,
BlogArticleProps,
BlogCardProps,
BlogHeaderProps,
BlogImageProps,
BlogContentProps,
BlogMetaProps,
BlogCategoriesProps,
BlogTagsProps,
BlogPaginationProps,
BlogSearchProps,
BlogFilterProps,
BlogSkeletonProps,
BlogErrorProps,
TableOfContentsProps,
ReadingProgressProps,
ShareButtonsProps,
AuthorCardProps,
RelatedPostsProps,
PostNavigationProps,
} from "@wrytze/react";
// Page-level component props (Next.js only)
import type {
BlogListPageProps,
BlogPostPageProps,
} from "@wrytze/react/next";SDK types (Blog, BlogDetail, Category, Tag, Pagination, etc.) are re-exported from @wrytze/sdk — import them directly from there. See the Usage guide for the full type reference.