Build your Wordpress front-end with Frontity and deploy it for free with Vercel

I grew bored of PHP templates, so I decided to have a go with building a website using a headless Wordpress and a React front-end framework. I will outline my experiences with Frontity, a React framework designed for Wordpress, and share a few tips.

Rcls
13 min readAug 1, 2020

I’ve been building Wordpress websites on and off for nearly a decade. It’s quick and easy for a small site, but I would never recommend it to be used for bigger solutions.

At some point, developing those PHP templates gets really boring. You do the same stuff over and over again with very little flexibility.

Project

When a customer orders a website from me, I often do the design, development and deployment myself. Full package. Because of this, I develop templates from scratch. It is much faster than picking up some commercial template and starting to figure out everything that goes to it.

This time around, after completing another design project, I wanted to do the development differently. I started looking into React, as I’ve spent a lot of time with it lately, and I stumbled into a framework called Frontity.

In this story I will try to explain what Frontity is, list it’s benefits, share a few tips for Wordpress, as well as Frontity/React development, and why you should try the framework too.

What’s Frontity

The React framework for Wordpress.
(https://frontity.org)

That’s it. An opinionated React framework designed solely for Wordpress development. As they like to put it:

Next.js and Gatsby.js are two great React frameworks that can work with WordPress in this way but none of them is exclusively focused on WordPress. (https://docs.frontity.org/about)

Quick overlook of the features

  • Quick loading. A HTML file is pre-served so the user can start navigating immediately without having to wait for JavaScript.
  • Easy access to posts via navigation. The framework contains a router which allows you to directly access pages and posts by simply navigating to a URL that Wordpress serves. This router also pre-fetches routes and data automatically.
  • SSR (server-side rendering). Frontity requires you to run the site on a Node.js server, or a serverless function platform, such as Vercel or AWS Lambda. This also ensures SEO remains intact.
  • Actively developed. You can check out the repo at https://github.com/frontity/frontity
  • TypeScript support
  • Multisite support

I personally have to disagree with the mention about quick navigation. What Frontity is trying to say is the HTML is served before JavaScript, so your navigation is accessible immediately, but this is only possible if you hard code your menu items into the application. I will try to discourage you from doing so!

Don’t hard code things

As a developer I want my customers to have the ability to pretty much edit any data on the website (but not the design). I don’t want any hard coded pieces in my front-end solutions because it means my customers would have to either:

  1. Contact me and ask me to change something.
  2. Have a person who can manipulate the code to change something.

As an honest developer I don’t want to create traps like this which force my customers to be dependent on me after I’ve handed over the website.

Frontity’s contains a settings file and instructs you to add your navigation items there. This would mean your front-end solution has hard coded information that requires a technical person to change it.

I went around this by creating a REST API endpoint which exposes the primary menu items. However, this means the navigation is only accessible once the API call is completed thus making that feature void.

Requirements and getting started

Now, of course as with Wordpress development you need a local environment that can support Wordpress. This includes things like Apache or Nginx, PHP and MySQL or MariaDB.

For Frontity, all you need is Node.js. The installation is pretty straightforward and I’m not gonna go through it. Instead I’m gonna refer you to read the Quick start guide which will also introduce you to the docs.

To summarize these two paragraphs:

  • Install Wordpress
  • Install Frontity

Frontity will automatically hook to the Wordpress REST API you’ve defined in the settings file and you’re good to go.

Project architecture

The application I was building had external services that had to be called. I wanted to avoid having multiple API addresses or any secrets that needed to be embedded into the front-end application, so I just put them behind Wordpress (see diagram below).

This is very simple, but I like drawing these diagrams

Frontity simply consumes Wordpress REST API without knowing about the services behind it. Building an architecture like this means you have latency in your API calls that call an external service. That’s why it was necessary to to cache responses received from the Hosting & Billing API as the data is not subject to frequent change.

If you’re doing something similar and can’t get Memcached (or some other in-memory caching solution) on your server, you can store the responses in your Wordpress database, which reduces the latency.

Tips for Wordpress

I will share some tips for Wordpress that I picked up while working on this project. These tips are not new to anyone who works with Wordpress daily, but I myself had to read up on a few things to refresh my memory and as it was my first time trying out the REST API portion of the CMS.

Extending the REST API

While it is not mandatory, I do recommend anyone who tries using Wordpress’ REST API learns how to extend it. I had not used it before, so I had to read about it.

Wordpress provides proper documentation on how to extend the API, but if you build an application like this you’re gonna have a functions.php file that’s over a 1000 lines long and very hard to navigate. So instead I split this into two files from the start:

  • callbacks.php
  • routes.php

These go inside your template folder. Anywhere you like. Just remember to require them in your functions.php file.

Now, routes.php just lists the routes, method and callback. I generate them in a simple loop. Like this:

$namespace = 'api/v1';

$routes = [
[
'endpoint' => '/products/(?P<id>\d+)',
'methods' => 'GET',
'callback' => 'browseShopProducts'
],
[
'endpoint' => '/domains',
'methods' => 'GET',
'callback' => 'browseDomains'
],
[
'endpoint' => '/customers',
'methods' => 'GET',
'callback' => 'browseCustomers'
]
];

# Register routes
foreach ($routes as $entry) {
add_action('rest_api_init', function () use ($entry, $namespace) {
register_rest_route($namespace, $entry['endpoint'], [
'methods' => $entry['methods'],
'callback' => $entry['callback']
]);
});
}

After this, you have to create the callback functions inside callbacks.php. These are just global functions that are named browseShopProducts, browseDomains, and browseCustomers. Each function receives a WP_REST_REQUEST object as it’s parameter where you can access provided arguments, like so:

function browseShopProducts(WP_REST_Request $request)
{
$id = $request->get_param('id');
}

In a solution like this you also risk callbacks.php growing too big. That’s why you should consider splitting it into even more files, or maybe try OOP. Instead of giving the callback as a string, you can provide an array where the first parameter is either the fully qualified class name, or an instance of an object. Look up call_user_func in PHP’s manual to get the hang of this.

Adding custom post types

Wordpress, again, has good documentation on how to generate custom post types, but once again it’ll bloat up your functions.php file. That’s why I put each post type inside a class, which I then initiate.

<?php

class FAQ
{
public function init()
{
$this->register();

add_action('add_meta_boxes', [$this, 'add_meta_box']);
add_action('save_post', [$this, 'save_page_id']);
}

private function register()
{
register_post_type('faq', [
// Add labels and information.
// Read more about register_post_type()
]);
}

public function add_meta_box()
{
// Add meta box for Page ID selection using add_meta_box()
}

public function page_id_meta_box_callback($post)
{
// Render (echo/print) HTML for Page ID selection.
// You can retrieve previously saved meta data from $post
}

public function save_page_id($post_id)
{
// Save Page ID using update_post_meta()
}
}

Now, you can either create a separate file that initiates all your custom post types allowing you to see them all in one place, or just add the initializations to your functions.php file. You do have to require these files, though, in your functions.php or build some type of autoloader.

$faq = new FAQ();
$faq->init();

You can also use static functions as well so you can reduce it to one FAQ::init(); call.

Adding custom columns to your custom post listings

Extending columns to your custom posts is really helpful when users navigate content lists. To add custom columns you need to use the manage_{$post_type}_posts_columns hook, like so:

add_filter('manage_block_posts_columns', [$this, 'add_columns']);# Notice that we are inside a class as we use $this 
# and add function visibility
public function add_columns($columns)
{
$columns['block_type'] = __('Type');
$columns['block_order'] = __('Order');
$columns['block_page'] = __('Page');
return $columns;
}

To add content to each column, you need to use the manage_{$post_type}_posts_custom_column hook, like so:

add_action('manage_block_posts_custom_column', [$this, 'columns_content'], 10, 2);# ...public function columns_content($column, $post_id)
{
switch ($column) {
case 'block_order' :
echo (int)get_post_meta($post_id, '_block_order', true);
break;
case 'block_page' :
$pageID = get_post_meta($post_id, '_block_page_id', true);
$post_title = get_the_title($pageID);
echo $post_title;
break;
case 'block_type' :
echo $this->block_types(get_post_meta($post_id, '_block_type', true));
break;
}
}

This is fairly simple.

Adding quick edit fields

Now, this was actually the trickiest thing to do. Since quick edit fields are dynamically loaded (AJAX) when you press that Quick Edit link, you have to overwrite the complete link HTML via a hook, to execute your own JavaScript functions that populates the field with existing data.

For this you need to use three hooks:

  • Action: quick_edit_custom_box — This will add your custom input to the quick edit field. You write your HTML, and print it.
  • Action: admin_footer — This adds your custom JS function to the page which allows you to populate your input with existing data. You write your JS inside a script block and print it.
  • Filter: post_row_actions — Overwrites the Quick Edit link HTML which allows you to call your JS function before the form is displayed. This HTML should be returned with the $actions variable.

Example:

# Quick edit
add_action('quick_edit_custom_box', [$this, 'quick_edit_add_columns'], 10, 1);
add_action('admin_footer', [$this, 'quick_edit_js']);
add_filter('post_row_actions', [$this, 'quick_edit_links_for_rows'], 10, 2);
public function quick_edit_add_columns($column_name)
{
switch ($column_name) {
case "block_order":
printf('<fieldset class="order-fieldset inline-edit-col-left">
<div class="inline-edit-col">
<div class="inline-edit-group wp-clearfix">
<label class="inline-edit-status alignleft">
<span class="title">%s</span>
<input type="number" max="99" min="0" name="block_order" class="block-order-input" />
</label>
</div>
</div>
</fieldset>',
__('Order'));
break;
default:
break;
}
}
public function quick_edit_js()
{
global $current_screen;

# Only allow for Blocks -page
if ($current_screen->post_type !== 'block') return;
?>
<script type="text/javascript">
function add_block_order_value (postId, fieldValue) {
inlineEditPost.revert();
jQuery('input.block-order-input').val(fieldValue);
}
</script>
<?php
}
public function quick_edit_links_for_rows($actions, $post)
{
# Only allow for Block
if ($post->post_type === 'block') {
$data = (int)get_post_meta($post->ID, '_block_order', true);

$actions['inline hide-if-no-js'] = '<a href="#" class="editinline"';
$actions['inline hide-if-no-js'] .= ' title="' . esc_attr(__('Edit this item inline', 'text-domain')) . '"';
$actions['inline hide-if-no-js'] .= " onclick=\"add_block_order_value({$data})\" >";
$actions['inline hide-if-no-js'] .= __('Quick edit');
$actions['inline hide-if-no-js'] .= '</a>';
}

return $actions;
}

So there you go. As you can see on quick_edit_links_for_rows we add a call to add_block_order_value JS function with the data we receive from get_post_meta, so the field gets populated.

This looks really messy, but it works.

Tips for Frontity/React

There were a few things that took me more than 5 minutes, when I tried to create an app where the front-end had only a structure that rendered data generated with HTML.

How to use Contact Form 7 with React

I’ve always preferred using the Contact Form plugin ever since I first stumbled upon it. It’s very easy to use and reduces the code I have to write.

Using it is very simple. You put a [contact-form-7 id=”1"] shortcode into your post. This gets turned into HTML either when Wordpress returns the $post (PHP templates) or by calling do_shortcode() in your REST API callback.

When you return the HTML from your REST API there are a few problems.

  1. The HTML contains the form element making it impossible to use onSubmit. You have to capture the submit event once the HTML is ready, with vanilla JS.
  2. The action points to the REST API endpoint the request was made to.

These mean your website page gets reloaded when someone presses the submit button, and the form gets posted to the wrong endpoint. Fortunately, though, CF7 exposes some of its own REST API endpoints. The endpoint where you POST your contact form data is located at:

/wp-json/contact-form-7/v1/contact-forms/{form-id}/feedback

You need to include each field provided in the generated HTML, including hidden fields, with name starting with _wpcf7, in the request data and the form should containenctype="multipart/form-data" for the encoding.

To make the form working in React I ended with two solutions:

  • Because of problem number 2, I used wpcf7_form_action_url hook (filter) to alter every CF7 form action value. The current value is accessible via a parameter that gets passed to the hook callback function. The regular URL ends in a hash such as #wpcf7–123 where 123 is the ID. Pretty easy to parse. After this all you need to do is return get_site_url() . “/wp-json/contact-form-7/v1/contact-forms/$id/feedback” and every form is pointing in the right place.
  • You have to look up the form element using document.querySelector (unless you can specify an identifier as a selector) and bind addEventListener(‘submit’, cb); to it, so you can prevent default action where your page gets reloaded. Then you have to POST the form. You can access document inside useEffect when using functional components.

Example:

const handleContactFormSubmit = (e) => {
e.preventDefault()

if (!contactFormLoading) {
setContactFormLoading(true)

const data = new FormData(e.target)

fetch(e.target.action, {
method: 'POST',
body: data,
})
.then(response => response.json())
.then((response) => {
if (response.status === 'mail_sent') {
toast.success(response.message)
}
else {
toast.error(response.message)
}
})
.finally(() => setContactFormLoading(false))
}
}

useEffect(() => {
if (contactFormBlock) {
const form = document.querySelector('#contact-form form');

form.addEventListener('submit', function(e) {
handleContactFormSubmit(e);
});
}
}, [contactFormBlock]);

As you can see there’s also loading animation state control there, as well as toast notifications. Now, this is not Frontity specific, rather React specific, but helpful either way.

Don’t import big CSS files — Try to use Styled Components

Frontity is heavily opinionated in the used of styled components. I, however, had to use SCSS to compile into a reusable CSS file that I can use on the Hosting & Billing app template later on, which meant I had to use their <Global> component to import CSS .

<Global styles={css(externalCss)} />

Frontity has a warning on their documentation page:

Using <Global> for anything other than HTML tags is not recommended because Frontity is not able to optimize it. That means you can use it to import external styles, but if you really want Frontity to be able to optimize it, you should extract that CSS and move it to styled-components instead.

Manual imports do not work , you have to use <Global>.

Now, I am not exactly sure why this happened as I was developing the app on a Windows machine (with pretty significant hardware), and Firefox, but I experienced very long loading times when I attempted to use the <Global> component to import Bootstrap’s minimized CSS file. Page loads got longer and eventually the browser started to freeze when the development server kept recompiling.

In the end I am not sure if it has something to do with the fact that the CSS was minimized, or some other bizarre factor like comments, but I managed to fix it by extracting the classes I needed from Bootstrap’s CSS file and inserting them to my SCSS file, which then got compiled into a single CSS file.

How to import webfonts

I actually found this little trick on their support forums as I couldn’t figure out a way to import Font Awesome’s webfonts into the app.

Full disclosure: I am also using the @fortawesome package via npm, but I had to get icons working using CSS pseudo classes.

The folder structure is as follows:

components/
index.js
utils/
webfonts/
fa-solid-900.ttf
...

I had to manually import each webfont and inject them via the Global component as such:

import FaSolid900Ttf from '../utils/webfonts/fa-solid-900.ttf';
import FaSolid900Eot from '../utils/webfonts/fa-solid-900.eot';
import FaSolid900Svg from '../utils/webfonts/fa-solid-900.svg';
import FaSolid900Woff from '../utils/webfonts/fa-solid-900.woff';
import FaSolid900Woff2 from '../utils/webfonts/fa-solid-900.woff2';
const Theme = ({ state }) => {
const data = state.source.get(state.router.link);

return (
<>
<Title/>
{state.frontity.platform === 'server' && <Global
styles={css`
@font-face {
font-family: "Font Awesome 5 Pro";
font-style: normal;
font-weight: 900;
font-display: block;
src: url("${FaSolid900Eot}");
src: url("${FaSolid900Eot}?#iefix") format("embedded-opentype"),
url("${FaSolid900Woff2}") format("woff2"),
url("${FaSolid900Woff}") format("woff"),
url("${FaSolid900Ttf}") format("truetype"),
url("${FaSolid900Svg}#fontawesome") format("svg")
}
`}
supressHydrationWarning
/>
}

Only then did I get the icon font working. Other attempts to import the font files failed and I didn’t want to spend too much time on this.

Final words about Frontity

Frontity is just like any other React framework. The good thing is you can easily set up a connection and easier way to access your WP REST API by using the framework. It saved me a lot of time when I didn’t have to write the router and calls manually. The framework handles the boring tasks and I can focus on building the template.

It’s a helpful tool and I do recommend it, but it’s only good for Wordpress. As a generic React framework with any other sort of backend platform, it’s useless.

Deploying your app to Vercel

Frontity requires a Node.js environment to run at, or a platform that support serverless functions. Some platforms that support this are Vercel, AWS (Lambda), Heroku or Azure (Functions). I wanted to try Vercel, which proved to be the easiest choice in this case as it was something Frontity provided a quick guide to.

To start, set up a free Vercel account. No credit card information is required. It’s is free for personal projects.

After this you should build your app:

npx frontity build

Now, you need to create a vercel.json file at the root of your project folder for Vercel to recognize your app:

{
“version”: 2,
“builds”: [
{
“src”: “package.json”,
“use”: “@frontity/now”
}
]
}

After this, you should login to Vercel using the following CLI command:

npx vercel login

Just follow the instructions on your CLI and you’re good to go. It will send you a link to your email which you have to click. After this you can just deploy:

npx vercel

This will automatically deploy your code to a placeholder address, such as

https://{your-app-name}.{your-username}.vercel.app

Very simple!

You can set up a custom domain afterwards if you like by following Vercel’s documentation on how to adjust your domain records. This will point your own domain to your Vercel app deployment.

To summarize

So, you want to build a Wordpress site using React? Install Frontity, build your headless Wordpress template and extend the API, then deploy to Vercel. I guarantee, it’s a refreshing break from just cranking out those PHP templates.

--

--

Rcls

Consultant, software architect and developer, freelance UI/UX designer, computer engineer, tech enthusiast, father.