How to consume WordPress from Next.js and the REST API in WordPress

Contents

Introduction

This tutorial explains, in exhaustive detail, how to consume WordPress data from a Next.js frontend using the WordPress REST API. It covers public read-only consumption, authenticated requests (Application Passwords and JWT), server-side rendering (SSR), static site generation (SSG), incremental static regeneration (ISR), client-side fetching with SWR/React Query, handling images with Next Image, rendering post content safely, accessing custom fields (ACF/custom post meta), posting data back to WordPress, CORS, caching strategies, and deployment considerations. Code examples are included for every major step.

Prerequisites

  • WordPress site with REST API enabled (default in WP 4.7 )
  • Next.js project (v10 recommended examples use next 12/13 compatible APIs)
  • Familiarity with JavaScript/React and basic WordPress admin tasks
  • Optional plugins: JWT Authentication for WP REST API, WP REST API – ACF plugin (or expose custom fields via register_rest_field)

WordPress REST API overview

WordPress exposes a REST API rooted at /wp-json. Out-of-the-box WOrdPress provides endpoints under /wp/v2 for posts, pages, categories, tags, media, users (limited), comments, custom post types, and taxonomies. Plugins and themes can add endpoints or extend existing ones.

Common endpoints

Endpoint Method Purpose
/wp-json/wp/v2/posts GET / POST List posts or create posts (authentication required for POST)
/wp-json/wp/v2/posts/{id} GET / PUT / DELETE Retrieve single post or update/delete with auth
/wp-json/wp/v2/pages GET Pages endpoint
/wp-json/wp/v2/media GET / POST Media library upload files with auth
/wp-json/wp/v2/categories GET Taxonomy categories
/wp-json/wp/v2/comments GET / POST Comment list and create comment

Useful documentation links

Configure WordPress for headless consumption

  1. Enable pretty permalinks (Settings → Permalinks). REST API works better with pretty permalinks.
  2. Consider CORS: If your Next.js app is served from a different origin, ensure WP allows cross-origin requests. You can add a small filter in functions.php or use a plugin.
  3. Install optional plugins when needed:
    • JWT Authentication for WP REST API (for token-based auth)
    • Advanced Custom Fields (ACF) and ACF to REST API plugin (to expose custom fields)
    • WP-REST-API Menus (if you want navigation menus exposed)
    • Yoast SEO (adds SEO fields to posts accessible via REST)
  4. For production, use HTTPS (required by some auth options and to protect credentials).

Example: Adding CORS headers in functions.php

add_action( rest_api_init, function() {
    remove_filter( rest_pre_serve_request, rest_send_cors_headers )
    add_filter( rest_pre_serve_request, function( value ) {
        header( Access-Control-Allow-Origin:  ) // restrict in production
        header( Access-Control-Allow-Methods: GET, POST, OPTIONS, PUT, DELETE )
        header( Access-Control-Allow-Headers: Authorization, Content-Type )
        return value
    })
}, 15 )

Authentication options

For read-only public content, no auth is required if posts are published. For creating/updating content or accessing private content, common methods:

  • Application Passwords (core WP feature since 5.6): simple Basic Auth using user:application-password base64. Good for server-to-server or build-time auth.
  • JWT Authentication (plugin): returns a token via /wp-json/jwt-auth/v1/token and uses Authorization: Bearer token for subsequent requests.
  • Cookie authentication (when Next.js renders on same origin or reverse proxy): uses WP nonces and cookies more complex.
  • OAuth: for complex multi-user flows more complex to implement.

Application Passwords example (Basic Auth)

Generate an Application Password in WordPress user profile (gives a string like abcd efgh ijkl mnop). Use Basic Auth header:

# Build header value: base64(username:application-password)
# Example: using curl to create a post
curl -X POST https://example.com/wp-json/wp/v2/posts 
  -H Authorization: Basic BASE64_ENCODED 
  -H Content-Type: application/json 
  -d {title:Hello from Next,content:Posted programmatically,status:publish}

JWT Authentication example

After installing JWT plugin and configuring secret key in wp-config.php, obtain token:

curl -X POST https://example.com/wp-json/jwt-auth/v1/token 
  -H Content-Type: application/json 
  -d {username:admin,password:yourpassword}
# Response contains token: {token:eyJ0eXAi...,user_email:...,user_nicename:admin}

Use it:

curl -X POST https://example.com/wp-json/wp/v2/posts 
  -H Authorization: Bearer eyJ0eXAi... 
  -H Content-Type: application/json 
  -d {title:From JWT,content:...,status:publish}

Setting up Next.js to fetch WordPress data

Decide rendering strategy depending on needs:

  • SSG (getStaticProps): fast, build-time fetch ideal for mostly-static content.
  • ISR: use getStaticProps with revalidate to update static pages periodically or after webhook-triggered rebuilds.
  • SSR (getServerSideProps): dynamic on each request required for user-specific content or preview mode.
  • Client-side fetching: for truly dynamic interactions (comments, infinite scroll).

Example: Next.js index page using getStaticProps to fetch posts

// pages/index.js
export async function getStaticProps() {
  const res = await fetch(https://example.com/wp-json/wp/v2/posts?_embedper_page=10)
  if (!res.ok) {
    return { props: { posts: [], error: true }, revalidate: 60 }
  }
  const posts = await res.json()
  return { props: { posts }, revalidate: 60 } // ISR: revalidate every 60s
}

export default function Home({ posts }) {
  return (
    
{posts.map(post => (

{/ link to single post page /}

))}
) }

Example: dynamic post page with getStaticPaths and getStaticProps

// pages/posts/[slug].js
export async function getStaticPaths() {
  const res = await fetch(https://example.com/wp-json/wp/v2/posts?per_page=100)
  const posts = await res.json()
  const paths = posts.map(p => ({ params: { slug: p.slug } }))
  return { paths, fallback: blocking }
}

export async function getStaticProps({ params }) {
  const res = await fetch(https://example.com/wp-json/wp/v2/posts?slug={params.slug}_embed)
  const posts = await res.json()
  if (!posts  posts.length === 0) {
    return { notFound: true }
  }
  const post = posts[0]
  return { props: { post }, revalidate: 60 }
}

export default function Post({ post }) {
  return (
    

) }

Handling images: next/image and remote images

WordPress returns featured media and media URLs. To use next/image with remote domains, configure next.config.js.

// next.config.js
module.exports = {
  images: {
    remotePatterns: [
      {
        protocol: https,
        hostname: example.com,
        port: ,
        pathname: /wp-content/uploads/,
      },
    ],
  },
}

Then pass the remote URL to next/image as src. If using the _embedded_ endpoint (_embed) you can get featured media at post._embedded[wp:featuredmedia][0].source_url.

Working with post content HTML and security

The REST API returns content.rendered (HTML). You can render it via dangerouslySetInnerHTML, but you must sanitize if you allow external content or user-submitted markup.

Sanitizing server/client safe approach

  • Use isomorphic-dompurify to sanitize HTML on server and client.
  • Alternatively, restrict allowed tags/attributes server-side in WordPress before publishing.
# Install DOMPurify for Next.js
npm install isomorphic-dompurify
import createDOMPurify from isomorphic-dompurify

export default function Post({ post }) {
  const DOMPurify = createDOMPurify()
  const safe = DOMPurify.sanitize(post.content.rendered)
  return 
}

Querying, filtering, pagination, and _embed

WordPress REST supports lots of query parameters. Important ones:

  • per_page (max 100 by default)
  • page (pagination)
  • search (search terms)
  • categories, tags, author (filter by ID)
  • slug (exact slug match)
  • _embed (include related resources like featured media and author)
  • _fields (limit returned JSON fields for performance)

Examples:

# get first 20 posts with embedded media
curl https://example.com/wp-json/wp/v2/posts?per_page=20_embed
# get posts in category 5
curl https://example.com/wp-json/wp/v2/posts?categories=5

Consuming custom fields (ACF) and custom post types

ACF data is not returned by default unless you expose it. Options:

  • Install ACF to REST API plugin — it will add fields under acf in the returned JSON.
  • Register custom fields using register_rest_field in your theme or plugin.
  • Use WPGraphQL alternative for more typed queries.

Example: register_rest_field to expose post meta

add_action( rest_api_init, function () {
  register_rest_field( post, my_meta, array(
    get_callback    => function ( object ) {
      return get_post_meta( object[id], my_meta_key, true )
    },
    update_callback => null,
    schema          => null,
  ) )
} )

Client-side fetching (SWR) and caching

For client-side data fetching (comments, live updates, infinite scroll), use SWR or React Query. They provide caching, revalidation, and mutation utilities.

npm install swr axios
import useSWR from swr
import axios from axios

const fetcher = url => axios.get(url).then(res => res.data)

export default function Comments({ postId }) {
  const { data, error } = useSWR(/api/comments?post={postId}, fetcher)
  if (error) return 
Error
if (!data) return
Loading...
return
    {data.map(c =>
  • {c.content.rendered}
  • )}
}

Posting data to WordPress from Next.js (create/update)

Sending POST/PUT/DELETE requires authentication. Use Application Passwords or JWT tokens when performing actions from server-side Next.js code or from an API route (recommended: do not store credentials in client-side code).

Example: Next.js API route that creates a post using Application Passwords

// pages/api/create-post.js
export default async function handler(req, res) {
  if (req.method !== POST) return res.status(405).end()
  const { title, content } = req.body
  const WP_USER = process.env.WP_USER
  const WP_APP_PASSWORD = process.env.WP_APP_PASSWORD // from WP profile
  const auth = Buffer.from({WP_USER}:{WP_APP_PASSWORD}).toString(base64)
  const wpRes = await fetch(https://example.com/wp-json/wp/v2/posts, {
    method: POST,
    headers: {
      Authorization: Basic {auth},
      Content-Type: application/json
    },
    body: JSON.stringify({ title, content, status: publish })
  })
  const json = await wpRes.json()
  return res.status(wpRes.status).json(json)
}

Example: using JWT token in server-side code

// get token (server-side)
async function getJwtToken() {
  const res = await fetch(https://example.com/wp-json/jwt-auth/v1/token, {
    method: POST,
    headers: { Content-Type: application/json },
    body: JSON.stringify({ username: process.env.WP_USER, password: process.env.WP_PASS })
  })
  const json = await res.json()
  return json.token
}

// create post using token
async function createPost(title, content) {
  const token = await getJwtToken()
  const res = await fetch(https://example.com/wp-json/wp/v2/posts, {
    method: POST,
    headers: {
      Authorization: Bearer {token},
      Content-Type: application/json
    },
    body: JSON.stringify({ title, content, status: publish })
  })
  return res.json()
}

Preview mode and editor previews

To enable WordPress previews in Next.js, implement the preview endpoint in Next.js that accepts WP preview query parameters, verifies the preview nonce server-side (or uses a secure secret), fetches the previewed content via the REST API (draft/revision), and sets Next.js preview mode cookie then redirects to the post path. Use getServerSideProps to render previews.

Handling Gutenberg blocks and structured content

Gutenberg stores block HTML inside post.content.rendered. If you want to render individual blocks as React components, consider:

  • Parse blocks server-side with the WordPress REST API that exposes blocks? (not by default)
  • Use libraries like html-react-parser or write a block parser that maps known block types to React components.
  • Consider using WPGraphQL WPGraphQL Gutenberg extension which exposes block attributes more cleanly.

Error handling, rate-limiting and pagination strategies

  • Check response codes: 200-299 is success. Handle 400/401/403/404/500 appropriately in Next.js.
  • Respect per_page and pagination headers: total and totalpages are provided in headers X-WP-Total and X-WP-TotalPages.
  • Use exponential backoff and retry strategies for flaky network or rate-limited APIs.

Performance and caching tips

  • Use getStaticProps with ISR for high performance and fresh content.
  • On write operations, trigger webhook to revalidate pages via On-Demand Revalidation (Next.js) or purge CDN cache.
  • Limit fields via _fields to reduce payload size.
  • Cache results on server or edge using a caching layer (CDN, Redis, or Next.js edge functions).

Security best practices

  • Never embed admin credentials in client-side code. Use server-side API routes to sign requests.
  • Use HTTPS for all endpoints.
  • Restrict CORS to specific origins in production.
  • Sanitize HTML content before rendering if not fully trusted.

Deploying Next.js app that consumes WordPress

  1. Use Vercel, Netlify, or any Node-capable host.
  2. Set environment variables (WP base URL, WP_USER, WP_APP_PASSWORD, JWT credentials) in your host’s settings do not commit secrets.
  3. If using ISR and on-demand revalidation, implement a secure webhook from WordPress to Next.js to revalidate specific paths via Next.js API route secure the webhook with a secret token.
  4. Consider hosting WordPress on a performant managed host and enable caching (object cache, page cache) there.

Example: Next.js on-demand revalidation endpoint (secure)

// pages/api/revalidate.js
export default async function handler(req, res) {
  if (req.method !== POST) return res.status(405).end()
  const token = req.headers[x-webhook-token]
  if (token !== process.env.REVALIDATE_TOKEN) return res.status(401).json({ message: Invalid token })

  const { path } = req.body
  try {
    await res.unstable_revalidate(path)
    return res.json({ revalidated: true })
  } catch (err) {
    return res.status(500).send(Error revalidating)
  }
}

Examples: Useful small recipes

Get featured image URL from _embedded

// assume post is from /wp/v2/posts?_embed
const featured = post._embedded  post._embedded[wp:featuredmedia]  post._embedded[wp:featuredmedia][0]
const imageUrl = featured ? featured.source_url : null

Paginate posts server-side with headers

const res = await fetch(https://example.com/wp-json/wp/v2/posts?per_page=10page=1)
const posts = await res.json()
const total = res.headers.get(X-WP-Total)
const totalPages = res.headers.get(X-WP-TotalPages)

Expose ACF fields and consume them

// expose all ACF fields under acf with plugin or:
add_action(rest_api_init, function() {
  register_rest_field(post, acf_custom, array(
    get_callback => function(object) {
      return get_fields(object[id]) // returns all ACF fields
    }
  ))
})
// consume
const res = await fetch(https://example.com/wp-json/wp/v2/posts?slug=my-post)
const [post] = await res.json()
console.log(post.acf_custom) // your ACF values

Advanced: Using WPGraphQL as an alternative

If you prefer a single query to fetch exactly the shape you need, install WPGraphQL. It allows GraphQL queries against WP including ACF and Gutenberg block fields with the right extensions. Next.js Apollo/URQL WPGraphQL is a common pattern.

Debugging tips

  • Visit the endpoint directly in a browser: https://example.com/wp-json/wp/v2/posts to inspect JSON.
  • Check HTTP response headers for pagination and CORS.
  • Use Postman or curl to replicate requests and confirm authentication works.
  • Inspect network tab for Next.js fetches and server logs for API route issues.

Complete example workflow summary

  1. Set up WordPress with pretty permalinks, CORS headers, and optional plugins (JWT / ACF).
  2. Create Next.js pages using getStaticProps/getStaticPaths for posts pages use revalidate for ISR.
  3. Fetch post lists and details from /wp-json/wp/v2 endpoints, using _embed for related resources.
  4. Render HTML safely via isomorphic-dompurify and dangerouslySetInnerHTML or transform into React components.
  5. Configure next/image remote domains to handle WP media URLs.
  6. Use server-side API routes for authenticated write operations with Application Passwords or JWT tokens never embed credentials client-side.
  7. Deploy Next.js and configure environment variables optionally implement on-demand revalidation using a secure webhook from WordPress.

Troubleshooting common issues

  1. Empty _embed fields: Ensure you request _embed and that the referenced media exist and are public.
  2. 401 on write: Verify credentials and ensure Basic Auth/Application Password or JWT plugin is configured and using HTTPS.
  3. CORS errors: Add proper Access-Control-Allow-Origin header on the WordPress site for your Next.js origin.
  4. Large payloads: Use _fields to limit response or paginate.
  5. HTML rendering differences: WordPress may include relative URLs — normalize or use absolute in the backend or transform on the frontend.

Appendix: Useful quick commands and snippets

Get a single post by slug

curl https://example.com/wp-json/wp/v2/posts?slug=hello-world_embed

Upload media with Basic Auth (Application Password)

curl -X POST https://example.com/wp-json/wp/v2/media 
  -H Authorization: Basic BASE64 
  -H Content-Disposition: attachment filename=image.jpg 
  -H Content-Type: image/jpeg 
  --data-binary @./image.jpg

Register a REST route in WordPress (PHP)

add_action(rest_api_init, function() {
  register_rest_route(myplugin/v1, /hello, array(
    methods => GET,
    callback => function() {
      return new WP_REST_Response(array(msg => Hello from WP), 200)
    }
  ))
})

Final notes

This article equips you to build a robust Next.js front end that consumes WordPress via the REST API for both public content and authenticated actions. Use SSG/ISR for performance, server-side API routes for secure write operations, sanitize HTML, and consider WPGraphQL for more complex data requirements. Follow security best practices and use environment variables for credentials. The examples provided are ready to adapt to your project and deployment environment.



Acepto donaciones de BAT's mediante el navegador Brave 🙂



Leave a Reply

Your email address will not be published. Required fields are marked *