In a headless setup, WordPress acts purely as a content management backend and the front end is served by a separate application — a React, Vue, or Nuxt.js app — that consumes content via the REST API. You get all of WordPress’s content management power while building the front end with whatever technology you choose.
Problem: How do you use WordPress as a headless CMS back end — delivering content via the REST API to a JavaScript front end — while keeping the familiar admin editorial workflow?
Solution: The WordPress REST API exposes posts, pages, custom post types, taxonomies, and media at /wp-json/wp/v2/. Register custom post types with show_in_rest: true and extend API responses with register_rest_field(). Any JavaScript framework can then fetch and render the content independently.
The key REST API endpoints for a typical blog:
# All published posts (paginated, 10 per page)
GET /wp-json/wp/v2/posts?per_page=10&page=1
# Single post by ID
GET /wp-json/wp/v2/posts/42
# Single post by slug
GET /wp-json/wp/v2/posts?slug=my-article
# Posts in a specific category (term ID = 5)
GET /wp-json/wp/v2/posts?categories=5
# Featured media (embedded — avoids a second request)
GET /wp-json/wp/v2/posts?_embed=wp:featuredmedia
# All categories
GET /wp-json/wp/v2/categories
Fetching posts with the featured image embedded, in JavaScript:
async function getPosts( page = 1 ) {
const url = `https://example.com/wp-json/wp/v2/posts`
+ `?per_page=10&page=${page}&_embed=wp:featuredmedia`;
const response = await fetch( url );
const total = parseInt( response.headers.get( 'X-WP-Total' ), 10 );
const pages = parseInt( response.headers.get( 'X-WP-TotalPages' ), 10 );
const posts = await response.json();
return { posts, total, pages };
}
getPosts( 1 ).then( ( { posts, pages } ) => {
posts.forEach( post => {
const thumbnail = post._embedded?.['wp:featuredmedia']?.[0]?.source_url;
console.log( post.title.rendered, thumbnail );
} );
console.log( 'Total pages:', pages );
} );
Expose additional fields via the REST API:
add_action( 'rest_api_init', function() {
register_rest_field( 'post', 'reading_time', [
'get_callback' => function( $post_arr ) {
$words = str_word_count( strip_tags( $post_arr['content']['rendered'] ) );
return max( 1, (int) round( $words / 200 ) ); // minutes at 200 wpm
},
'schema' => [
'description' => 'Estimated reading time in minutes.',
'type' => 'integer',
],
] );
} );
NOTE: Use the _fields parameter to limit response size to only the fields you need — ?_fields=id,title,slug,date,excerpt. This can dramatically reduce payload size when building list views that don't need full post content.