Skip to content

Instantly share code, notes, and snippets.

@iamlos
Created March 19, 2025 09:39
Show Gist options
  • Save iamlos/211310a7f9308f724c983a8e5a1d6ab0 to your computer and use it in GitHub Desktop.
Save iamlos/211310a7f9308f724c983a8e5a1d6ab0 to your computer and use it in GitHub Desktop.
Prompt: Implementing WordPress as a Headless CMS in a Next.js Application

Implementing WordPress as a Headless CMS in a Next.js Application

This guide provides step-by-step instructions for implementing WordPress as a headless CMS in a Next.js application using Faust.js and GraphQL. This approach allows you to use WordPress for content management while leveraging Next.js for the frontend.

Prerequisites

  • A WordPress site with admin access
  • Node.js and npm/yarn installed
  • Basic knowledge of React and Next.js
  • Basic understanding of GraphQL

Step 1: Set Up WordPress

  1. Install Required WordPress Plugins:

    • WPGraphQL: Exposes your WordPress data as a GraphQL API
    • FaustWP: Connects your WordPress site to your headless frontend
    # These can be installed via the WordPress admin dashboard or using WP-CLI
    wp plugin install wp-graphql --activate
    wp plugin install faustwp --activate
  2. Configure FaustWP Plugin:

    • Go to WordPress Admin → Settings → Headless
    • Set your frontend URL (your Next.js app URL)
    • Generate a secret key for preview functionality
    • Save settings

Step 2: Create a Next.js Project

  1. Initialize a New Next.js Project:

    npx create-next-app my-headless-wp
    cd my-headless-wp
  2. Install Required Dependencies:

    npm install @apollo/client graphql @faustwp/core @faustwp/cli

Step 3: Configure Environment Variables

  1. Create a .env.local File:

    # WordPress
    NEXT_PUBLIC_WORDPRESS_API_URL=https://your-wordpress-site.com/graphql
    NEXT_PUBLIC_WORDPRESS_IMAGE_DOMAIN=your-wordpress-site.com
    
    # WordPress Preview (optional)
    WORDPRESS_AUTH_REFRESH_TOKEN=
    WORDPRESS_PREVIEW_SECRET=your-secret-key
    

Step 4: Set Up Apollo Client for WordPress

  1. Create a WordPress Client File (lib/wordpress-client.ts):

    import { ApolloClient, InMemoryCache, createHttpLink } from '@apollo/client';
    
    const cache = new InMemoryCache();
    
    const link = createHttpLink({
      uri: process.env.NEXT_PUBLIC_WORDPRESS_API_URL,
      credentials: 'same-origin'
    });
    
    export const client = new ApolloClient({
      link,
      cache,
      defaultOptions: {
        query: {
          fetchPolicy: 'no-cache',
        },
      },
    });
    
    export default client;
  2. Create a WordPress Utility File (lib/wordpress.ts):

    import { ApolloClient, InMemoryCache, createHttpLink, FetchPolicy, gql } from '@apollo/client';
    import type { WPPost, WPPage } from '@/types/wordpress';
    
    const defaultOptions = {
      watchQuery: {
        fetchPolicy: 'no-cache' as const,
        errorPolicy: 'all' as const,
      },
      query: {
        fetchPolicy: 'no-cache' as FetchPolicy,
        errorPolicy: 'all' as const,
      },
    };
    
    const cache = new InMemoryCache({
      typePolicies: {
        Post: {
          keyFields: ["id", "slug"],
        },
        Page: {
          keyFields: ["id", "slug"],
        },
      },
    });
    
    const link = createHttpLink({
      uri: process.env.NEXT_PUBLIC_WORDPRESS_API_URL,
    });
    
    export const client = new ApolloClient({
      link,
      cache,
      defaultOptions,
    });
    
    // Define your GraphQL queries
    export const POSTS_QUERY = gql`
      query Posts($first: Int!) {
        posts(first: $first, where: { orderby: { field: DATE, order: DESC } }) {
          nodes {
            id
            title
            excerpt
            slug
            date
            featuredImage {
              node {
                sourceUrl
                altText
              }
            }
            author {
              node {
                name
                avatar {
                  url
                }
              }
            }
            categories {
              nodes {
                id
                name
                slug
              }
            }
          }
        }
      }
    `;
    
    export const POST_QUERY = gql`
      query Post($uri: ID!) {
        post(id: $uri, idType: URI) {
          id
          title
          content
          slug
          date
          featuredImage {
            node {
              sourceUrl
              altText
            }
          }
          author {
            node {
              name
              avatar {
                url
              }
            }
          }
          categories {
            nodes {
              id
              name
              slug
            }
          }
        }
      }
    `;
    
    // Add more queries as needed

Step 5: Define WordPress Types

  1. Create WordPress Types (types/wordpress.ts):

    export interface WPImage {
      sourceUrl: string;
      altText?: string;
    }
    
    export interface WPAuthor {
      node: {
        name: string;
        avatar?: {
          url: string;
        };
      };
    }
    
    export interface WPCategory {
      node: {
        name: string;
        uri: string;
      };
    }
    
    export interface WPPost {
      id: string;
      title: string;
      content: string;
      slug: string;
      uri?: string;
      date: string;
      excerpt: string;
      categories?: {
        nodes: WPCategory[];
      };
      featuredImage?: {
        node: WPImage;
      };
      author?: WPAuthor;
    }
    
    export interface WPPage {
      id: string;
      title: string;
      content: string;
      slug: string;
      uri?: string;
      template?: {
        templateName: string;
      };
      featuredImage?: {
        node: WPImage;
      };
    }
    
    export interface WPPaginationVars {
      first: number;
      after?: string;
    }
    
    export interface WPPostsResponse {
      posts: {
        nodes: WPPost[];
        pageInfo: {
          hasNextPage: boolean;
          endCursor: string;
        };
      };
    }

Step 6: Create Blog Pages

  1. Create a Blog Index Page (app/blog/page.tsx):

    import client from '@/lib/wordpress-client';
    import type { WPPost } from '@/types/wordpress';
    import { POSTS_QUERY } from '@/lib/wordpress';
    import Link from 'next/link';
    
    export default async function BlogPage() {
      const { data } = await client.query({
        query: POSTS_QUERY,
        variables: {
          first: 10,
        },
      });
    
      const posts = data?.posts?.nodes || [];
    
      return (
        <div className="container mx-auto px-4 py-8">
          <h1 className="text-3xl font-bold mb-8">Blog Posts</h1>
          <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
            {posts.map((post: WPPost) => (
              <article key={post.uri || post.slug} className="border rounded-lg p-4">
                {post.featuredImage?.node && (
                  <img
                    src={post.featuredImage.node.sourceUrl}
                    alt={post.featuredImage.node.altText || post.title}
                    className="w-full h-48 object-cover rounded-md mb-4"
                  />
                )}
                <h2 className="text-xl font-semibold mb-2">{post.title}</h2>
                <div 
                  className="text-gray-600"
                  dangerouslySetInnerHTML={{ __html: post.excerpt }}
                />
                <Link href={`/blog/${post.slug}`} className="text-blue-600 hover:underline mt-4 inline-block">
                  Read more
                </Link>
              </article>
            ))}
          </div>
        </div>
      );
    }
  2. Create a Blog Post Page (app/blog/[slug]/page.tsx):

    import { cookies } from 'next/headers';
    import { POST_QUERY, isPreviewMode, getClient } from '@/lib/wordpress';
    import type { WPPost } from '@/types/wordpress';
    
    export default async function PostPage({ params }: { params: { slug: string } }) {
      const { data } = await client.query({
        query: POST_QUERY,
        variables: {
          uri: params.slug,
        },
      });
    
      if (!data?.post) return <div>Post not found</div>;
    
      const post = data.post as WPPost;
      const categories = post.categories?.nodes || [];
    
      return (
        <article className="container mx-auto px-4 py-8 max-w-4xl">
          <h1 className="text-4xl font-bold mb-4">{post.title}</h1>
        
          {post.featuredImage?.node && (
            <img
              src={post.featuredImage.node.sourceUrl}
              alt={post.featuredImage.node.altText || post.title}
              className="w-full h-64 object-cover rounded-lg mb-6"
            />
          )}
    
          <div className="flex items-center text-gray-600 mb-6">
            <span>By {post.author?.node?.name}</span>
            <span className="mx-2"></span>
            <span>{new Date(post.date).toLocaleDateString()}</span>
            {categories.length > 0 && (
              <>
                <span className="mx-2"></span>
                <span>
                  {categories.map((cat) => cat.name).join(', ')}
                </span>
              </>
            )}
          </div>
    
          <div 
            className="prose max-w-none"
            dangerouslySetInnerHTML={{ __html: post.content }}
          />
        </article>
      );
    }

Step 7: Set Up Preview Functionality (Optional)

  1. Create a Preview Client (lib/preview-client.ts):

    import { ApolloClient, InMemoryCache, createHttpLink } from '@apollo/client';
    import { setContext } from '@apollo/client/link/context';
    
    const cache = new InMemoryCache();
    
    /**
     * Create an Apollo Client instance configured for WordPress preview mode
     * This client includes authentication headers for preview requests
     */
    export function createPreviewClient(token?: string) {
      // Create the standard HTTP link
      const httpLink = createHttpLink({
        uri: process.env.NEXT_PUBLIC_WORDPRESS_API_URL,
        credentials: 'same-origin'
      });
    
      // Add auth headers if token is provided
      const authLink = setContext((_, { headers }) => {
        if (!token) {
          return { headers };
        }
    
        // Return the headers to the context so httpLink can read them
        return {
          headers: {
            ...headers,
            authorization: token ? `Bearer ${token}` : '',
          }
        };
      });
    
      return new ApolloClient({
        link: authLink.concat(httpLink),
        cache,
        defaultOptions: {
          query: {
            fetchPolicy: 'no-cache',
          },
        },
      });
    }
  2. Create a Preview API Route (app/api/preview/route.ts):

    import { NextResponse } from 'next/server';
    
    export async function GET(request: Request) {
      const { searchParams } = new URL(request.url);
      const secret = searchParams.get('secret');
      const slug = searchParams.get('slug');
      const postType = searchParams.get('postType') || 'post';
    
      // Check the secret and next parameters
      if (secret !== process.env.WORDPRESS_PREVIEW_SECRET || !slug) {
        return NextResponse.json(
          { message: 'Invalid token or missing slug' },
          { status: 401 }
        );
      }
    
      // Enable Preview Mode by setting the cookies
      const response = NextResponse.redirect(
        new URL(`/${postType}/${slug}`, request.url)
      );
      
      response.cookies.set('__preview_mode', 'true', {
        path: '/',
        httpOnly: true,
        sameSite: 'strict'
      });
    
      return response;
    }
  3. Add Preview Mode Detection (add to lib/wordpress.ts):

    /**
     * Check if the current request is in preview mode
     */
    export function isPreviewMode(cookies: any): boolean {
      return cookies.get('__preview_mode')?.value === 'true';
    }
    
    /**
     * Get the appropriate client based on preview mode
     */
    export function getClient(preview: boolean = false, token?: string) {
      if (preview && token) {
        // Use dynamic import to avoid loading on the server unnecessarily
        const { createPreviewClient } = require('./preview-client');
        return createPreviewClient(token);
      }
      
      return client;
    }

Step 8: Set Up Faust.js Integration (Optional)

  1. Create a Faust Config File (faust.config.js):

    import { config } from '@faustwp/core';
    
    export default config({
      wpUrl: process.env.NEXT_PUBLIC_WORDPRESS_URL || 'https://your-wordpress-site.com',
      apiClientSecret: process.env.WORDPRESS_PREVIEW_SECRET,
    });
  2. Create a Faust API Route (app/api/faust/[[...route]].js):

    import "../../../faust.config";
    
    export { apiRouter as default } from "@faustwp/core";
  3. Create a WordPress Node Catch-All Route (app/[...wordpressNode].js):

    import { getWordPressProps, WordPressTemplate } from "@faustwp/core";
    
    export default function Page(props) {
      return <WordPressTemplate {...props} />;
    }
    
    export function getStaticProps(ctx) {
      return getWordPressProps({ ctx });
    }
    
    export async function getStaticPaths() {
      return {
        paths: [],
        fallback: "blocking",
      };
    }

Step 9: Set Up Template Mapping

  1. Create a Template Mapping File (lib/template-mapping.ts):

    /**
     * Template mapping configuration
     * Maps WordPress template names to Next.js page components
     */
    
    export interface TemplateMapping {
      [key: string]: string;
    }
    
    /**
     * Maps WordPress template names to Next.js routes
     * Default template is used when no specific template is found
     */
    export const templateMapping: TemplateMapping = {
      'default': '/page/[slug]',
      'template-full-width': '/page/full-width/[slug]',
      'template-contact': '/contact',
      'template-about': '/about',
      'template-blog': '/blog'
    };
    
    /**
     * Get the appropriate Next.js route based on WordPress template
     */
    export function getTemplateRoute(templateName: string | undefined, slug: string): string {
      if (!templateName) {
        return `/page/${slug}`;
      }
      
      const route = templateMapping[templateName] || templateMapping['default'];
      return route.replace('[slug]', slug);
    }

Step 10: Configure Next.js for WordPress Images

  1. Update Next.js Config (next.config.mjs):

    /** @type {import('next').NextConfig} */
    const nextConfig = {
      images: {
        remotePatterns: [
          {
            protocol: "https",
            hostname: process.env.NEXT_PUBLIC_WORDPRESS_IMAGE_DOMAIN || "your-wordpress-site.com",
            pathname: '/wp-content/uploads/**',
          }
        ],
      },
    };
    
    export default nextConfig;

Step 11: Create WordPress Templates (Optional)

  1. Create a WordPress Single Post Template (app/wp-templates/single.js):

    import { gql } from "@apollo/client";
    
    export default function SingleTemplate(props) {
      const { title, content } = props.data.post;
    
      return (
        <>
          <h1>{title}</h1>
          <div dangerouslySetInnerHTML={{ __html: content }} />
        </>
      );
    }
    
    SingleTemplate.query = gql`
      query GetPost($uri: ID!) {
        post(id: $uri, idType: URI) {
          title
          content
        }
      }
    `;
    
    SingleTemplate.variables = (seedQuery, ctx) => {
      return {
        uri: seedQuery?.uri,
      };
    };
  2. Register Templates (app/wp-templates/index.js):

    import SingleTemplate from "./single";
    
    const templates = {
      single: SingleTemplate,
    };
    
    export default templates;

Conclusion

You've now successfully set up a headless WordPress integration with Next.js! This approach gives you the best of both worlds: WordPress's powerful content management capabilities and Next.js's performance and developer experience.

With this setup, you can:

  • Use WordPress as a headless CMS
  • Fetch data from WordPress using GraphQL
  • Create dynamic routes based on WordPress content
  • Preview content before publishing
  • Use WordPress templates with Next.js components

Remember to customize the queries and components based on your specific needs. The WordPress GraphQL schema provides many more fields and connections that you can use to build rich, dynamic websites.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment