Adding React to Ghost

My goal is to add a payments integration to my ghost theme. I'm doing that by loading a react app within one of my theme's pages that will communicate with an external API.

Adding React to Ghost

I really enjoy hosting my site with ghost, and there are a lot of different integrations that can be used to extend functionality. I've been wanting to add a "store", but want something more functional than most of the current integration's "buy now" buttons.

I figured a full-fledged solution with a payment provider such as square or stripe would work well; however, Ghost uses handlebar templates and doesn't have a good solution for that kind of integration.

Enter React. I recently created an API that handles post publishes to X and Mastodon, so the goal is to extend the API to support payments while using a react front-end for my store inside of the ghost theme.

Setup

My theme's based off of the official starter theme, and hasn't changed much. I wanted to keep my API, theme, and react files all in a single repo though, so I restructured things a bit:

- Repo
  - API (My .Net API)
    - Api
    - Core
    - Domain
    - Infrastructure
  - Theme (The starter ghost theme)
    - assets
    - members
    - partials
    - store (My react app)

The store directory was created by setting up a base react app using Vite

Config

Next, I had to update the rollup.config.js used by ghost to build the theme and combine all of the files. Three additional plugins were installed:

npm install -D @svgr/rollup @rollup/plugin-replace @rollup/plugin-url

And the changes from the starter config were as follows:

// original imports
...

import svgr from '@svgr/rollup';
import replace from '@rollup/plugin-replace';
import url from '@rollup/plugin-url';

export default defineConfig([
  // original ghost config
  ...,
  
    {
        input: 'store/src/main.jsx',
        output: {
            dir: 'assets/built/store',
            sourcemap: true,
            format: 'iife',
            plugins: [terser()]
        },
        plugins: [
            replace({
                'process.env.NODE_ENV': JSON.stringify(process.env.BUILD === 'production' ? 'production' : 'development'),
                preventAssignment: true,
            }),
            commonjs(),
            nodeResolve(),
            svgr(),
            url({
                include: ['**/*.svg', '**/*.png', '**/*.jpg', '**/*.jpeg', '**/*.gif', '**/*.webp'],
                limit: 0,
                fileName: 'assets/[name][extname]',
                publicPath: '/assets/built/store/',
            }),
            babel({
                babelHelpers: 'bundled',
                presets: [
                    '@babel/preset-env',
                    ['@babel/preset-react', {
                        runtime: 'automatic'
                    }]
                ],
                exclude: 'node_modules/**'
            }),
            postcss({
                extract: 'main.css',
                sourceMap: true,
                plugins: [
                    atImport(),
                    postcssPresetEnv({})
                ],
                minimize: true
            }),
            terser(),
            process.env.BUILD !== "production" && livereload({
                watch: resolve('.'),
                extraExts: ['hbs'],
                exclusions: [resolve('node_modules')]
            }),
        ]
    }
])

Going into a bit more detail, we have

Plugin-Replace

import replace from '@rollup/plugin-replace';

...

replace({
  'process.env.NODE_ENV': JSON.stringify(process.env.BUILD === 'production' ? 'production' : 'development'),
  preventAssignment: true,
}),

This section replaces any instances of process.env.NODE_ENV with the string value during build. This was used to resolve an issue where trying to load the react app resulted in Uncaught ReferenceError: process is not defined

SVGR

import svgr from '@svgr/rollup';

...

svgr(),

svgr() simply allows the build process to read the svg files during build. This was required since the default react / vite app has svg logos being imported

URL

import url from '@rollup/plugin-url';

url({
  include: ['**/*.svg', '**/*.png', '**/*.jpg', '**/*.jpeg', '**/*.gif', '**/*.webp'],
  limit: 0,
  fileName: 'assets/[name][extname]',
  publicPath: '/assets/built/store/',
}),

url() is copying all of the assets from my /store/src/assets directory during the build process, making them available to the react app when it actually loads

Template

Finally, I had to update my store.hbs template to utilize the react app.

{{!< default}}
<div class="gh-page">
    <div class="gh-container">
        <div id="root"></div>
        <link rel="stylesheet" href={{asset "built/store/main.css"}}>
        <script src={{asset "built/store/main.js"}}></script>
    </div>
</div>

This uses the default template for the page, and then adds the content via my react app with included styles

Result

The results? A react app running within my store page, https://kevin-williams.net/store/