While it would be nice from WordPress plugins to always offer a way to overwrite their template files, that’s not the reality.

This is especially frustrating when working with libraries like Alpine.js, which makes use of directives.

Parenthesis: A directive is just a HTML attribute that has a special meaning for the JS library, for example:

<div x-bind:class="{ '--open': isOpen }"></div>

Without Alpine.js the x-bind:class is a meaningless attribute that doesn't do anything.

DomDocument and DOMXPath

When there is no direct way of modifying the HTML, I tend to reach for the DomDocument class.

With DomDocument you parse HTML code and remove parts of it, add new elements or attributes.

If you never used DomDocument before, here's how it typically looks and works:

$normalizedHtml = mb_convert_encoding($htmlContentThatComesFromSomewhere, 'HTML-ENTITIES', 'UTF-8');

// New up the objects and pass the HTML that you have
$dom = new DOMDocument();

$xPath = new DOMXPath($dom);
// Find all <sup> tags
$supTags = $xPath->query('//sup');

foreach ($supTags as $node) {
    // Get whatever that is in the <sup> tag
    $nodeContent = $node->nodeValue;

    // Create a new <a> tag
    $anchor = $dom->createElement('a');
    $anchor->setAttribute('href', '#');
    // Add the directives that are just attributes with certain value
    $anchor->setAttribute('x-data', '{}');
    $anchor->setAttribute('x-on:click', '$dispatch("open-footnotes")');
    // Set contents of the <a> tag to be the same as the <sup> tag
    $anchor->nodeValue = $nodeContent;

    // Replace the <sup> tag content with the <a> tag
    $node->nodeValue = '';

// Save the modification
$modifiedHtml = $dom->saveHTML();

This turns a HTML like this:

<p>Lorem ipsizzle<sup>1</sup> dolizzle sit break it down...</p>

into this:

<p>Lorem ipsizzle<sup><a href="#" x-data="{}" x-on:click="$dispatch('open-footnotes')">1</a></sup> dolizzle sit break it down...</p>

By the way, there are many wrapper packages that offer the same functionality and more with nicer API.

It's also handy to know that tools for converting CSS selector to XPath queries exist. This is because XPath queries quickly get more verbose than you expect. Selecting a plain HTML tag is straightforward, but selecting an element that has the has-blue-color class looks like this:

.//*[contains(concat(" ",normalize-space(@class)," ")," has-blue-color ")]

Most of the SEO tools (and WordPress plugins) generate an XML file for the sitemap. However, it’s perfectly fine to keep the sitemap in a simple text file that contains one URL per line.

If you are using Nunjucks as a templating engine, this is one of those rare cases when you have to care about the whitespace control.

As comparison, with the "regular" for loop tag:

{% for post in collections.sitemap %}
{{ post.url }}
{% endfor %}

the output looks like this:

The empty line before the first URL, between the URLs and after the last one, is part of the output. It's not a formatting mistake in this article.

This does not conform with the requirements, as we need exactly one URL per line without empty lines.

You can control the whitespace by adding a minus sign (-) to the start ({%) or end block (%}). If you add it, then it will strip all leading or trailing whitespace.

Here's what worked for me to generate the sitemap.txt:

{%- for post in collections.sitemap -%}
{{ post.url }}
{% endfor -%}

Notice how the beginning of the endfor does not have the - sign, but the rest do.

This resulted in the correct output, without empty lines:

CUBE CSS is a CSS methodology put forward by Andy Bell. After using BEM for years, some ideas seem wild at first. One of these ideas is: group ordering.

Group ordering suggests that if we have multiple classes applied to a DOM element, we should group them to achieve clarity and communicate relatedness.

<div class="[ card ] [ section box ] [ bg-base color-primary ]"></div>

With all the hype around JS and Go based static site generators, it's easy to miss that you can use PHP with Netlify.

PHP comes preinstalled (with composer), but the default version is 5.6.

To switch to 7.4, you have to configure the build image. Create a netlify.toml config file in the root of your site repo. Define the environment variable like so:

    environment = { PHP_VERSION = "7.4" }

There are so many excellent form plugins - both paid and free - that we rarely see other solutions used for WordPress sites. They are convenient, easy to use, feature-rich, and offer plenty of customizations.

We take these form plugins for granted as they almost feel like part of WordPress itself.

Forms become, once again, a challenge to solve

But what should we do when we move away from the traditional WordPress themes? For example, when we use WordPress as a headless CMS or as a data source for a Jamstack site. 🤔

Fortunately, there are plenty of alternative solutions to choose from. Sure, services like FormKeep or Netlify provide a fantastic developer experience, but we can’t go wrong with the good old Google Forms or the better-looking Typeform.

While this is all well and good, aren’t we overlooking an obvious solution?

Here’s an idea: let’s continue using our favorite WordPress form plugin to handle form submissions.

Gravity Forms REST API to rescue

Not all WordPress form plugins have a REST API, but some do. And one of these plugins is Gravity Forms. While this is not a new feature, it often remains undiscovered, perhaps because there is no mention of the REST API on its features page.

Let’s put together a working example

The REST API is disabled by default. Mark the „Enable access to the API option” as checked in the WP Admin -> Forms -> Settings -> REST API page and save the change in order to enable it.

Enabling the Gravity Forms REST API

There is no need to create an API key, as the form submission endpoint is available without authentication.

A simple example form

After creating a form with a few fields, we can now test the REST API endpoint.

Form fields

  1. Full Name - required text input
  2. Email address - required email input
  3. Phone number - phone input
  4. Message - required textarea input, minimum 10 characters

The example Gravity Forms form

Understanding the submission endpoint

The submission endpoint is accessible under the http://gravityforms.localhost/wp-json/gf/v2/forms/2/submissions URL, and the 2 represents the ID of the form.

Error response

If we send a request to the endpoint without any data to simulate a form submission with empty fields, we get a response with the following validation message:

$ curl -X POST http://gravityforms.localhost/wp-json/gf/v2/forms/2/submissions

The keys in the validation_messages object correspond to the IDs of the fields.

   "validation_messages" : {
      "1" : "This field is required.",
      "2" : "This field is required.",
      "4" : "This field is required."
   "is_valid" : false,
   "page_number" : 1,
   "source_page_number" : 1

The whole range of validation rules still apply. For example, if we send jibberish as the email address, we will receive the corresponding validation message:

    "is_valid": false,
    "validation_messages": {
        "2": "Please enter a valid email address."
    "page_number": 1,
    "source_page_number": 1

Confirmation response

While we only get back the ID of the inputs in the validation_messages object, when submitting we have to prefix it with input_.

$ curl -X POST -H "Content-Type: application/json" -d '{"input_1":"John Doe","input_2":"","input_4":"Wondering if ..."}' http://gravityforms.localhost/wp-json/gf/v2/forms/2/submissions

Once we have fulfilled the requirements, we will get a response with the confirmation details:

    "is_valid": true,
    "page_number": 0,
    "source_page_number": 1,
    "confirmation_message": "<div id='gform_confirmation_wrapper_2' class='gform_confirmation_wrapper '><div id='gform_confirmation_message_2' class='gform_confirmation_message_2 gform_confirmation_message'>Thanks for contacting us! We will get in touch with you shortly.<\\/div><\\/div>",
    "confirmation_type": "message"

A word about status codes

To determine if the submission was successful, we can use the value of the is_valid key, but we can also infer it from the status code of the response.

The status code is 400 Bad Request when there are validation errors; for successful submissions we get a 200 OK.

Submitting the form with JavaScript

It’s better if we use the input_ID format for name attribute, since we can quickly obtain the key/value pairs of the inputs using FormData.

It doesn’t matter how we structure our HTML as long as we add the match name attribute to the proper Gravity Form fields.

<form action="http://gravityforms.localhost/wp-json/gf/v2/forms/2/submissions"

        <label for="message">Message<sup>*</sup></label>
        <textarea id="message" name="input_4" required rows="5"></textarea>

        <label for="full-name">Full name<sup>*</sup></label>
        <input id="full-name" name="input_1" required type="text">

        <label for="email-address">Email address<sup>*</sup></label>
        <input id="email-address" name="input_2" required type="email">

        <label for="phone-number">Phone number</label>
        <input id="phone-number" name="input_3" type="tel">

        <button type="submit">Submit</button>


The bare minimum of JavaScript would look something like this:

const theForm = document.querySelector('form');

theForm.addEventListener('submit', event => {

    const form =;

    const body = new FormData(form),
        {action, method} = form;

    fetch(action, {
    }).then(response => response.json()).then(result => {
        if (!result.is_valid) {
            alert(`Oh no! Something went wrong, but we won't tell you what 😈`);
        } else {
            alert(`All good! 💪`);

📹 Check out the video demonstration

While this is all very rudimentary, it’s still a working example that we can improve on.

In the next article, we are going to display the validation messages for the individual inputs as well as add some other niceties.


If WordPress is used as a headless CMS, the installation remains accessible but hidden from the general public. With the proper security settings in place, using Gravity Forms’ REST API for handling the forms is a cost-effective option that provides better integration with the other moving parts.

In addition, you can still take advantage of the premium and community add-ons to integrate with other services and perform other tasks, such as generating reports, PDFs, etc.

As for the developer experience, it’s no different than using any other service that offers an endpoint.

For those who are not using WordPress as a CMS, especially agencies who are into Jamstack, having a WordPress installation with Gravity Forms serving as a form service is something worth considering.