
Next.js Starter for WordPress Headless CMS
This is a starter template for building a Next.js application that fetches data from a WordPress site using the WordPress REST API. The template includes functions for fetching posts, categories, tags, authors, and featured media from a WordPress site and rendering them in a Next.js application.
next-wp
is built with Next.js 15, React, Typescript, Tailwind, shadcn/ui, and brijr/craft. It pairs nicely with brijr/components for a rapid development experience. Built by Cameron and Bridger at 9d8.
Table of Contents
- Next.js Starter for WordPress Headless CMS
- Table of Contents
- Overview
- WordPress Functions
- Pagination System
- WordPress Types
- Post Card Component
- Filter Component
- Dynamic Sitemap
- Dynamic OG Images
- Revalidation Setup
- Search Functionality
- AI Assistant Guidelines
Overview
What's included?
✅ Type-safe data layer with the WordPress RestAPI ✅ Efficient server-side pagination system ✅ WordPress Plugin for revalidation ✅ Granular access to revalidation and cache tags ✅ Setup for all basic WordPress options: Posts, Pages, Authors, Categories, Tags ✅ Easy integration with custom post types and ACF ✅ Dynamic routes for Posts and Pages ✅ Design system for layout and prose styling (craft-ds.com) ✅ Filter, Search, and Card components ✅ Dynamically rendered sitemap ✅ Dynamically generated metadata ✅ Dynamically generated OG/Twitter Cards for Posts and pages ✅ Responsive Nav and Footer components ✅ Site configuration file ✅ Menu configuration file ✅ Lite and dark mode support ✅ shadcn/ui components and theming ✅ Vercel analytics
Important files
lib/wordpress.ts
-> Functions for fetching WordPress CMS via Rest API with cache tagslib/wordpress.d.ts
-> Type declarations for the WordPress Rest APIcomponents/craft.tsx
-> Handles the design system for the site and prose stylingcomponents/posts/post-card.tsx
-> Component and styling for postscomponents/posts/filter.tsx
-> Filter component for Postscomponents/posts/search-input.tsx
-> Search component for Postsmenu.config.ts
-> Site nav menu configuration for desktop and mobilesite.config.ts
-> Configuration forsitemap.ts
and moreapp/sitemap.ts
-> Dynamically generated sitemap
The following environment variables are required in your .env.local
file:
WORDPRESS_URL="https://wordpress.com"WORDPRESS_HOSTNAME="wordpress.com"WORDPRESS_WEBHOOK_SECRET="your-secret-key-here"
You can find the example of .env.local
file in the .env.example
file (and in Vercel).
WordPress Functions
The lib/wordpress.ts
file contains a comprehensive set of functions for interacting with the WordPress REST API. Each function is optimized for Next.js 15's caching system and includes proper error handling.
Core Functionality
// Default fetch options for all WordPress API callsconst defaultFetchOptions = {next: {tags: ["wordpress"],revalidate: 3600, // 1 hour cache},headers: {Accept: "application/json","Content-Type": "application/json",},};
Available Functions
PostsgetAllPosts(filterParams?: { author?: string; tag?: string; category?: string; search?: string; })
: Fetches posts with optional filtering by author, tag, category, or search query. Uses cache tags for efficient revalidation. Note: Limited to 100 posts for performance.getPostsPaginated(page?: number, perPage?: number, filterParams?: { author?: string; tag?: string; category?: string; search?: string; })
: Recommended - Fetches posts with server-side pagination and filtering. Returns both data and pagination headers for efficient large-scale post handling.getPostById(id: number)
: Retrieves a specific post by ID with proper error handling.getPostBySlug(slug: string)
: Fetches a post using its URL-friendly slug.
getAllCategories()
: Retrieves all categories with cache invalidation support.getCategoryById(id: number)
: Gets a specific category with error handling.getCategoryBySlug(slug: string)
: Fetches a category by its slug.getPostsByCategory(categoryId: number)
: Gets all posts in a category, using proper cache tags.
getAllTags()
: Fetches all available tags.getTagById(id: number)
: Retrieves a specific tag.getTagBySlug(slug: string)
: Gets a tag by its slug.getTagsByPost(postId: number)
: Fetches all tags associated with a post.getPostsByTag(tagId: number)
: Gets all posts with a specific tag.
getAllPages()
: Retrieves all WordPress pages.getPageById(id: number)
: Gets a specific page by ID.getPageBySlug(slug: string)
: Fetches a page by its slug.
getAllAuthors()
: Fetches all WordPress authors.getAuthorById(id: number)
: Gets a specific author.getAuthorBySlug(slug: string)
: Retrieves an author by slug.getPostsByAuthor(authorId: number)
: Gets all posts by a specific author.
getFeaturedMediaById(id: number)
: Retrieves featured media (images) with size information.
Error Handling
All functions use the custom WordPressAPIError
class for consistent error handling:
class WordPressAPIError extends Error {constructor(message: string,public status: number,public endpoint: string,) {super(message);this.name = "WordPressAPIError";}}
Cache Management
Each function supports Next.js 15's cache tags for efficient revalidation:
// Example cache configuration{next: {tags: ["wordpress", "posts", `post-${id}`],revalidate: 3600,}}
Usage Example
try {// Fetch posts with filteringconst posts = await getAllPosts({author: "123",category: "news",tag: "featured",});// Handle errors properly} catch (error) {if (error instanceof WordPressAPIError) {console.error(`API Error: ${error.message} (${error.status})`);}}
These functions are designed to work seamlessly with Next.js 15's App Router and provide proper TypeScript support through the types defined in wordpress.d.ts
.
Pagination System
The starter includes an efficient pagination system designed for high-performance WordPress sites with large amounts of content.
Server-Side Pagination
Instead of fetching all posts and paginating client-side, the getPostsPaginated
function implements true server-side pagination:
// Fetch page 2 with 10 posts per pageconst response = await getPostsPaginated(2, 10, {author: "123",category: "news",search: "nextjs"});const { data: posts, headers } = response;const { total, totalPages } = headers;
Pagination Response Structure
The getPostsPaginated
function returns a WordPressResponse<Post[]>
object:
interface WordPressResponse<T> {data: T; // The actual posts arrayheaders: {total: number; // Total number of posts matching the querytotalPages: number; // Total number of pages};}
Benefits of Server-Side Pagination
- Performance: Only fetch the posts you need (e.g., 9 posts instead of 100+)
- Memory Efficiency: Reduced memory usage, especially for sites with many posts
- Network Optimization: Smaller response payloads (up to 90% reduction)
- Scalability: Handles thousands of posts without performance degradation
- Real Pagination Info: Access to total count without processing all data
Migration from getAllPosts
For existing implementations using getAllPosts
, you can migrate to the more efficient pagination:
// Before: Client-side paginationconst allPosts = await getAllPosts({ author, category });const page = 1;const postsPerPage = 9;const paginatedPosts = allPosts.slice((page - 1) * postsPerPage, page * postsPerPage);const totalPages = Math.ceil(allPosts.length / postsPerPage);// After: Server-side paginationconst { data: posts, headers } = await getPostsPaginated(page, postsPerPage, { author, category });const { total, totalPages } = headers;
Example Implementation
The main posts page (app/posts/page.tsx
) demonstrates the pagination system:
export default async function PostsPage({ searchParams }) {const params = await searchParams;const page = params.page ? parseInt(params.page, 10) : 1;const postsPerPage = 9;// Efficient server-side paginationconst { data: posts, headers } = await getPostsPaginated(page,postsPerPage,{author: params.author,category: params.category,tag: params.tag,search: params.search});const { total, totalPages } = headers;return (<div><p>{total} posts found</p>{posts.map(post => <PostCard key={post.id} post={post} />)}{totalPages > 1 && <PaginationComponent />}</div>);}
WordPress API Headers
The pagination system leverages WordPress REST API headers:
X-WP-Total
: Total number of posts matching the queryX-WP-TotalPages
: Total number of pages based onper_page
parameter
These headers are automatically parsed and included in the response for easy access to pagination metadata.
Cache Tags Integration
The pagination system includes sophisticated cache tags for optimal performance:
// Dynamic cache tags based on query parameters["wordpress", "posts", "posts-page-1", "posts-category-123"]
This ensures that when content changes, only the relevant pagination pages are revalidated, maintaining excellent performance even with large content sets.
WordPress Types
The lib/wordpress.d.ts
file contains comprehensive TypeScript type definitions for WordPress entities. The type system is built around a core WPEntity
interface that provides common properties for WordPress content:
interface WPEntity {id: number;date: string;date_gmt: string;modified: string;modified_gmt: string;slug: string;status: "publish" | "future" | "draft" | "pending" | "private";link: string;guid: {rendered: string;};}
Key type definitions include:
Content Types
Post
: Blog posts and articles (extendsWPEntity
)Page
: Static pages (extendsWPEntity
)Author
: User informationCategory
: Post categories (extendsTaxonomy
)Tag
: Post tags (extendsTaxonomy
)FeaturedMedia
: Media attachments (extendsWPEntity
)
Pagination Types
WordPressResponse<T>
: Wrapper for paginated responses containing data and headersWordPressPaginationHeaders
: Contains pagination metadata (total
,totalPages
)
Shared Interfaces
RenderedContent
: For content with HTML renderingRenderedTitle
: For titles with HTML renderingTaxonomy
: Base interface for categories and tags
Component Types
interface FilterBarProps {authors: Author[];tags: Tag[];categories: Category[];selectedAuthor?: Author["id"];selectedTag?: Tag["id"];selectedCategory?: Category["id"];onAuthorChange?: (authorId: Author["id"] | undefined) => void;onTagChange?: (tagId: Tag["id"] | undefined) => void;onCategoryChange?: (categoryId: Category["id"] | undefined) => void;}
Media Types
interface MediaDetails {width: number;height: number;file: string;sizes: Record<string, MediaSize>;}interface MediaSize {file: string;width: number;height: number;mime_type: string;source_url: string;}
All types are designed to be:
- Fully type-safe
- Extensible
- Self-documenting
- Compatible with the WordPress REST API
Post Card Component
The components/posts/post-card.tsx
file contains the PostCard
component, which is responsible for rendering a single post card in the application. Here's an overview of the component:
Props
post
: APost
object representing the WordPress post to be rendered.
Functionality
-
The component fetches the featured media, author, and category associated with the post using the
getFeaturedMediaById
,getAuthorById
, andgetCategoryById
functions fromlib/wordpress.ts
. -
It formats the post date using the
toLocaleDateString
method with the specified options. -
The component renders a link to the individual post page using the post's slug.
-
Inside the link, it displays the post's featured image, title, excerpt, category, and date.
-
The post title and excerpt are rendered using the
dangerouslySetInnerHTML
attribute to handle HTML content. -
The component applies various CSS classes to style the post card, including hover effects and transitions.
Usage
To use the PostCard
component, import it into your desired page or component and pass a Post
object as the post
prop.
Filter Component
The components/posts/filter.tsx
file contains the FilterPosts
component, which provides a filtering interface for posts based on tags, categories, and authors. Here's an overview of the component:
Props
authors
: An array ofAuthor
objects representing the available authors to filter by.tags
: An array ofTag
objects representing the available tags to filter by.categories
: An array ofCategory
objects representing the available categories to filter by.selectedAuthor
: An optional string representing the currently selected author ID.selectedTag
: An optional string representing the currently selected tag ID.selectedCategory
: An optional string representing the currently selected category ID.
Functionality
-
The component uses the
useRouter
hook from Next.js to handle navigation and URL updates based on the selected filters. -
It renders three
Select
components for filtering posts by tag, category, and author. EachSelect
component displays the available options and allows the user to select a specific value or choose "All" to reset the filter. -
When a filter value is changed, the
handleFilterChange
function is called with the filter type and selected value. It updates the URL query parameters accordingly and navigates to the updated URL. -
The component also includes a "Reset Filters" button that, when clicked, calls the
handleResetFilters
function to navigate back to the/posts
page without any filters applied. -
The selected filter values are passed as props to the component and used to set the initial values of the
Select
components.
Search Functionality
The template includes a powerful search system that works seamlessly with WordPress's REST API:
Search Component
Located in components/posts/search-input.tsx
, the SearchInput component provides real-time search capabilities:
// Usage exampleimport { SearchInput } from "@/components/posts/search-input";<SearchInput defaultValue={search} />
Features:
- Real-time search with 300ms debouncing
- URL-based state management
- Maintains filters while searching
- Server-side rendering for SEO
- Combines with existing category, tag, and author filters
Search Implementation
The search system is implemented across several layers:
-
Client-Side Component (
search-input.tsx
):- Uses Next.js App Router's URL handling
- Debounced input for better performance
- Maintains search state in URL parameters
-
Server-Side Processing (
page.tsx
):- Handles search parameters server-side
- Combines search with other filters
- Parallel data fetching for better performance
-
WordPress API Integration (
wordpress.ts
):- Comprehensive search across:
- Post content and titles
- Author names
- Category names
- Tag names
- Smart query construction
- Filter combination support
- Comprehensive search across:
Search API Functions
The following search-related functions are available in lib/wordpress.ts
:
// Search posts with combined filtersgetAllPosts({search?: string,author?: string,tag?: string,category?: string})// Search specific content typessearchCategories(query: string)searchTags(query: string)searchAuthors(query: string)
Example Usage
// In your page componentconst { search } = await searchParams;const posts = search ? await getAllPosts({ search }) : await getAllPosts();
The search functionality automatically updates filters and results as you type, providing a smooth user experience while maintaining good performance through debouncing and server-side rendering.
Dynamic OG Images
This starter includes automatic OG image generation for both posts and pages. The OG images are generated on-demand using the Edge Runtime and include:
- Dynamic title and description
- Modern, responsive design
- Proper social media card sizes
- Automatic text wrapping and scaling
You can test the OG image generation by visiting:
/api/og?title=Your Title&description=Your Description
The OG images are automatically generated for:
- Blog posts:
/posts/[slug]
- Pages:
/pages/[slug]
Each OG image includes:
- The post/page title
- A snippet of the content (automatically trimmed and cleaned)
- Consistent branding across your site
- Proper dimensions for social media platforms
Dynamic Sitemap
The sitemap for next-wp
is generated at @/app/sitemap.ts
and will appear live on your site at yourdomain.com/sitemap.xml
. In order to set up your sitemap correctly please make sure to update the site_domain
in the site.config.ts
to be the domain of your frontend (not your WordPress instance).
Revalidation Setup
This starter implements an intelligent caching and revalidation system using Next.js 15's cache tags. Here's how it works:
Cache Tags System
The WordPress API functions use a sophisticated hierarchical cache tag system for granular revalidation:
Global Tagswordpress
- Affects all WordPress content
posts
- All post contentcategories
- All category contenttags
- All tag contentauthors
- All author content
posts-page-1
,posts-page-2
, etc. - Individual pagination pagesposts-search
- Search result pagesposts-author-123
- Posts filtered by specific authorposts-category-456
- Posts filtered by specific categoryposts-tag-789
- Posts filtered by specific tag
post-123
- Specific post contentcategory-456
- Specific category contenttag-789
- Specific tag contentauthor-123
- Specific author content
This granular system ensures that when content changes, only the relevant cached data is invalidated, providing optimal performance.
Automatic Revalidation
-
Install the WordPress Plugin:
- Navigate to the
/plugin
directory - Use the pre-built
next-revalidate.zip
file or create a ZIP from thenext-revalidate
folder - Install and activate through WordPress admin
- Go to Settings > Next.js Revalidation
- Configure your Next.js URL and webhook secret
- Navigate to the
-
Configure Next.js:
- Add
WORDPRESS_WEBHOOK_SECRET
to your environment variables (same secret as in WordPress plugin) - The webhook endpoint at
/api/revalidate
is already set up - No additional configuration needed
- Add
-
How it Works:
- When content is updated in WordPress, the plugin sends a webhook
- The webhook includes content type and ID information
- Next.js intelligently revalidates specific cache tags based on the change:
- Post changes: Revalidates
posts
,post-{id}
, andposts-page-1
- Category changes: Revalidates
categories
,category-{id}
, andposts-category-{id}
- Tag changes: Revalidates
tags
,tag-{id}
, andposts-tag-{id}
- Author changes: Revalidates
authors
,author-{id}
, andposts-author-{id}
- Post changes: Revalidates
- Only affected cached content is updated, maintaining optimal performance
Plugin Features
The Next.js Revalidation plugin includes:
- Automatic revalidation when posts, pages, categories, tags, authors, or media are modified
- Settings page to configure your Next.js site URL and webhook secret
- Manual revalidation option for full site refresh
- Support for custom post types and taxonomies
- Optional admin notifications for revalidation events
Manual Revalidation
You can manually revalidate content using Next.js cache functions:
import { revalidateTag } from "next/cache";// Revalidate all WordPress contentrevalidateTag("wordpress");// Revalidate specific content typesrevalidateTag("posts");revalidateTag("categories");revalidateTag("tags");revalidateTag("authors");// Revalidate specific itemsrevalidateTag("post-123");revalidateTag("category-456");// Revalidate pagination-specific contentrevalidateTag("posts-page-1");revalidateTag("posts-category-123");revalidateTag("posts-search");
This system ensures your content stays fresh while maintaining optimal performance through intelligent caching.
AI Assistant Guidelines
This codebase includes a CLAUDE.md file that provides guidance to AI assistants (like Claude) when working with code in this repository. It contains:
- Project architecture overview
- Code style guidelines
- Build and development commands
- TypeScript and component patterns
- Environment variable configuration
This ensures AI assistants maintain consistency and follow project conventions when helping with development tasks.
Built by Bridger Tower and Cameron Youngblood at 9d8