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 }],
"stage-0"
],
"plugins": [
["transform-decorators-legacy"],
["transform-react-jsx", { "pragma": "h" }]
]
}
webpack.config.babel.js
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"),
'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 oursrc
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 theindex.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);
}
init();
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.
RelatedList.jsx: The Related Posts Component
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 {
constructor(props){
super(props);
this.state = {
posts: []
}
this.getPosts();
}
getPosts(){
var self = this;
// The actual API call
fetch('/wp-json/wp-preact-related/v1/posts?tagids=' + this.props.tagids +'&id=' + this.props.id)
.then(function(response) {
return response.json();
})
.then(function(response) {
self.setState({posts: response})
});
}
render(props, state){
return(
<div class="relatedWrap">
<h2>Related Posts:</h2>
<ul className="relatedList">
{this.state.posts.map(function(post, i) {
return <RelatedPost
key={i}
link={post.link}
title={post.post_title}
thumb={post.thumb}
/>
})
}
</ul>
</div>
)
}
}
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:
this.state.posts.map(function(post, 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) {
super(props);
// set up the initial style object that is populated through the props
this.state = {
style: {
backgroundImage: "url('" + this.props.thumb + "')"
}
};
}
render(state, props){
return(
<li className="related-post">
<a style={this.state.style} href={this.props.link} dangerouslySetInnerHTML={{__html: this.props.title}}></a>
</li>
)
}
}
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={this.state.style}
on the <a>
tag.
Compiling your JavaScript code
Let's see if everything compiles fine by runningwebpack
, 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:
<?php
/*
Plugin Name: Preact Related Posts
Plugin URI: http://jonathanmh.com
Description: another related posts plugin
Version: 1.0
Author: Jonathan M. Hethey
Author URI: http://jonathanmh.com
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',
array(
'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;
}
Summary
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.