Using Preact with WordPress Themes, Plugins and more

Preact is a faster and smaller alternative to React and I have come to like it for building interactive components into WordPress themes and plugins. In this post we’ll go through how to write a Preact Component and how to inject it into an existing WordPress theme.

On my blog, at the time of writing, I’m using the twentysixteen theme with a few minor changes, one of those is the related posts that show up at the bottom of each blog post, which is a tiny plugin exposing an API and rendering through Preact.

If you’re used to React, you’ll feel right at home with Preact straight away.

This is what my related posts plugin looks like at the moment:

Installing Preact and Webpack

We’re going to use webpack for compiling and bundling all our code into one file.

First let’s install our primary dependency:

npm install --save preact

Now for the development dependencies, that will allow us to transpile/compile the code, make use of JavaScript modules and so forth:

npm install --save-dev webpack babel babel-core babel-plugin-transform-react-jsx extract-text-webpack-plugin babel-preset-es2015 babel-preset-stage-0 source-map-loader

Lastly for easier development we can install webpack globally:

npm i -g webpack

Next we’ll set up the .babelrc and the webpack.config.babel.js files


  "sourceMaps": true,
  "presets": [
    ["es2015", { "loose":true }],
  "plugins": [
    ["transform-react-jsx", { "pragma": "h" }]


import webpack from 'webpack';
import ExtractTextPlugin from 'extract-text-webpack-plugin';
import autoprefixer from 'autoprefixer';
import CopyWebpackPlugin from 'copy-webpack-plugin';
import path from 'path';
const ENV = process.env.NODE_ENV || 'development';

const CSS_MAPS = ENV!=='production';

module.exports = {
    context: path.resolve(__dirname, "src"),
    entry: './index.js',

    output: {
        path: path.resolve(__dirname, "build"),
        publicPath: '/',
        filename: 'bundle.js'

    resolve: {
        extensions: ['.jsx', '.js', '.json'],
        modules: [
            path.resolve(__dirname, "src/lib"),
            path.resolve(__dirname, "node_modules"),

    module: {
        rules: [
                test: /\.jsx?$/,
                exclude: path.resolve(__dirname, 'src'),
                enforce: 'pre',
                use: 'source-map-loader'
                test: /\.jsx?$/,
                exclude: /node_modules/,
                use: 'babel-loader'
                test: /\.(css)$/,
                exclude: [path.resolve(__dirname, 'src/components')],
                use: ExtractTextPlugin.extract({
                    fallback: 'style-loader',
                    use: [
                            loader: 'css-loader',
                            options: { sourceMap: CSS_MAPS, importLoaders: 1 }
                test: /\.json$/,
                use: 'json-loader'
                test: /\.(xml|html|txt|md)$/,
                use: 'raw-loader'
                test: /\.(svg|woff2?|ttf|eot|jpe?g|png|gif)(\?.*)?$/i,
                use: ENV==='production' ? 'file-loader' : 'url-loader'
    plugins: ([
        new webpack.NoEmitOnErrorsPlugin(),
        new ExtractTextPlugin({
            filename: 'style.css',
            allChunks: true,
            disable: true,
            //ENV !== 'production'
        new webpack.DefinePlugin({
            'process.env.NODE_ENV': JSON.stringify(ENV)
        new CopyWebpackPlugin([
            { from: './manifest.json', to: './' },
            { from: './favicon.ico', to: './' }
    ]).concat(ENV==='production' ? [
        new webpack.optimize.UglifyJsPlugin({
            output: {
                comments: false
            compress: {
                unsafe_comps: true,
                properties: true,
                keep_fargs: false,
                pure_getters: true,
                collapse_vars: true,
                unsafe: true,
                warnings: false,
                screw_ie8: true,
                sequences: true,
                dead_code: true,
                drop_debugger: true,
                comparisons: true,
                conditionals: true,
                evaluate: true,
                booleans: true,
                loops: true,
                unused: true,
                hoist_funs: true,
                if_return: true,
                join_vars: true,
                cascade: true,
                drop_console: true

    ] : []),

    stats: { colors: true },

    node: {
        global: true,
        process: false,
        Buffer: false,
        __filename: false,
        __dirname: false,
        setImmediate: false

    devtool: ENV==='production' ? 'source-map' : 'cheap-module-eval-source-map',


You’ll probably have noticed that the keyword production shows up. This will help us to minify the code later for production.

Writing our Preact Component

Let’s get started on the real development. We want those related posts to render inside WordPress, so let’s go ahead and create our src and src/components directory:

mkdir src
mkdir src/components

and create the necessary files:

touch src/index.js
touch src/style.css

Lastly our Preact components:

touch components/RelatedList.jsx
touch components/RelatedPost.jsx

index.js: our entry point

Inside the index.js file we’re going to initialise our component:

import { h, render } from 'preact';
import './style.css';

import RelatedList from './components/RelatedList.jsx'

let root;

const EL_ID = 'wp-preact-related-posts';

var rootElement = document.getElementById(EL_ID);

function init() {

  var id = rootElement.getAttribute('data-id')
  var tagids = rootElement.getAttribute('data-tagids')

  root = render(<RelatedList id={id} tagids={tagids}/>, rootElement, root);


The lines that define id and tagids are going to be read from data attributes that we’ll later render with WordPress, with the posts ID and the numeric IDs of the tags on that post.

Next, we’ll need a component that renders the list of posts after making a request to the API endpoint and parsing the JSON data.

import { h, Component } from 'preact';

import RelatedPost from './RelatedPost.jsx';

export default class RelatedList extends Component {
    this.state = {
      posts: []

    var self = this;

    // The actual API call
    fetch('/wp-json/wp-preact-related/v1/posts?tagids=' + this.props.tagids +'&id=' +
      .then(function(response) {
        return response.json();
      .then(function(response) {
        self.setState({posts: response})

  render(props, state){
      <div class="relatedWrap">
        <h2>Related Posts:</h2>
        <ul className="relatedList">
            {, i) {
              return <RelatedPost

As you can see we’re making use of the fetch API to request the JSON data from the server. We could also use axios, other libraries or even an xhr request (old, booo).

When the JSON data has arrived back in the browser, we’re using setState({posts: response}) and rendering a list of the imported component RelatedPost by mapping over the array:, i) {
  return <RelatedPost>

RelatedPost.jsx: The Actual Post, Link and Image

Lastly we’ll need to display all the information that we’ve received from the WordPress REST API and actually give our users the opportunity to click on the related posts.

import { h, Component } from 'preact';

export default class RelatedPost extends Component {
  constructor(props) {

    // set up the initial style object that is populated through the props
    this.state = {
      style: {
        backgroundImage: "url('" + this.props.thumb + "')"

  render(state, props){
      <li className="related-post">
        <a style={} href={} dangerouslySetInnerHTML={{__html: this.props.title}}></a>

Noteworthy about that component is the conditional styling through the style part of the local components state, that later is used in the render fucntion through style={} on the <a> tag.

Compiling your JavaScript code

Let’s see if everything compiles fine by running webpack, for development purposes and in watch mode you might prefer: webpack -w.

The output should look like the following

Webpack is watching the files…

Hash: a4db347bf232d8d4bb5d
Version: webpack 3.10.0
Time: 1080ms
    Asset    Size  Chunks             Chunk Names
bundle.js  139 kB       0  [emitted]  main
   [1] ./index.js 857 bytes {0} [built]
   [2] ./style.css 941 bytes {0} [built]
   [3] /home/jonathan/projects/node_modules/css-loader?{"sourceMap":true,"importLoaders":1}!./style.css 4.05 kB {0} [built]
    + 5 hidden modules

and produce a file called bundle.js in your build directory.

Compiling for production can be done by running:

NODE_ENV=production webpack

which will decrease the file size significantly.

webpack && ls -lsh build
140K -rw-r--r-- 1 jonathan jonathan 137K Jan  7 13:50 bundle.js

NODE_ENV=production webpack && ls -lsh build
20K -rw-r--r-- 1 jonathan jonathan 18K Jan  7 13:50 bundle.js

Reduction: 120Kb from 140Kb to 20Kb.

The PHP / WordPress part

The code below is written for a plugin, but you can just as well transfer it to a theme without much/any work.

This by the way is one of the reasons why we chose a predictable file name, bundle.js in our webpack config, because we’ll use wp_enqueue_script to load the JS file into WordPress. I have created a directory called wp-preact-related in my /wp-content/plugins/ directory.

So let’s get the preact-related.php created with the following content:

Plugin Name: Preact Related Posts
Plugin URI:
Description: another related posts plugin
Version: 1.0
Author: Jonathan M. Hethey
Author URI:
License: GPL2


class Preact_Related {

  public $post_amount = 3;

  public function __construct(){

    // setup actions and filters
    add_action( 'wp_enqueue_scripts', [$this,'load_scripts'] );
    add_action( 'rest_api_init', [$this, 'init_api_routes'] );
    add_filter( 'the_content', [$this, 'inject_container'] );

  public function inject_container( $content ){
    // make $post variable available
    global $post;
    if( is_single() ){
      // which tags does the current post have?
      $tags = wp_get_post_tags($post->ID);

      $tagIDs = [];

      foreach( $tags as $tag ){
        array_push($tagIDs, $tag->term_id);

      // string to inject at the end of the article
      $content .= '<div id="wp-preact-related-posts" data-id="'.$post->ID.'" data-tagIDs="'.join(',',$tagIDs).'"></div>';

    return $content;

  // enqueue the compiled JavaScript file
  public function load_scripts(){
    wp_enqueue_script( 'preact-related', plugin_dir_url( __FILE__ ) . 'build/bundle.js', [], false, true );

  // setup the REST route for the XHR call from within Preact
  public function init_api_routes(){
    register_rest_route( '/wp-preact-related/v1', '/posts',
        'methods' => 'GET',
        'callback' => [$this, 'api_get_posts']

  // function that responds to the XHR call
  public function api_get_posts( WP_REST_Request $request ){
    $tagids = $request['tagids'];
    $id = $request['id'];

    $args = [
      'post__not_in' => [$id],
      'post_type' => 'post',
      'post_status' => 'publish',
      'posts_per_page' => $this->post_amount

    if( isset($tagids) && strlen($tagids) > 0 ){
      $args['tag__in'] = explode(',', $tagids);

    $query = new WP_Query( $args );
    $posts = $query->posts;

    // add the permalink and the thumbnail to the JSON response
    foreach ($posts as $key => $post) {
      $posts[$key]->link = get_the_permalink($post->ID);
      $posts[$key]->thumb = get_the_post_thumbnail_url($post->ID, 'full');

    return $posts;

// initialise the class
$preact_related = new Preact_Related();

Most important about the PHP file are the api_get_posts function and that everything is initialised and hooked into the correct parts properly.

We’re using a filter to append the <div> that Preact will render in to every is_single() post on our WordPress site:

$content .= '<div id="wp-preact-related-posts" data-id="'.$post->ID.'" data-tagIDs="'.join(',',$tagIDs).'"></div>';

The init_api_routes function makes sure that we’ll have the /wp-preact-related/v1/posts endpoint available when doing our fetch request.

api_get_posts will query the WordPress database, find posts that have the same tags and exclude the post with the current ID from the query, so we don’t recommend the post that a user currently is reading.

You can now go ahead and activate your WordPress plugin in the admin settings!

Lastly, the quick stylesheet I wrote for this:

#wp-preact-related-posts ul li {
  display: block;
  list-style-type: none;

#wp-preact-related-posts ul li a {
  display: block;
  position: relative;
  min-height: 30vh;
  background-repeat: no-repeat;
  background-size: cover;
  background-position: center center;
  background-color: black;
  margin-bottom: 3vh;
  font-size: 2rem;
  color: white;
  font-weight: bold;
  text-align: left;
  text-decoration: none;
  border-bottom-color: rgba(255, 255, 255, 0);
  padding: 3vh;
  padding-top: 5vh;
  line-height: 1.2;
  text-shadow: 1px 1px 3px rgba(0, 0, 0, 1);
  z-index: 1;

#wp-preact-related-posts ul li a:after {
  content: '';
  display: block;
  position: absolute;
  height: auto;
  top: 0;
  right: 0;
  left: 0;
  bottom: 0;
  z-index: -1;
  background: -moz-linear-gradient(top, rgba(0, 0, 0, 0) 0%, rgba(0, 0, 0, 0.65) 100%);
  /* FF3.6+ */
  background: -webkit-gradient(linear, left top, left bottom, color-stop(0%, rgba(0, 0, 0, 0)), color-stop(100%, rgba(0, 0, 0, 0.65)));
  /* Chrome,Safari4+ */
  background: -webkit-linear-gradient(top, rgba(0, 0, 0, 0) 0%, rgba(0, 0, 0, 0.65) 100%);
  /* Chrome10+,Safari5.1+ */
  background: -o-linear-gradient(top, rgba(0, 0, 0, 0) 0%, rgba(0, 0, 0, 0.65) 100%);
  /* Opera 11.10+ */
  background: -ms-linear-gradient(top, rgba(0, 0, 0, 0) 0%, rgba(0, 0, 0, 0.65) 100%);
  /* IE10+ */
  background: linear-gradient(to bottom, rgba(0, 0, 0, 0) 0%, rgba(0, 0, 0, 0.65) 100%);
  /* W3C */

#wp-preact-related-posts ul li a:visited {
  color: white;


That’s it! Excuse the long code snippets, I hope the post still was interesting and not to hard to wrap your head around.

I have omitted the webpack dev server and focused on compiling the file straight down to the disk so the WordPress plugin can read it without caring about the hash of the file.

Leave a Reply

Your email address will not be published. Required fields are marked *