Skip to Content

ISR Blog with Next.js and WordPress

An Incremental Static Regeneration Blog Example Using Next.js and WordPress

A Feature Rich App Router WordPress Example

This is an example on how you can build a Next.js 14 project (with App Router), using WordPress as the data source.

Key features:


  • robots.ts: This automatically gets the robots.txt of the API route and serves it on the /robots.txt route.
  • sitemap.ts: This automatically gets all paths from the API and generates a sitemap to serve on the /sitemap.xml route.
  • middleware.ts: This contains a middleware function that checks the users path for stored redirects, and redirects the user if a match is found.
  • [[...slug]]: This is the catch-all route that is used to render all pages. It is important that this route is not removed, as it is used to render all pages. It fetches the ContentType and renders the corresponding
  • not-found.tsx: This page is used for dynamic 404 handling - adjust the database id to match your decired WordPress page, and make sure the WordPress slug is "not-found", your 404 page will then be editable from your CMS.
  • codegen.ts: Automatic type generation for your WordPress installation
  • Draft Mode: Seamless Preview / Draft Preview support, using authentication through WPGraphQL JWT Authentication and Next.js Draft Mode
  • On Demand Cache Revalidation: Including a bare minimum WordPress theme that implements cache revalidation, WordPress link rewrites and other utils for integrating with Next.js

Deploy your own

Related examples


How to use

Execute create-next-app with npm, Yarn, pnpm, or Bun to bootstrap the example:

1 npx create-next-app --example cms-wordpress cms-wordpress-app


1 yarn create next-app --example cms-wordpress cms-wordpress-app


1 pnpm create next-app --example cms-wordpress cms-wordpress-app


1 bunx create-next-app --example cms-wordpress cms-wordpress-app

Deploy it to the cloud with Vercel ( Documentation).

Configuration


WordPress

  1. Set Site Address (URL) to your frontend URL, e.g. https://localhost:3000 in Settings -> General
  2. Make sure Permalinks are set to Post name in Settings -> Permalinks
  3. Set Sample page as Static page in Settings -> Reading
  4. Create a new page called 404 not found ensuring the slug is 404-not-found
  5. Install and activate following plugins:
  6. Do first-time install of Redirection. Recommended to enable monitor of changes
  7. Configure Yoast SEO with:
    • Disable XML Sitemaps under Yoast SEO -> Settings
    • If you did not change the Site Address (URL) before installing Yoast, it will ask you to run optimize SEO data after changing permalinks, do so
    • Generate a robots.txt file under Yoast SEO -> Tools -> File Editor
    • Modify robots.txt sitemap reference from wp-sitemap.xml to sitemap.xml
  8. Enable Public Introspection under GraphQL -> Settings
  9. Add following constants to wp-config.php 1 define('HEADLESS_SECRET', 'INSERT_RANDOM_SECRET_KEY'); 2 define('HEADLESS_URL', 'INSERT_LOCAL_DEVELOPMENT_URL'); // http://localhost:3000 for local development 3 define('GRAPHQL_JWT_AUTH_SECRET_KEY', 'INSERT_RANDOM_SECRET_KEY'); 4 define('GRAPHQL_JWT_AUTH_CORS_ENABLE', true);
  10. Create a bare minimum custom WordPress theme, consisting of only 2 files:

Next.js

Name

  1. Clone the repository
  2. Run npm install to install dependencies
  3. Create .env file in the root directory and add the following variables:

You can reach our customer support team by emailing info@yourcompany.example.com, calling +1 555-555-5556, or using the live chat on our website. Our dedicated team is available 24/7 to assist with any inquiries or issues.

We’re committed to providing prompt and effective solutions to ensure your satisfaction.

We offer a 30-day return policy for all products. Items must be in their original condition, unused, and include the receipt or proof of purchase. Refunds are processed within 5-7 business days of receiving the returned item.

Our Services

NEXT_PUBLIC_BASE_URL

NEXT_PUBLIC_WORDPRESS_API_URL

NEXT_PUBLIC_WORDPRESS_API_HOSTNAME

Brand & Design

HEADLESS_SECRET

Insert base url of your WordPress installation
Insert base url of frontend
INSERT_RANDOM_SECRET_KEY
The hostname without protocol for your WordPress installation

WP_APP_PASS

Insert the same random key, that you generated for your wp-config.php

username
Insert a valid WordPress username
1234 5678 abcd efgh
Insert application password

[!WARNING] > WP_USER and WP_APP_PASS are critical for making preview and redirection work

  1. Adjust the ID in not-found.tsx to match the post id of your "404 Not Found" page in WordPress
  2. npm run dev and build an awesome application with WordPress!

[!NOTE] > Running npm run dev will automatically generate typings from the WordPress installation found on the url provided in your environment variable: NEXT_PUBLIC_WORDPRESS_API_URL

GraphQL and typescript types

We are generating typescript types from the provided schema with Codegen.

Enabling Auto Completion for graphql queries

If you want to add auto completion for your queries, you can do this by installing the "Apollo GraphQL" extension in VS Code and adding an apollo.config.js file, next to the next.config.js, and add the following to it:

1

module.exports={

2

client:{

3

service:{

4

name:"WordPress",

5

localSchemaFile:"./src/gql/schema.gql",

6

},

7

},

8 };


Advanced Custom Fields PRO (optional, but recommended)

I will recommend building your page content by using the Flexible Content data type in ACF Pro. This will make you able to create a "Block Builder" editor experience, but still having everything automatically type generated, and receiving the data in a structured way. The default "Gutenberg" editor returns a lot of HTML, which makes you loose a lot of the advantages of using GraphQL with type generation.

Redirection setup

The example supports the WordPress "Redirection" plugin. the WP_USER and WP_APP_PASS environment variables are required, for this to work. By implementing this you can manage redirects for your content, through your WordPress CMS

Draft / Preview support

The example supports WordPress preview (also draft preview), when enabling draftMode in the api/preview/route.ts it logs the WP_USER in with the WP_APP_PASS and requests the GraphQL as an authenticated user. This makes draft and preview available. If a post is in "draft" status, it doesn't have a real slug. In this case we redirect to a "fake" route called /preview/${id} and uses the supplied id for fetching data for the post.

Cache Revalidation

Template handling

SEO

We use an "Optional Catch-all Segment" for handling all WordPress content. When rendering this component we simply ask GraphQL "what type of content is this route?" and fetch the corresponding template. Each template can then have their own queries for fetching specific content for that template.

You can reach our customer support team by emailing info@yourcompany.example.com, calling +1 555-555-5556, or using the live chat on our website. Our dedicated team is available 24/7 to assist with any inquiries or issues.

We’re committed to providing prompt and effective solutions to ensure your satisfaction.

We offer a 30-day return policy for all products. Items must be in their original condition, unused, and include the receipt or proof of purchase. Refunds are processed within 5-7 business days of receiving the returned item.

The boilerplate is structured as follows:

  • app: Contains the routes and pages of the application
  • assets: Contains helpful styles such as the variables
  • components: Contains the components used in the application
  • gql: Contains auto-generated types from GraphQL via CodeGen
  • queries: Contains reusable data fetch requests to GraphQL
  • utils: Contains helpful functions used across the application

WordPress theme functions.php

This functions.php is implementing different useful features for using WordPress with Next.js:

1

2 /**


4 *

6 */

5 * @return void

13 )

3 * Registers new menus

7 add_action('init', 'register_new_menu');

9 {

14 );

10 register_nav_menus(

11 array(

8 function register_new_menu()

12 'primary-menu' => __('Primary menu')

17 /**


  • 21 * @return string The REST API root URL. 15 }
  • 22 */ 16
  • 24 function home_url_as_api_url($url) 28 }
  • 37 * @return string Modified headless preview link. 29
  • 36 * @param WP_Post $post Current post object. 34 *
  • 18 * Changes the REST API root URL to use the home URL as the base. 19 *
  • 23 add_filter('rest_url', 'home_url_as_api_url'); 25 {
  • 39 add_filter( 'preview_post_link', 'set_headless_preview_link', 10, 2 ); 30 /**
  • 31 * Customize the preview button in the WordPress admin. 32 *
  • 35 * @param string $link Original WordPress preview link. 38 */

46 [

57 // Check if the post status is 'draft' and set the preview link accordingly.

41 // Set the front-end preview route.

56 function set_headless_rest_preview_link( WP_REST_Response $response, WP_Post $post ): WP_REST_Response {

42 $frontendUrl = HEADLESS_URL;
49 ],
50 esc_url_raw( esc_url_raw( "$frontendUrl/api/preview" ))
52 }

51 );

55 add_filter( 'rest_prepare_post', 'set_headless_rest_preview_link' , 10, 2 );

44 // Update the preview link in WordPress.
45 return add_query_arg(
54 add_filter( 'rest_prepare_page', 'set_headless_rest_preview_link', 10, 2 );
47 'secret' => HEADLESS_SECRET,

64 if ( 'publish' === $post->post_status ) {

63 // For published posts, modify the permalink to point to the frontend.

61 }
48 'id' => $post->ID,
58 if ( 'draft' === $post->post_status ) {
60 return $response;

79 );


  • 66 // Get the post permalink. 68
  • 77 $frontendUrl, 71
  • 80 } 82
  • 75 $response->data['link'] = str_ireplace( 85
  • 72 $frontendUrl = HEADLESS_URL; 86
  • 69 // Check if the permalink contains the site URL. 65
  • 83 return $response; 73
  • 88 * Adds the headless_revalidate function to the save_post action hook. 81 }
  • 76 get_site_url(), 84 }
  • 74 // Replace the site URL with the frontend URL. 87 /**

93 * @return void


  • 92 * @param int $post_ID The ID of the post being saved. 97 {
  • 99 return; 101
  • 107 $frontendUrl = HEADLESS_URL; 106
  • 111 'tags' => ['wordpress'], 109
  • 110 $data = json_encode([ 113
  • 95 add_action('transition_post_status', 'headless_revalidate', 10, 3); 91 *
  • 104 return; 94 */
  • 90 * Requires HEADLESS_URL and HEADLESS_SECRET to be defined in wp-config.php 100 }
  • 102 // Ignore drafts and inherited posts. 105 }
  • 108 $headlessSecret = HEADLESS_SECRET; 112 ]);

120 ],

119 'Content-Type' => 'application/json',

124 if (is_wp_error($response)) {

118 'X-Headless-Secret-Key' => $headlessSecret,

115 'method' => 'PUT',
121 ]);
126 error_log($response->get_error_message());
128 }

127 }

123 // Check if the request was successful

116 'body' => $data,
117 'headers' => [
125 // Handle error
134 // Check for array key taxonomyType

137 } else {

135 if (array_key_exists('taxonomyType', $_GET)) {

139 }
130 function wsra_get_user_inputs()
138 $taxonomy = 'category';
131 {

148 $postArgs = array(

150 'post_type' => strval($postType ? $postType : 'post'),

140 $postType = $_GET['postType'];

142 $perPage = $perPage ? $perPage : 100;

145 'number' => $perPage,
147 );
149 'posts_per_page' => $perPage,
155 }

146 'offset' => $offset,

164 $url = str_replace(home_url(), '', $fullUrl);

157 function wsra_generate_author_api()
151 'paged' => $paged,
144 $args = array(
159 [$args] = wsra_get_user_inputs();

162 foreach ($authors as $author) {

163 $fullUrl = esc_url(get_author_posts_url($author->ID));

152 );
160 $author_urls = array();
161 $authors = get_users($args);
158 {

167 ];

175 [$args,, $taxonomy] = wsra_get_user_inputs();

170 return array_merge($author_urls);

180 $url = str_replace(home_url(), '', $fullUrl);

165 $tempArray = [
169 }
173 function wsra_generate_taxonomy_api()
171 }

174 {

184 array_push($taxonomy_urls, $tempArray);

176 $taxonomy_urls = array();
166 'url' => $url,
183 ];
186 return array_merge($taxonomy_urls);

189 function wsra_generate_posts_api()

179 $fullUrl = esc_url(get_category_link($taxonomy->term_id));

185 }
181 $tempArray = [
182 'url' => $url,
187 }

190 {

191 [, $postArgs] = wsra_get_user_inputs();

195 while ($query->have_posts()) {

197 $uri = str_replace(home_url(), '', get_permalink());

192 $postUrls = array();
203 }
208 function wsra_generate_totalpages_api()
206 }

201 ];

200 'post_modified_date' => get_the_modified_date(),

196 $query->the_post();
199 'url' => $uri,
209 {
205 return array_merge($postUrls);

214 'publicly_queryable' => true

211 'exclude_from_search' => false

212 );
204 wp_reset_postdata();
213 $argsTwo = array(
210 $args = array(

215 );

217 $post_typesTwo = get_post_types($argsTwo, 'names');

220 $defaultArray = [

229 return array_merge($defaultArray, $tempValueHolder);

219 unset($post_types['attachment']);
224 ];
223 'user' => (int)count_users()['total_users'],
230 }

222 'tag' => count(get_tags()),

233 register_rest_route('sitemap/v1', '/posts', array(

221 'category' => count(get_categories()),
225 $tempValueHolder = array();
228 }
232 add_action('rest_api_init', function () {

235 'callback' => 'wsra_generate_posts_api',

239 register_rest_route('sitemap/v1', '/taxonomy', array(

236 ));
234 'methods' => 'GET',
238 add_action('rest_api_init', function () {
237 });

242 ));

251 register_rest_route('sitemap/v1', '/totalpages', array(


243 });

249 });

246 'methods' => 'GET',

241 'callback' => 'wsra_generate_taxonomy_api',

240 'methods' => 'GET',

245 register_rest_route('sitemap/v1', '/author', array(

248 ));

254 ));

252 'methods' => 'GET',

253 'callback' => 'wsra_generate_totalpages_api',

250 add_action('rest_api_init', function () {

247 'callback' => 'wsra_generate_author_api',