Headless WordPress Monorepo Setup

Francis Agulto Avatar

·

In a decoupled environment, maintaining a streamlined workflow is key to creating and managing effective apps and sites. Combining WordPress with a modern JavaScript framework like Next.js in a monorepo setup offers developers an organized way to keep frontend and backend changes in sync. 

By housing both your site’s backend and frontend code within the same repository, you can streamline deployments, simplify local development, and maintain tighter integration between your systems.

This guide will walk you through the steps required for setting up a monorepo that contains both backend WordPress code and frontend Next.js app code and deploying each of them to WP Engine’s Headless Platform. By the end, you’ll understand how to use WP Engine’s tools and best practices to ensure your frontend and backend remain in sync, all while deploying both parts of your stack simultaneously.

Prerequisites

Before diving into this guide, ensure you have the following tools and knowledge in place:

  • Tools Needed:
    • Node.js and npm for managing dependencies and building the frontend.
    • Composer to handle PHP dependencies for WordPress.
    • Git for version control and managing your monorepo.
    • A local WordPress development environment (e.g., Local or an equivalent setup).
    • A WP Engine Headless Platform account which will have an existing WP install for you to target from your local machine as well as the frontend node hosting environment.
  • Basic Knowledge:
    • Familiarity with WordPress and PHP development.
    • An understanding of Next.js fundamentals.
    • An understanding of headless WordPress fundamentals

Having these prerequisites in place will ensure you can follow the guide smoothly and effectively.  If you want to follow the exact file structure and code I use, here is the full repository related to this guide.

Steps

1. Create a local git repository and separate codebases

In your terminal, navigate to the folder where you want your project to reside. Run the commands below to create and navigate into a new my-monorepo-project directory for our project.

`mkdir my-monorepo-project`

`cd my-monorepo-project`
Code language: JavaScript (javascript)

Initialize a Git repository:

`git init`

This will create a .git folder, signaling that Git is now tracking changes in this directory.

Inside your project directory, create two subfolders: one for the frontend and one for the backend:

`mkdir frontend backend`

At this stage, your project structure should look like this:

2. Create WordPress code for the backend with Composer

Now that we have our main directories created, let’s start with WordPress and add to the `backend` directory using Composer.

2 a. What is Composer?

Composer is a dependency manager for PHP that simplifies the process of pulling in libraries and packages. Instead of manually downloading and managing code, Composer lets you specify the dependencies your project needs in a `composer.json` file. It then handles downloading those dependencies into a vendor folder and keeps them up to date. This makes your project more modular, easier to maintain and ensures consistency across environments.


First, create a `.gitignore` at the root of the `backend` folder so we can place the files and folders there that we do not want git to push up into our remote repos:

`touch .gitignore`
Code language: JavaScript (javascript)



Then open up the `.gitignore` file you just created and add this code:

# WordPress core and default directories (managed by Composer)
index.php

# Themes: ignore all themes except our custom demo theme
wordpress/wp-content/themes/*
!wordpress/wp-content/themes/demo-theme/

# Plugins: ignore all plugins except our custom demo plugin
wordpress/wp-content/plugins/*
!wordpress/wp-content/plugins/demo-plugin/

# Must-use plugins (managed by the platform)
wordpress/wp-content/mu-plugins/

# Uploads directory (dynamically generated content)
wordpress/wp-content/uploads/

# Upgrade files (dynamically generated)
wordpress/wp-content/upgrade/

# Composer-related files and directories
vendor/
auth.json
.env
.env.*
!.env.example


Code language: PHP (php)

In this setup, we’re ignoring the majority of WordPress core and default directories because Composer is responsible for pulling in the specified version of WordPress and placing it inside of the `wordpress` directory.  Since WP is a dependency of the project and not our own custom code, we don’t want the WP core files in version control.

You should also disable the “update WordPress” buttons in the WP Engine User Portal and the WordPress dashboard. This ensures that the version of WordPress deployed on the server always matches the version specified and locked by Composer, avoiding any unintentional discrepancies. Here is a link to the guide that explains how to do this.

The `wordpress/wp-content/themes/*` and `wordpress/wp-content/plugins/*` rules ensure all third-party themes and plugins are excluded from version control. The  `!wordpress/wp-content/themes/demo-theme` and ` `!wordpress/wp-content/plugins/demo-plugin` lines ensure that the “demo-theme” theme and “demo-plugin” plugin that contain our project’s custom code are version-controlled.

Upload directories and upgrade files are excluded since these typically contain dynamically generated content that doesn’t belong in version control. 

The `/vendor` directory and other Composer-related files like `auth.json` are ignored because they can be rebuilt with `composer install`. This approach reduces unnecessary clutter in your repository, ensures version control only tracks what you actively manage, and helps maintain a clean and efficient workflow.

2 b. Configure Composer and Add Dependencies

Inside the backend directory, we’ll set up Composer so that WordPress and any required plugins (like WPGraphQL) are installed into a wordpress/ folder rather than the default vendor folder. This approach keeps your WordPress files in a clearly defined location and helps ensure that core files, plugins, and themes remain neatly organized.

Initialize Composer

Open your terminal and navigate to the backend folder:

cd backend
composer init

Follow the prompts to create a basic composer.json. You can accept defaults for most questions or customize them to your preference.

Require WordPress as a Project Dependency

Next, add WordPress to your Composer project:

composer require johnpbloch/wordpress
Code language: JavaScript (javascript)

This command updates your newly created composer.json file to include WordPress as a dependency and downloads the necessary files.

Add WPGraphQL (Optional but Recommended for Headless)

WPGraphQL is a popular plugin for exposing your WordPress data via GraphQL. Let’s install it using the wpackagist.org repository:

composer require wpackagist-plugin/wp-graphql
Code language: JavaScript (javascript)

Update the composer.json Configuration

For WordPress to install into backend/wordpress instead of vendor, you need to include the John P. Bloch core installer and define custom installer paths.

Below is an example composer.json you can use as a reference (feel free to adjust names, versions, and authors as needed):

{
  "name": "myorg/my-headless-wp",
  "version": "1.0.0",
  "authors": [
    {
      "name": "Your Name"
    }
  ],
  "require": {
    "johnpbloch/wordpress": "^6.7",
    "wpackagist-plugin/wp-graphql": "^2.0"
  },
  "minimum-stability": "dev",
  "repositories": [
    {
      "type": "composer",
      "url": "https://wpackagist.org",
      "only": [
        "wpackagist-plugin/*",
        "wpackagist-theme/*"
      ]
    }
  ],
  "extra": {
    "installer-paths": {
      "wordpress/wp-content/plugins/{$name}/": [
        "type:wordpress-plugin"
      ],
      "wordpress/wp-content/themes/{$name}/": [
        "type:wordpress-theme"
      ]
    }
  },
  "config": {
    "allow-plugins": {
      "johnpbloch/wordpress-core-installer": true,
      "composer/installers": true
    }
  }
}

Code language: JSON / JSON with Comments (json)

installer-paths: Tells Composer to place plugins in wordpress/wp-content/plugins/ and themes in wordpress/wp-content/themes/ instead of the default vendor folder.

repositories: Defines wpackagist.org as a source for WordPress plugins and themes, allowing you to install them via Composer.

allow-plugins: Enables the custom installers needed to place WordPress core in wordpress/ and plugins/themes in their correct directories.

Install Dependencies
Once you’ve finalized your composer.json file, install the dependencies:

composer install

Composer will download WordPress into backend/wordpress/, along with any specified plugins into backend/wordpress/wp-content/plugins/. For example, WPGraphQL should appear at:

backend/
  └─ wordpress/
      └─ wp-content/
          └─ plugins/
              └─ wp-graphql/

2 c. Install Composer (if not already installed):

Before installing Composer, make sure PHP and cURL are installed on your system. If you’re missing either, refer to the official documentation for your operating system:

Once PHP and cURL are installed, you can proceed with installing Composer:

`curl -sS https://getcomposer.org/installer | php
sudo mv composer.phar /usr/local/bin/composer`

Code language: JavaScript (javascript)

2 d. Verify WordPress Installation

After running composer install, open your code editor to confirm that WordPress has been successfully installed into the backend/wordpress folder. For example, you should see the following structure:

backend/
  ├─ .gitignore
  ├─ composer.json
  ├─ composer.lock
  └─ wordpress/
      ├─ wp-admin/
      ├─ wp-content/
      ├─ wp-includes/
      └─ ...

Within wp-content/plugins, you’ll also find any Composer-installed plugins, such as WPGraphQL:

backend/
  └─ wordpress/
      └─ wp-content/
          └─ plugins/
              └─ wp-graphql/

You should see this if you open up the code editor:

At this stage, you’ve successfully set up Composer in the backend directory, configured your project to install WordPress core and plugins into a dedicated wordpress folder, and used .gitignore to keep unnecessary files out of version control. You’re now ready to start configuring WordPress and connecting it to the rest of your monorepo setup. The next thing we need to do is create our frontend.

3. Create the Frontend App

Since we are focusing on the monorepo setup in headless, you can go ahead and clone down the repo related to this article here:

https://github.com/Fran-A-Dev/monorepo-for-headlessWP

The example repo provided uses the Next.js App Router, but you can apply the concepts of this article to other versions of Next.js, or your preferred frontend framework.

3 a. Add environment variables

After the project is created, you’ll need to provide the WordPress GraphQL endpoint so the frontend can fetch data from the backend. Create a .env.local file in the frontend directory and add:

NEXT_PUBLIC_WPGRAPHQL_ENDPOINT=https://your-wpengine-site.com/graphql`

Code language: JavaScript (javascript)

Replace https://your-wpengine-site.com/graphql with the correct URL of your WordPress install’s GraphQL API.  (Just a reminder, should you choose another frontend, their variables in key and value for your endpoint would differ)

3 b. Verify the configuration:

Open your new frontend project in a code editor and inspect the folders. Ensure that the NEXT_PUBLIC_GRAPHQL_ENDPOINT is correctly set by using process.env.NEXT_PUBLIC_GRAPHQL_ENDPOINT in your components or data-fetching functions and running `npm run dev` in the terminal to ensure that it’s working on the browser.


If you are following along with my repo, I created an example as the home page of the app to display the first 5 post titles and their content from WordPress.  Here is the example file from my repo:

app/page.jsx

import Link from "next/link";
import { Suspense } from "react";
import Loading from "./loading";

async function getPosts() {
  const query = `
  {
    posts(first: 5) {
      nodes {
        title
        content
        uri
      }
    }
  }
  `;

  const res = await fetch(
    `${process.env.NEXT_PUBLIC_GRAPHQL_ENDPOINT}?query=${encodeURIComponent(
      query
    )}`,
    { next: { revalidate: 10 } },
    {
      method: "GET",
      headers: {
        "Content-Type": "application/json",
        // ... any other headers you need to include (like authentication tokens)
      },
    }
  );

  const { data } = await res.json();

  return data.posts.nodes;
}

export default async function PostList() {
  const posts = await getPosts();

  return (
    <Suspense fallback={<Loading />}>
      <div>
        {posts.map((post) => (
          <div key={post.uri} className="card">
            <Link href={`post${post.uri}`}>
              <div className="border-t border-gray-300 pt-2 mb-2">
                <h3 className="mb-2">{post.title}</h3>
                <h4>test 10</h4>
                <div
                  className="mt-1"
                  dangerouslySetInnerHTML={{
                    __html: post.content.slice(0, 200) + "...",
                  }}
                />
              </div>
            </Link>
          </div>
        ))}
      </div>
    </Suspense>
  );
}

Code language: JavaScript (javascript)

4. Create a remote GitHub repository

Now that we have our frontend and backend code set up, let’s push it to a remote GitHub repo.

Here are links to creating a new repository on GitHub and then adding it as a remote for this project.

5. Create and configure the GitHub Action and `yml` file

Now that our frontend and backend directories are set up, we need to automate the deployment process using GitHub Actions and target our remote WP install on WP Engine. 

WP Engine has great documentation and how-to guides for this feature here that you can follow step by step here:

https://wpengine.com/support/github-action-deploy

6. Push the code up to your remote repository and test the connection

Now that everything is set up and connected via SSH, it’s time to commit your code and confirm that your GitHub Actions workflow is running properly. Once the workflow completes, you’ll verify the changes on WP Engine’s hosting environment.

6 a.  Stage and commit changes


In your local repository, navigate to the root directory of your project. Remember to add changes to the files we purposely wanted to reflect in our remote WP Engine WP install and GitHub repo. For this article, this included anything on the frontend code that would render in the browser and the custom plugin and theme we created in WordPress.

Run the following commands to stage and commit your backend and frontend changes:

git add .
git commit -m "Initial commit of backend and frontend code"
git push origin main
Code language: JavaScript (javascript)

We are committing both the backend and frontend directories since they’re housed together in the same repository. This ensures the changes you’ve made to your WordPress files and Next.js app are included.

6 b. Check the GitHub Actions workflow

Now that we have pushed our code to our remote GitHub repository, let’s check our GH actions UI to see if it is working properly.

  1. Open your GitHub repository in the browser.
  2. Go to the Actions tab and click on it.


3. You should see a running workflow for the deployment process. Click on it to view the logs and ensure everything runs without errors.

4. If you see a green checkmark, your code has been successfully deployed to WP Engine.

6 c. Verify changes on WP Engine’s headless WordPress environment

  1. Log in to the WP Engine User Portal and navigate to your WordPress environment.
  2. Open your site in the browser and confirm that the changes you made such as new or updated content, custom plugins, or themes – are reflected.
  3. If you’re using a custom headless frontend, check that your site is showing the correct data from your WordPress backend.

If you are new to the WP Engine user portal that features the headless WP hosting platform along with your WP installs (both headless and traditional) please refer to their documentation on navigating the user portal here:

https://developers.wpengine.com/docs/atlas/getting-started/create-app/

https://wpengine.com/support/wp-engine-user-portal

Conclusion 

Headless WordPress and a monorepo setup can get a bit daunting and confusing.  Fortunately, WP Engine has tools on their platform to make deployment easier on both your frontend and backend, especially when you house it in one repo.  We hope you have a better understanding of how to do so with this article.

As always, we’re super stoked to hear your feedback and learn about the headless projects you’re working on, so hit us up in our Discord!