WrytzeWrytze Docs
SDK

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 in next/image for 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/react

Peer dependencies

PackageVersionRequired
react>= 18Yes
react-dom>= 18Yes
@wrytze/sdkanyYes
next>= 14Only 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

PropTypeRequiredDescription
clientWrytzeClientYesThe SDK client instance to provide to children
childrenReactNodeYesComponent 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

PropTypeDefaultDescription
blogsBlog[]Array of blogs to render
paginationPaginationPagination metadata
categories{ name: string; slug: string }[]Categories for the filter bar
basePathstring"/blog"URL prefix for blog card links
titlestring"Blog"Page heading rendered as <h1>
descriptionstringOptional subtitle below the heading
classNamestringAdditional 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

PropTypeDefaultDescription
blogBlogDetailFull blog post object (must include contentHtml and tableOfContents)
basePathstring"/blog"URL prefix for category, tag, and navigation links
prev{ title: string; slug: string } | nullPrevious post for navigation
next{ title: string; slug: string } | nullNext post for navigation
classNamestringAdditional 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/image optimization
  • Share buttons positioned to the right of the title on desktop, inline on mobile
  • Table of contents sidebar on desktop (left of content) when tableOfContents has entries
  • Related posts grid rendered when blog.relatedBlogs has entries
  • Previous/Next navigation rendered when prev or next is 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
): UseBlogsResult

Parameters

ParameterTypeDescription
clientWrytzeClientAn initialized SDK client
paramsListBlogsParamsOptional filter/pagination params (see below)

ListBlogsParams fields — all optional:

FieldTypeDefaultDescription
pagenumber1Page number
limitnumber20Results per page (max 100)
categorystringFilter by category slug
tagstringFilter by tag slug
searchstringSearch by title (case-insensitive)
websiteIdstringScope to a specific website

Return type

FieldTypeDescription
dataBlog[] | nullBlog list, or null while loading
paginationPagination | nullPagination metadata, or null while loading
errorWrytzeError | nullError if the request failed
isLoadingbooleantrue 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
): UseBlogResult

Parameters

ParameterTypeDescription
clientWrytzeClientAn initialized SDK client
identifierBlogIdentifierEither { id: string } or { slug: string }

Return type

FieldTypeDescription
dataBlogDetail | nullFull blog post with contentHtml, or null while loading
errorWrytzeError | nullError if the request failed
isLoadingbooleantrue 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
): UseCategoriesResult

Parameters

ParameterTypeDescription
clientWrytzeClientAn initialized SDK client
paramsListResourceParamsOptional { websiteId?: string } to scope results

Return type

FieldTypeDescription
dataCategory[] | nullCategory list, or null while loading
errorWrytzeError | nullError if the request failed
isLoadingbooleantrue 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
): UseTagsResult

Parameters

ParameterTypeDescription
clientWrytzeClientAn initialized SDK client
paramsListResourceParamsOptional { websiteId?: string } to scope results

Return type

FieldTypeDescription
dataTag[] | nullTag list, or null while loading
errorWrytzeError | nullError if the request failed
isLoadingbooleantrue 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):

PropTypeRequiredDescription
blogsBlog[]YesArray of blogs to render
paginationPaginationYesPagination metadata for the current page
categories{ name: string; slug: string }[]NoCategories for the filter bar

Client mode props (mutually exclusive with data mode):

PropTypeRequiredDescription
clientWrytzeClientNoClient instance; falls back to WrytzeProvider context
websiteIdstringNoWebsite ID to scope fetched content

Shared props (available in both modes):

PropTypeDefaultDescription
classNamestringAdditional CSS classes on the root element
basePathstring"/blog"URL prefix for blog card links (e.g. "/articles")
imageComponentComponentTypeCustom image component (see image customization)
onPageChange(page: number) => voidCalled when the user navigates to a new page
onCategoryChange(category: string | undefined) => voidCalled when the user selects a category filter
onSearch(query: string) => voidCalled 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):

PropTypeRequiredDescription
blogBlogDetailYesFull blog post object (must include contentHtml)

Client mode props (mutually exclusive with data mode):

PropTypeRequiredDescription
clientWrytzeClientNoClient instance; falls back to WrytzeProvider context
slugstringNoBlog slug to fetch (one of slug or id is required)
idstringNoBlog ID to fetch (one of slug or id is required)

Shared props (available in both modes):

PropTypeDefaultDescription
classNamestringAdditional CSS classes on the root <article> element
basePathstring"/blog"URL prefix used for category and tag links
imageComponentComponentTypeCustom 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" />
PropTypeDefaultDescription
blogBlogBlog object from the list API
basePathstring"/blog"URL prefix for the card link
classNamestringAdditional CSS classes
imageComponentComponentTypeCustom 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}
/>
PropTypeDefaultDescription
titlestringThe blog post title, rendered as <h1>
featuredImageUrlstring | nullURL of the featured image; omit to hide the image
featuredImageAltstring | nullAlt text for the featured image
categories{ name: string; slug: string }[]Category badges shown above the title
basePathstring"/blog"URL prefix for category links
classNamestringAdditional CSS classes on the <header> element
imageComponentComponentTypeCustom 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
/>
PropTypeDefaultDescription
srcstringImage URL
altstringAlt text
classNamestringAdditional CSS classes on the container element
aspectRatiostring"aspect-[2/1]"Tailwind aspect-ratio class (e.g. "aspect-[16/9]")
prioritybooleanfalseWhen true, skips lazy loading (use for above-the-fold images)
sizesstring"(max-width: 768px) 100vw, (max-width: 1200px) 66vw, 800px"Responsive sizes hint passed to the image component
imageComponentComponentTypeCustom 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} />
PropTypeDefaultDescription
contentHtmlstringSanitized HTML string from the Wrytze API
classNamestringAdditional 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}
/>
PropTypeDefaultDescription
publishedAtstringISO 8601 timestamp string
readingTimeMinutesnumberEstimated reading time in minutes
wordCountnumberTotal word count
classNamestringAdditional 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" />
PropTypeDefaultDescription
categories{ name: string; slug: string }[]Categories to render
basePathstring"/blog"URL prefix; each link goes to {basePath}?category={slug}
classNamestringAdditional 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" />
PropTypeDefaultDescription
tags{ name: string; slug: string }[]Tags to render
basePathstring"/blog"URL prefix; each link goes to {basePath}?tag={slug}
classNamestringAdditional 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)}
/>
PropTypeDefaultDescription
paginationPaginationPagination metadata from the API
onPageChange(page: number) => voidCalled when a page button is clicked; when omitted, buttons render as <a href="?page=N"> links
classNamestringAdditional 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..."
/>
PropTypeDefaultDescription
onSearch(query: string) => voidCalled with the current input value after the debounce delay
defaultValuestring""Initial input value
placeholderstring"Search posts..."Input placeholder text
classNamestringAdditional 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)}
/>
PropTypeDefaultDescription
categories{ name: string; slug: string }[]Categories to display as filter pills
activeCategorystringSlug of the currently active category
onCategoryChange(slug: string | undefined) => voidCalled with the selected slug, or undefined when "All" is clicked
classNamestringAdditional 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" />
PropTypeDefaultDescription
variant"list" | "article""list"Layout to mimic — card grid or full article
countnumber6Number of card skeletons to render in "list" variant
classNamestringAdditional 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()}
/>
PropTypeDefaultDescription
messagestring"Something went wrong while loading the content."Error message shown below the icon
onRetry() => voidWhen provided, a "Try again" button is rendered
classNamestringAdditional 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} />
PropTypeDefaultDescription
itemsTocItem[]Array of heading items from BlogDetail.tableOfContents
classNamestringAdditional 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 />
PropTypeDefaultDescription
classNamestringAdditional 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 />
PropTypeDefaultDescription
urlstringThe page URL to share
titlestringThe page title for share text
classNamestringAdditional CSS classes
verticalbooleanfalseWhen 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}
/>
PropTypeDefaultDescription
authorAuthorAuthor object with name and optional image URL
publishedAtstringISO 8601 timestamp
readingTimeMinutesnumberEstimated reading time in minutes
wordCountnumberTotal word count
classNamestringAdditional CSS classes

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}
/>
PropTypeDefaultDescription
blogsRelatedBlog[]Array of related blog objects
basePathstring"/blog"URL prefix for post links
classNamestringAdditional CSS classes
imageComponentComponentTypeCustom 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"
/>
PropTypeDefaultDescription
prev{ title: string; slug: string } | nullPrevious post info
next{ title: string; slug: string } | nullNext post info
basePathstring"/blog"URL prefix for navigation links
classNamestringAdditional 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

ComponentCore behavior/next behavior
BlogImageRenders <img>Uses next/image for automatic optimization, lazy loading, and WebP conversion
BlogListManages state internallyAlso pushes ?page=, ?category=, ?search= to the URL via useRouter + useSearchParams
BlogSearchCalls onSearch on debounceAlso reads ?search= from URL as defaultValue and pushes updates to URL
BlogListPageNot availableFull listing page: title, filters, card grid, pagination, URL sync
BlogPostPageNot availableFull 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): string
formatDate("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): string
formatNumber(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",
  });
}
OptionTypeDefaultDescription
titlestring"Blog"Page title
descriptionstring"Browse our latest blog posts"Meta description
baseUrlstringCanonical 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",
  });
}
OptionTypeDefaultDescription
siteNamestring"Blog"Used in OpenGraph siteName and JSON-LD publisher
baseUrlstringUsed 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/typography

Then 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.

On this page