loading...
Cover image for CRA+Craft : How to use Create React App in a Craft CMS multiple pages site

CRA+Craft : How to use Create React App in a Craft CMS multiple pages site

fraqe profile image fraqe ・9 min read

TL;DR: Customize Create React App to use it as a frontend foundation for a multiple page site powered by Craft CMS.


There are a lot of articles that already cover how to use Craft CMS with React but they focus mainly on the usage of Graphql. In this post we will see how to use the great Create React App with a template, in conjunction with Craft CMS, to get the best of both worlds: a community optimized React app, battle-tested front-end tools, real multiple entry points with different bundle per pages.

We’ll be going together throughout all the process, there is quite a journey ahead of us, so let’s get started.

I recommend to have at least basic knowledges about Craft CMS, PHP, CRA, and a good comprehension of Webpack

  1. Installing CRA with a template
  2. Installing Craft CMS
  3. Customize CRA using craco
  4. Change dev and build paths
  5. Add support for multiple entry points
  6. Add Tailwind and tweak Postcss
  7. Add another plugin
  8. Injecting the js and css files associated with each template when necessary

Installing CRA with a template

Since v3.3 react-scripts support templates, we’ll use the wonderful react boilerplate template especially the v5-with-cra branch which add Typescript support, but it’s not mandatory, you can use any templates, or no template at all, depending on your needs, anyway lets continue:

For what follows our project will be named cracraft, and CRA refer to Create react app

$ npx create-react-app --template cra-template-rb cracraft

Fix A template was not provided error

If you ever come across this error:

A template was not provided. This is likely because you're using an outdated version of create-react-app.
Please note that global installs of create-react-app are no longer supported.

First, remove all globally installed version of CRA:

  • with npm uninstall -g create-react-app
  • or yarn global remove create-react-app

Then if yarn gives you this: error This module isn't specified in a package.json file. don't trust him blindly, run:

$ readlink which `create-react-app`

And if you get a path, it’s another version of CRA package remaining in your system; so remove it, and try again.


Once installed, cd into the directory and run npm start to make sure that everything is running smoothly.

Installing Craft CMS

Installing Craft CMS is quite simple, thanks to composer, but there is a small problem: we can’t install it in a non-empty directory, so we should do it in two steps:

  • first install craft in a temporary directory composer create-project craftcms/craft cracraft-temp
  • then, once finished, move all files in the directory where we installed CRA previously and delete the temporary cracraft-temp directory

You can now follow the rest of the installation process here: https://craftcms.com/docs/3.x/installation.html#step-2-set-the-file-permissions

The purpose of this setup is not only to integrate CRA in a Twig template, it will do the job for a single page application, but for a website with multiple pages, where some page could actually contain elaborate widgets or complex applications, and others just need a few lines of javascript or no javascript at all... we need more flexibility.

But as well designed as CRA is, it is not really flexible, and it makes sense, because it was made to be a SPA: there is only one html page and you inject the whole bundle or nothing. At this point, we now have on one side a multi-page site powered by Craft CMS and on the other side a SPA powered by CRA, we need to fusion the two.

Customize CRA using craco

Let’s customize CRA to play well with Craft CMS, the goal is to tweak the Webpack configuration without ejecting, and thus keep the advantages of being able to update either CRA or the template.
There are a couple of options for customization:

Craco has my preference, because I like the way it handles the tweaking of different parts, it exposes the Webpack configuration and we can overwrite almost anything we want. Add it to the project:

$ npm install @craco/craco --save

Next, create in the root directory, a file that will contains all of our modifications, name it craco.config.js.

And finally update the start and build script command to use craco instead of react-scripts.

In package.json

"scripts": {
  "start": "craco start",
  "build": "craco build",
  "test": "craco test",
  ...
},

If you want to know more about the full details of the craco installation, it’s here

That’s it, time to write some code.

At this point we want to:

  • Change some dev and build paths to match our Craft folder structure
  • Set up multiple entry points to be able to inject different bundle in different pages
  • Add and modify Webpack plugins
  • Add Tailwind and tweak Postcss
  • Retrieve the js and css files associated with each pages when needed

We will share variables between CRA and Craft CMS, the easiest way is to add them in the existing .env file:

WDS_SOCKET_HOST=localhost
WDS_SOCKET_PORT=3000
PUBLIC_PATH="http://localhost:3000/"
MANIFEST_PATH=/asset-manifest.json
FAST_REFRESH=true

FAST_REFRESH enable react-refresh, it’s not mandatory to enable it, but it’s allow to improve components updates

Change dev and build paths

Assuming that the src directory contains all the javascript and styles sources, and that we want to output the result of the build step in web/dist:

cracraft
├── src
│   ├── styles
│   │   ├── **/*.css
│   ├── app
│   │   ├── **/*.tsx
│   ├── js
│   │   ├── **/*.js
│   └── ...
├── web
│   ├── dist
│   └── ...
├── templates
├── craco.config.js
├── .env
└── ...

We have to tell Webpack where our files are and where we want the output, both for dev and build mode:

In craco.config.js

const {
  whenDev,
  whenProd,
} = require('@craco/craco');

module.exports = function ({ env }) {
  return {
    webpack: {
      configure: (webpackConfig, { env, paths }) => {
        whenDev(() => {
          webpackConfig.output.publicPath = process.env.PUBLIC_PATH;
        });

        whenProd(() => {
          const buildPath = `${paths.appPath}/web${process.env.PUBLIC_PATH}`;

          paths.appBuild = buildPath;
          webpackConfig.output.path = buildPath;
          webpackConfig.output.publicPath = process.env.PUBLIC_PATH;

          return webpackConfig;
        });
      }
    },
    plugins: [],
    style: {}
  }
}

Using a local domain like .local

To avoid CORS errors between your local domain and the dev server, add a header to the webpack dev server, using craco’s ability to change the configuration of the dev server.

In craco.config.js

...
plugins: [
  {
    plugin: {
      overrideDevServerConfig: ({
        devServerConfig,
        cracoConfig,
        pluginOptions,
        context: { env, paths, proxy, allowedHost },
      }) => {
        devServerConfig.headers = {
          'Access-Control-Allow-Origin': '*',
        };
        return devServerConfig;
      },
    }
  },
],
...

Add support for multiple entry points

CRA doesn’t support multiple entry points out of the box, so we have to reconfigure Webpack to add some. Let’s say we have 3 different pages:

  • home where we want to use Typescript
  • editor a page containing the react SPA
  • about a page that need only a simple javascript snippet

NB: Dont forget to create these pages and their associated Twig templates

In craco.config.js add our 3 entry points

...
module.exports = function ({ env }) {
  return {
    webpack: {
      configure: (webpackConfig, { env, paths }) => {
        const entries = {
          index: [...webpackConfig.entry],
          home: [`${paths.appSrc}/js/home.ts`],
          about: [`${paths.appSrc}/js/about/index.js`],
        };
        ...
      }
    }
  }
}

Note: We keep index as name for the entry point of the react SPA

It won’t work yet, because the ManifestPlugin already used in CRA will cause a problem, it is configured to support a single entry point. And in order to overwrite the config of a Webpack plugin we need to replace it.

Install the plugin:

$ npm i ManifestPlugin -D

Create a new instance of the plugin, and replace the existing one in the plugin array:

In craco.config.js

...
module.exports = function ({ env }) {
  return {
    webpack: {
      configure: (webpackConfig, { env, paths }) => {
        ...
        // Substitute ManifestPlugin:
        const pluginPosition = webpackConfig.plugins.findIndex(
          ({ constructor }) => constructor.name === 'ManifestPlugin',
        );

        const multipleEntriesManifestPlugin = new ManifestPlugin({
          fileName: 'asset-manifest.json',
          publicPath: paths.publicUrlOrPath,
          generate: (seed, files, entrypoints) => {
            const manifestFiles = files.reduce((manifest, file) => {
              manifest[file.name] = file.path;
              return manifest;
            }, seed);

            // Keep the existing entry point
            const indexEntrypointFiles = entrypoints.index.filter(
              fileName => !fileName.endsWith('.map'),
            );

            let { index, ...pagesAllEntryPointFiles } = entrypoints;
            // Create our pages entry points
            const pagesEntryPointFiles = Object.keys(
              pagesAllEntryPointFiles,
            ).reduce((filtered, entryKey) => {
              filtered[entryKey] = pagesAllEntryPointFiles[entryKey].filter(
                fileName => !fileName.endsWith('.map'),
              );
              return filtered;
            }, {});

            return {
              files: manifestFiles,
              entrypoints: indexEntrypointFiles,
              pages: pagesEntryPointFiles,
            };
          },
        });

        webpackConfig.plugins.splice(
          pluginPosition,
          1,
          multipleEntriesManifestPlugin,
        );
        ...
      }
    }
  }
}

And hop! It’s done, we’ve just substituted the plugin.

We’re almost done, there is one step left to support our new entry points, we have to add the webpackHotDevClient for each one to support HMR

In craco.config.js

...
whenDev(() => {
  webpackConfig.output.publicPath = process.env.PUBLIC_PATH;
  webpackConfig.optimization.runtimeChunk = 'single';

  const webpackHotDevClientPath = require.resolve(
    'react-dev-utils/webpackHotDevClient',
  );
  Object.keys(entries).forEach(entryKey => {
    if (!entries[entryKey].includes(webpackHotDevClientPath)) {
      entries[entryKey].unshift(webpackHotDevClientPath);
    }
  });
});
...

Quick tips: debuging your customizations

If you customize CRA and come across a tricky bug, remember that you can still debug the process like any nodejs app by adding the --inspect flag to your npm script command craco --inspect build

Add Tailwind and tweak Postcss

Once everything is in place and the dev server plus the build step are running without any errors, we can customize further to integrate all our needs, for the demonstration we’ll add a favicon plugin, customize Postcss and use Tailwind css framework.

So first, Tailwind and Postcss, it’s quite straightforward, start by adding the necessary packages:

$ npm i -D postcss-import postcss-preset-env tailwindcss

In the root directory of the project create a tailwind.config.js file.

In craco.config.js add our Postcss configuration:

...
style: {
  postcss: {
    plugins: [
      require('postcss-import')({
        plugins: [require('stylelint')],
        path: ['./node_modules'],
      }),
      require('tailwindcss')('./tailwind.config.js'),
      require('postcss-preset-env')({
        autoprefixer: {},
        features: {
          'nesting-rules': true,
        },
      })
    ],
  },
},
...

And to make it perfect, we need to tell Stylelint to chill out with some unorthodox rules used in Tailwind.
Add these rules to the .stylelintrc config file:

"rules": {
    "at-rule-no-unknown": [ true, {
      "ignoreAtRules": [
        "screen",
        "extends",
        "responsive",
        "tailwind"
      ]
    }],
    "block-no-empty": null
  }

Add another plugin

Next, add the Favicons Webpack Plugin, here it’s even simplier because we just have to push it in the Webpack config plugin array, provided by craco, like this:

whenProd(() => {
  ...

  webpackConfig.plugins.push(
    new FaviconsWebpackPlugin({
      logo: './src/img/favicon-src.png',
      prefix: 'img/favicons/',
      cache: true,
      inject: 'force',
      favicons: {
        appName: 'Cracraft',
        appDescription: 'Create react app and Craft play well together',
        developerName: 'Dev name',
        developerURL: 'name@mail.dev',
        path: 'web/dist/',
      },
    }),
  );
});

Note that when you add a plugin which is already used in the CRA config you have to replace it, using the same trick as the one used previously for the ManifestPlugin.

Injecting the js and css files associated with each templates when necessary

Ooook now that CRA is customized, there is one last step to link it to Craft CMS: we need to retrieve the content of the different endpoints, and, since the manifest file is a plain json file, it’s easy to read it and get the parts we need.

How are we going to do that?

  • Quick answer: this can be done with a Twig function
  • Long answer: there is a better way to do it, but we’ll talk about it in another post, as this one is starting to be quite long (congrats if you're still reading from the beginning).

So, lets write a simple Twig function that will load our manifest file and create the HTML tags.

First install a PHP implementation of JsonPath

$ composer require galbar/jsonpath

In the file where you declare your Twig extensions, import all the dependencies:

use craft\helpers\Html;
use craft\helpers\Template;
use craft\helpers\Json as JsonHelper;
use JsonPath\JsonObject;

And add a function that will get the content of the manifest file and return the paths of the chunks we are looking for; let’s call it getEntryPointChunks, and it will take a $jsonPath param, I let you go through the code:

public function getEntryPointChunks(string $path)
{
    $publicPath = getenv('PUBLIC_PATH');
    $manifestPath = getenv('MANIFEST_PATH');
    $manifestContent = file_get_contents($publicPath.$manifestPath);
    $manifest = JsonHelper::decodeIfJson($manifestContent);
    $jsonObject = new JsonObject($manifestContent);

    $moduleList = $jsonObject->get($jsonPath);

    if (!$moduleList) {
        return null;
    }

    // Ensure flat array, ex: if [*] is forgotten in the json path to an array
    if (is_array($moduleList)) {
        $flattened = [];
        array_walk_recursive($moduleList, function ($item) use (&$flattened) {
            $flattened[] = $item;
        });

        $moduleList = $flattened;
    }

    $moduleTags = [];
    foreach ($moduleList as $k => $m) {
        if (strpos($m, '.hot-update.js') === false) {
            $moduleName = preg_replace('/^\//', '', $m);
            if (preg_match('/\.css(\?.*)?$/i', $moduleName)) {
                $moduleTags[] = Html::cssFile("$publicPath/$moduleName");
            } elseif (preg_match('/\.js(\?.*)?$/i', $moduleName)) {
                $moduleTags[] = Html::jsFile("$publicPath/$moduleName");
            } elseif (preg_match('/\.(svg|png|jpe?g|webp|avif|gif)(\?.*)?$/i', $moduleName)) {
                $moduleTags[] = Html::img("$publicPath/$moduleName");
            } else {
                $moduleTags[] = "$publicPath/$moduleName";
            }
        }
    }

    return Template::raw(implode("\r\n", $moduleTags));
}

And now, finally, call our function from any twig template:

{# Inject the spa react app #}
{{getEntryPointChunks('$.entrypoints')}}

and for another page

{# Inject vanilla javascript #}
{{getEntryPointChunks('$.pages.about')}}

NB: don’t forget to start the development server before accessing your Craft site $ npm start


And that’s it, this is the end, we have an (almost) complete fusion between CRA and Craft CMS with a multi entry points setup and a customizable webpack configuration.

Next time we will complete this setup with a better way to integrate all this with Craft CMS, because even though the Twig function does the job, there is room for improvement, for now enjoy this setup to harness the full power of CRA+Craft.

Thanks for reading, hope you learned something new and it will help you.

Discussion

pic
Editor guide