In the dynamic world of WordPress development, efficiency and automation are key to staying ahead. Deploying your theme to the WordPress.org repository can be a meticulous process, but with GitHub Actions, you can automate and streamline this workflow. This guide will walk you through setting up GitHub Actions to deploy your WordPress.org theme, addressing common questions, and providing valuable insights to enhance your development process.

Introduction to GitHub Actions for WordPress Theme Deployment

GitHub Actions is a powerful tool that enables developers to automate, customize, and enhance their software development workflows directly within GitHub. For WordPress theme developers, this means you can automate the deployment of your themes to the WordPress.org repository, reducing manual effort and minimizing errors.

By leveraging GitHub Actions, you can set up a continuous integration and deployment (CI/CD) pipeline that automatically pushes your theme updates to the WordPress.org repository whenever you create a new release. This not only saves time but also ensures that your users always have access to the latest features and improvements.

Setting Up GitHub Actions for Theme Deployment

To deploy your WordPress theme using GitHub Actions, follow these steps:

1. Prepare Your Theme Repository

Ensure your theme’s code is hosted on GitHub and that your repository follows the standard structure. This includes having a style.css file with the necessary theme headers and a functions.php file.

2. Create the .distignore File

The .distignore file specifies which files and directories should be excluded from the deployment. Common exclusions include development files, tests, and configuration files not required for the theme’s functionality. For example:

node_modules/
tests/
.gitignore
package.json

3. Set Up Secrets in GitHub

To authenticate with the WordPress.org Subversion (SVN) repository, you’ll need to set up the following secrets in your GitHub repository:

To add these secrets, navigate to your repository’s settings, select “Secrets and variables,” then “Actions,” and click on “New repository secret.”

4. Create the GitHub Actions Workflow

In your repository, create a .github/workflows/deploy.yml file with the following content:

name: Deploy Theme to WordPress.org

on:
  push:
    tags:
      - '*'

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout Repository
        uses: actions/checkout@v3

      - name: WordPress.org Theme Deploy
        uses: actions/[email protected]
        env:
          SVN_USERNAME: ${{ secrets.SVN_USERNAME }}
          SVN_PASSWORD: ${{ secrets.SVN_PASSWORD }}
          SLUG: your-theme-slug

This workflow triggers on every push to a tag (e.g., when you create a new release). It checks out your repository and uses the wordpress-theme-deploy action to deploy your theme to WordPress.org. Ensure you replace your-theme-slug with your actual theme slug.

Frequently Asked Questions

How do I handle build processes before deployment?

If your theme requires a build process (e.g., compiling Sass or JavaScript), you can add steps before the deployment step in your workflow. For example:

      - name: Install Dependencies
        run: npm install

      - name: Build Assets
        run: npm run build

These steps ensure that your assets are built before the theme is deployed.

Can I deploy only specific branches?

Yes, you can configure the workflow to trigger on specific branches by modifying the on section:

on:
  push:
    branches:
      - main

This configuration triggers the workflow only when changes are pushed to the main branch.

How do I exclude files from deployment?

The .distignore file is used to exclude files and directories from deployment. Ensure this file is in your repository’s root and lists all items to be excluded.

Is it secure to store SVN credentials in GitHub?

Storing credentials as GitHub Secrets is a secure practice. GitHub encrypts and stores these secrets securely, making them accessible only to workflows in your repository.

What if the deployment fails?

If the deployment fails, GitHub Actions provides logs that can help you diagnose the issue. Common issues include incorrect SVN credentials, network problems, or issues with the theme’s code.

Best Practices for Theme Deployment

Conclusion

Automating your WordPress theme deployment using GitHub Actions can significantly enhance your development workflow, ensuring efficiency, accuracy, and consistency. By following the steps outlined in this guide, you can set up a robust deployment pipeline that saves time and reduces the potential for errors.

In a WordPress settings page 3rd-party developers can inject their own settings fields / HTML with action hooks, but you cannot do the same if you decide to build your plugin’s settings page in React.

This article explores the options to make our React Components extendible such that 3rd-party developers can easily inject their own components in them.

We are going to use SlotFillProviderSlotFill and PluginArea Gutenberg React Components.

Part 1: Building a simple settings page

We’re going to build a very simple settings page. For the sake of simplicity of this article, we won’t complicate it by writing any server-side logic to save the fields, nor will cover managing the state of our application because our focus is just rendering our components and allowing other developers to render their own components.

So, we will create an AdminSettings Component that renders First Name, Last Name text fields and a Save button.

WordPress settings page built using Gutenberg components.
import { __ } from '@wordpress/i18n';
import {
    BaseControl,
    TextControl,
    Button,
} from '@wordpress/components';

export const AdminSettings = () => {
    return (
        <>
            <h1>{ __( 'Settings using Gutenberg components' ) }h1>

            <BaseControl label={ __( 'First Name' ) }>
                <TextControl/>
            BaseControl>

            <BaseControl label={ __( 'Last Name' ) }>
                <TextControl/>
            BaseControl>

            <br />

            <Button variant='primary'>
                { __( 'Save' ) }
            Button>
        
    );
};

Understanding Slot and Fill Components

If we were to compare the behavior with the PHP hooks, the I’d say Slot is similar to what do_action() does and Fill is similar to add_action().

As a plugin developer, you usually provide both the Slot and Fill. 3rd-party developers will inject their Components in this Slot using Fills. But we will do it differently. Instead of relying on other developers to implement Fills, we will expose a component on the global window object that implement Fills.

A Slot and a Fill is connected through the name attribute. For example:

<Slot name="additional-fields-area"/>
<Fill name="additional-fields-area">
    <TextControl label="Injected field"/>
Fill>

They are only connected if both share the same value for the name attribute.

SlotFillProvider is a Context provider for Slot and Fill. When you’re using Slot-Fill, you must ensure that both Slot and Fill are inside the SlotFillProvider, else it won’t work.

When you’re building an extendible Gutenberg block, you don’t need to use SlotFillProvider because the entire Gutenberg editor Component is already inside it. But if you’re building an extendible component which will be used outside of the Gutenberg editor, then you must use it.

Understanding how Fill works

As long as Fill is inside the SlotFillProvider, you can render it anywhere, it doesn’t matter. Whatever is between the Fill open and close tags will always be rendered where the connected Slot is.

The SlotFill mechanism is built using React Portals.

Part 2: Adding a Slot to our Settings Component

import { __ } from '@wordpress/i18n';
import { PluginArea } from '@wordpress/plugins';
import {
    BaseControl,
    TextControl,
    Button,
    SlotFillProvider,
    Slot
} from '@wordpress/components';

export const AdminSettings = () => {
    return (
        <SlotFillProvider>
            <h1>{ __( 'Settings using Gutenberg components' ) }h1>

            <BaseControl label={ __( 'First Name' ) }>
                <TextControl/>
            BaseControl>

            <BaseControl label={ __( 'Last Name' ) }>
                <TextControl/>
            BaseControl>

            <Slot name="gutenberg-settings-additional-fields" />

            <br />
            <Button variant='primary'>
                { __( 'Save' ) }
            Button>
        SlotFillProvider>
    );
};

Great! We’re already halfway into building an extendible component.

Now remember I said in the previous point – Fill has to be inside the SlotFillProvider for this to work. You might wonder, how can 3rd-party developers add a Fill here? This will be explained next.

Revising registerPlugin

Gutenberg editor provides a number of SlotFills to allow developers to inject their Components in specific areas of the editor. You may have already used some of them already. If you remember, you may have done something like:

import { registerPlugin } from '@wordpress/plugins';
import { __ } from '@wordpress/i18n';
import { PluginPostPublishPanel } from '@wordpress/editor';

registerPlugin( 'third-party-plugin', {
    render: InjectSomeText,
} );

function InjectSomeText() {
    return (
        <PluginPostPublishPanel>
            <p>Post Publish Panelp>
        PluginPostPublishPanel>
    );
}
The PluginPostPublishPanel Slot is highlighted in blue at the bottom.

Gutenberg provides the following SlotFills:

If InjectSomeText does not return at least one of these, then whatever the component returns won’t be rendered anywhere. So it is clear – The Components rendered by registerPlugin must return at least one of the above.

But why is that? Why does registerPlugin refuse to render a Component that does not return one of the core SlotFills?

This is because, the core SlotFills return Fill components that are connected to the pre-defined Slot areas. And these Fill components are rendered inside PluginArea components.

Understanding PluginArea

The registerPlugin function is very closely related to the PluginArea component and they work together. Let’s try to understand it with a practical example.

import { __ } from '@wordpress/i18n';
import {
    BaseControl,
    TextControl,
    Button,
    SlotFillProvider,
    Slot,
    Fill
} from '@wordpress/components';

export const AdminSettings = () => {
    return (
        <SlotFillProvider>
            <h1>{ __( 'Settings using Gutenberg components' ) }h1>

            <BaseControl label={ __( 'First Name' ) }>
                <TextControl/>
            BaseControl>

            <BaseControl label={ __( 'Last Name' ) }>
                <TextControl/>
            BaseControl>

            <Slot name="gutenberg-settings-additional-fields" />

            <br />

            <Button variant='primary'>
                { __( 'Save' ) }
            Button>
        SlotFillProvider>
    );
};

window.PluginGutenbergSettingsFields = ( { children } ) => {
    return (
        <>
            <Fill name="gutenberg-settings-additional-fields">
                { children }
            Fill>
        
    );
};

Similar to how core provides us with a list of SlotFills, we created a custom SlotFill Component called PluginGutenbergSettingsFields and exposed it on the global window object, so that other developers can easily use it with registerPlugin.

But you might wonder, wait! Didn’t I mention previously that Fill should be inside the same SlotFillProvider ? If other 3rd-party developers use PluginGutenbergSettingsFields to inject code, where would it be rendered? – That’s a good question!

This is where, PluginArea is important!

import { __ } from '@wordpress/i18n';
import { PluginArea } from '@wordpress/plugins';
import {
    BaseControl,
    TextControl,
    Button,
    SlotFillProvider,
    Slot,
    Fill
} from '@wordpress/components';

export const AdminSettings = () => {
    return (
        <SlotFillProvider>
            <h1>{ __( 'Settings using Gutenberg components' ) }h1>

            <BaseControl label={ __( 'First Name' ) }>
                <TextControl/>
            BaseControl>

            <BaseControl label={ __( 'Last Name' ) }>
                <TextControl/>
            BaseControl>

            <Slot name="gutenberg-settings-additional-fields" />

            <PluginArea/>

            <br />

            <Button variant='primary'>
                { __( 'Save' ) }
            Button>
        SlotFillProvider>
    );
};

window.PluginGutenbergSettingsFields = ( { children } ) => {
    return (
        <>
            <Fill name="gutenberg-settings-additional-fields">
                { children }
            Fill>
        
    );
};

I will explain the entire flow in the next section.

Part 3: Extending as a 3rd-party developer

As a 3rd-party developer, I would like to add a ToggleControl Component to the settings page.

Injecting a ToggleControl by extending a Component using Slot and Fills.
mport { registerPlugin } from '@wordpress/plugins';
import { __ } from '@wordpress/i18n';
import {
    BaseControl,
    ToggleControl
} from '@wordpress/components';

const { PluginGutenbergSettingsFields } = window;

registerPlugin( 'third-party-plugin', {
    render: InjectAdditionalFields,
    scope: PluginGutenbergSettingsFields.scope // Ignore this for now.
} );

function InjectAdditionalFields() {
    return (
        <PluginGutenbergSettingsFields>
            <BaseControl label={ __( 'Activate Account?' ) }>
                <ToggleControl/>
            BaseControl>
        PluginGutenbergSettingsFields>
    );
}

Summary:

– registerPlugin renders InjectAdditionalFields where the PluginArea component is.

– PluginArea renders a hidden div which renders the contents of InjectAdditionalFields.

– InjectAdditionalFields returns our custom SlotFill PluginGutenbergSettingsFields.

– PluginGutenbergSettingsFields renders Fill next to the Slot and also inside the same SlotFillProvider

– Slot with the help of SlotFillProvider renders content that is between the Fill tags.

Extra: Exposing application data to 3rd party components

Obviously, the Toggle Field will require access to the AdminSettings state data. For example, if you want to make the Toggle Field conditional depending on the value of some other field?

Slot component accepts a prop called as fillProps. This is how you do it:

But to access this data, AdminSettingsPluginGutenbergSettingsFields and InjectAdditionalFields needs to be updated like the following:

import { useState } from '@wordpress/element';

export const AdminSettings = () => {

    const [ appData, setAppData ] = useState( { fname: 'Siddharth', lname: 'Thevaril' } );

    return (
        <SlotFillProvider>
            <h1>{ __( 'Settings using Gutenberg components' ) }h1>

            <BaseControl label={ __( 'First Name' ) }>
                <TextControl value={ appData.fname }/>
            BaseControl>

            <BaseControl label={ __( 'Last Name' ) }>
                <TextControl value={ appData.lname }/>
            BaseControl>

            <Slot name="gutenberg-settings-additional-fields" fillProps={ { appData, setAppData } } />

            <PluginArea />

            <br />

            <Button variant='primary'>
                { __( 'Save' ) }
            Button>
        SlotFillProvider>
    );
};

window.PluginGutenbergSettingsFields = ( { children } ) => {
    return (
        <>
            <Fill name="gutenberg-settings-additional-fields">
                { ( fillProps ) => children( fillProps ) }
            Fill>
        
    );
};

And for the plugin:

function InjectAdditionalFields() {
    return (
        <PluginGutenbergSettingsFields>
            {
                ( fillProps ) => {
                    return (
                        <BaseControl label={ __( 'Activate Account?' ) }>
                            <ToggleControl />
                        BaseControl>
                    )
                }
            }
        PluginGutenbergSettingsFields>
    );
}

If your settings page is large and the app data is complex, I would suggest to implement a separate data store instead of passing app state via fillProps.

Automating the deployment of your WordPress plugin to the WordPress.org repository can save you time and ensure a smooth release process. With GitHub Actions and the 10up/action-wordpress-plugin-asset-update action, you can streamline this process. Here’s how to set it up:

Step 1: Prepare Your Plugin

Ensure your plugin follows WordPress.org guidelines and includes the necessary files (readme.txt, plugin header, etc.).

Step 2: Create GitHub Repository

Host your plugin code in a GitHub repository. Make sure your repository is public.

Step 3: Set Up GitHub Actions Workflow

In your GitHub repository, create a .github/workflows/deploy.yml file with the following configuration:

name: Deploy to WordPress.org

on:
  push:
    tags:
      - '*'

jobs:
  deploy:
    name: Deploy
    runs-on: ubuntu-latest
    steps:
      - name: Checkout code
        uses: actions/checkout@v2

      - name: Run build script
        run: |
          # Add your build scripts here, e.g., npm install && npm run build

      - name: WordPress Plugin Asset Update
        uses: 10up/[email protected]
        with:
          # Your WordPress.org plugin repository name
          plugin-slug: your-plugin-slug
          # WordPress.org username and password (use GitHub Secrets)
          svn-username: ${{ secrets.WP_ORG_USERNAME }}
          svn-password: ${{ secrets.WP_ORG_PASSWORD }}

Step 4: Configure Secrets

In your GitHub repository, go to Settings > Secrets and variables > Actions > New repository secret and add the following secrets:

Step 5: Push Tags to Trigger Deployment

Tag your release in GitHub to trigger the workflow:

git tag v1.0.0
git push origin v1.0.0

This will initiate the GitHub Actions workflow, running the build script and deploying your plugin to the WordPress.org repository.

Conclusion

Using GitHub Actions to deploy your WordPress plugin simplifies the release process, making it more efficient and reliable. By following the steps above, you can automate your deployments and focus more on developing great plugins.

For more detailed information, check out the 10up/action-wordpress-plugin-asset-update documentation.

Creating a custom admin page using React in WordPress can enhance the functionality and user experience of your site. In this guide, we’ll use @wordpress/element for React, and @wordpress/components for UI elements. We’ll also set up a minimal workable Webpack configuration.

Step 1: Install Necessary Packages

First, ensure you have Node.js and npm installed. Then, install the required packages:

npm install @wordpress/element @wordpress/components @babel/preset-react @babel/preset-env webpack webpack-cli webpack-dev-server babel-loader --save-dev

Step 2: Set Up Your Project Structure

Create the following directory structure:

my-plugin/
├── admin
   ├── build
      
   └── src
       └── index.js
├── package.json
├── webpack.config.js
└── wp-react.php

Step 3: Configure Webpack

Create webpack.config.js with the following configuration:

const path = require('path');

module.exports = {
    entry: './admin/src/index.js',
    output: {
        path: path.resolve(__dirname, 'admin/build'),
        filename: 'bundle.js'
    },
    module: {
        rules: [
            {
                test: /\.js$/,
                exclude: /node_modules/,
                use: {
                    loader: 'babel-loader',
                    options: {
                        presets: ['@babel/preset-env', '@babel/preset-react']
                    }
                }
            }
        ]
    },
    devServer: {
        contentBase: path.join(__dirname, 'admin/build'),
        compress: true,
        port: 9000
    }
};

Step 4: Create React Component

In admin/src/index.js, create a simple React component:

import { render } from '@wordpress/element';
import { Button } from '@wordpress/components';

const App = () => (
    

My Admin Page

<Button variant="primary" className="button default">Click MeButton>
); render(<App />, document.getElementById('admin-page'));

Step 5: Set Up package.json

In your package.json, add scripts for building and watching your files:

{
  "name": "wp-react",
  "version": "1.0.0",
  "description": "",
  "main": "webpack.config.js",
  "scripts": {
    "build": "webpack --mode production",
    "watch": "webpack --mode development --watch"
  },
  "keywords": [],
  "author": "Kamal Hosen",
  "license": "GPL-v2-or-later",
  "devDependencies": {
    "@babel/preset-env": "^7.24.7",
    "@babel/preset-react": "^7.24.7",
    "@wordpress/components": "^28.2.0",
    "@wordpress/element": "^6.2.0",
    "babel-loader": "^8.3.0",
    "webpack": "^5.92.1",
    "webpack-cli": "^4.10.0",
    "webpack-dev-server": "^3.11.3"
  }
}

Step 6: Enqueue Your Scripts in WordPress

In my-plugin.php, enqueue the compiled JavaScript file:

php
/**
 * Plugin Name: WP React
 * Description: A simple plugin to demonstrate how to use React in WordPress.
 * Version: 1.0.0
 * Author: Kamal Hosen
 * Author URI: https://kamalhosen.com
 * License: GPL2
 * License URI: https://www.gnu.org/licenses/gpl-2.0.html
 * Text Domain: wp-react
 * Domain Path: /languages
 * Requires at least: 5.2
 * Requires PHP: 7.2
 * 
 */

function wp_react_enqueue_scripts() {
    wp_enqueue_script(
        'wp-react-admin-page',
        plugins_url('admin/build/bundle.js', __FILE__),
        ['wp-element', 'wp-components'],
        filemtime(plugin_dir_path(__FILE__) . 'admin/build/bundle.js'),
        true
    );
}
add_action('admin_enqueue_scripts', 'wp_react_enqueue_scripts');

function wp_react_admin_page() {
    add_menu_page(
        'WP React Admin Page',
        'WP React',
        'manage_options',
        'wp-react',
        'wp_react_render_admin_page',
        '',
        6
    );
}
add_action('admin_menu', 'wp_react_admin_page');

function wp_react_render_admin_page() {
    echo '
';
} ?>

Step 7: Build and Watch Your Project

Run the following commands to build and watch your project:

npm run build
npm run watch

Conclusion

By following these steps, you can set up a React-based admin page in WordPress, leveraging modern JavaScript development tools like Webpack and Babel. This setup ensures your code is modular, maintainable, and easy to extend. Happy coding!