WordPress Rest API extensions for going headless WP

Current WordPress Rest API extension list

If you want to have all the endpoints as plugin, there is one in the plugin directory: WUXT Headless WordPress API Extensions

I love the WordPress Rest API and switching more and more from theme development to a headless WP approach, with an nice front-end framework. Right now I’m favoring Nuxt.js, which is build on Vue.js (check out wuxt, my very own dockerized nuxt/wp development environment).

For using WPs full strength with the Rest API I’ve collected/build a useful snippet library with¬†WordPress Rest API extensions. I’ll try to maintain the following list as development goes on. All of the following extensions can be embedded in the functions.php file. If you wondering about the wuxt_ prefix, I’ve got the code from my Wuxt project and the prefix is as good as anyone.

Front-page extension

Everything starts with a nice front-page, but there no obvious way to get the WordPress front-page via the Rest API. To read the settings, you have to be authorized, which makes things unnecessary complicated. So here a custom endpoint for getting the front-page.

GET: /wp-json/wp/v2/front-page

<?php

/**
 * Adds a front-page endpoint for generell front-page settings in the
 * Front-end
 */
add_action( 'rest_api_init', 'wuxt_front_page_route' );


function wuxt_front_page_route() {
    register_rest_route( 'wp', '/v2/front-page', array(
        'methods'  => 'GET',
        'callback' => 'wuxt_get_front_page'
    ) );
}


function wuxt_get_front_page( $object ) {

    $request  = new WP_REST_Request( 'GET', '/wp/v2/posts' );

    $frontpage_id = get_option( 'page_on_front' );
    if ( $frontpage_id ) {
      $request  = new WP_REST_Request( 'GET', '/wp/v2/pages/' . $frontpage_id );
    }

    $response = rest_do_request( $request );
    if ($response->is_error()) {
        return new WP_Error( 'wuxt_request_error', __( 'Request Error' ), array( 'status' => 500 ) );
    }

    $embed = $object->get_param( '_embed' ) !== NULL;
    $data = rest_get_server()->response_to_data( $response, $embed );

    return $data;

}

?>

Right now, there is no way I know of for getting menus from the WordPress Rest API. I’m not sure if it’s the most effective way, but here my WordPress Rest API custom endpoint for menus. It registers even a standard ‘main’ menu as default if no location is requested.

Note: The most work for this snippet is done by the menu-class of Michael Cox, you have to include it to get the endpoint to work.

GET: /wp-json/wp/v2/menu?location=<location>

<?php

/**
 * Adds a menu endpoint
 */
require_once('/path/to/Menu.php');

add_action('init', 'wuxt_register_menu');
add_action('rest_api_init', 'wuxt_route_menu');

function wuxt_register_menu()
{
    register_nav_menu('main', __('Main meny'));
}

function wuxt_route_menu()
{
    register_rest_route('wp', '/v2/menu', array(
        'methods' => 'GET',
        'callback' => 'wuxt_get_menu',
    ));
}

function wuxt_get_menu($params)
{
    $params = $params->get_params();
    $theme_locations = get_nav_menu_locations();

    if (!isset($params['location'])) {
        $params['location'] = 'main';
    }

    if ( ! isset( $theme_locations[$params['location']] ) ) {
        return new WP_Error( 'wuxt_menu_error', __( 'Menu location does not exist' ), array( 'status' => 404 ) );
    }

    $menu_obj = get_term( $theme_locations[$params['location']], 'nav_menu' );
    $menu_name = $menu_obj->name;
    $menu = new Menu( $menu_name );
    return $menu->getTree();
}

?>

Filtering Categories and taxonomies

When filtering taxonomies with an Rest API request, you are stuck with OR-queries, because the category endpoint doesn’t give you the full complexity of a tax_query. That means you can get posts which are either in category A or B. The following adjustment doesn’t give you the full complexity either, but it lets you switch all tax_queries to an AND-relation, so that you can select posts which are both in category A and B.

GET: /wp-json/wp/v2/posts/?categories=1,2&and=true

<?php

  /**
   * Ads AND relation on rest category filter queries
   */
  add_action( 'pre_get_posts', 'wuxt_override_relation' );

  function wuxt_override_relation( $query ) {

    // bail early when not a rest request
  	if ( ! defined( 'REST_REQUEST' ) || ! REST_REQUEST ) {
  		return;
  	}

    // check if we want to force an "and" relation
    if ( ! isset( $_GET['and'] ) || !$_GET['and'] || 'false' === $_GET['and'] || !is_array( $tax_query = $query->get( 'tax_query' ) ) ) {
  		return;
  	}

    foreach ( $tax_query as $index => $tax ) {
      $tax_query[$index]['operator'] = 'AND';
    }

  	$query->set( 'tax_query', $tax_query );

  }

?>

Loading ACF Meta-fields

Integrating meta-fields from the Advanced-Custom-Fields plugin into the Rest API responses can be done with this plugin. If you need a simpler solution (only integrating meta fields into post-objects, not writing them), or simply a bit more control, the following snippet can get you started. It sets all ACF-fields to show_in_rest, which lets them appear in the post-objects meta-section:

<?php

    /**
     * Register meta fields from ACF
     */
    add_action( 'init', 'wuxt_register_acf_meta' );

    function wuxt_register_acf_meta() {

        if( function_exists( 'acf_get_field_groups' ) ){
            $result = array();
            $acf_field_groups = acf_get_field_groups();
            foreach( $acf_field_groups as $acf_field_group) {
                foreach($acf_field_group['location'] as $group_locations) {
                    foreach($group_locations as $rule) {
                        foreach(acf_get_fields( $acf_field_group ) as $field) {
                            register_meta( 'post', $field['name'], array( 'show_in_rest' => true ) );
                        }
                    }

                }

            }
        }

    }

?>

SEO-Data

The register_meta trick is handy, even for other plugins. If you want to integrate data from our favorite SEO add-on, Yoast WordPress SEO, into the post objects, you can do it like that:

<?php

    /**
     * Register meta fields for WordPress SEO
     */
    add_action( 'init', 'wuxt_register_yoast_meta' );

    function wuxt_register_yoast_meta() {
      if(in_array('wordpress-seo/wp-seo.php', apply_filters('active_plugins', get_option('active_plugins')))){

          $allowed_yoast_keywords = array(
              '_yoast_wpseo_title',
              '_yoast_wpseo_bctitle',
              '_yoast_wpseo_metadesc',
              '_yoast_wpseo_focuskw',
              '_yoast_wpseo_meta-robots-noindex',
              '_yoast_wpseo_meta-robots-nofollow',
              '_yoast_wpseo_meta-robots-adv',
              '_yoast_wpseo_canonical',
              '_yoast_wpseo_redirect',
              '_yoast_wpseo_opengraph-description',
          );

          foreach( $allowed_yoast_keywords as $field) {
              register_meta( 'post', $field, array( 'show_in_rest' => true ) );
          }

      }
    }

?>

Building Urls

If you are building a front-end app on top of WordPress, you have to think about how to structure your urls. WordPress has two default post-types (posts & pages) and in the urls is not distinguished which type you are requesting, so http://wp-site.expl/something might lead to a page or a post, dependent on the type of the object with the slug something.

That means, that if you want to mirror that behaviour in your app, you have to do two requests for each url, one searching pages, one searching posts. To make that one request, use the following.

GET: /wp-json/wp/v2/slug/<post-or-page-slug>

<?php

/**
 * Adds a slug endpoint for getting the page or post for a given slug
 */
add_action('rest_api_init', 'wuxt_slug_route');


function wuxt_slug_route()
{
    register_rest_route('wp', '/v2/slug/(?P<slug>\S+)', array(
        'methods'  => 'GET',
        'callback' => 'wuxt_get_slug'
    ));
}


function wuxt_get_slug($object)
{

    $slug = $object->get_param('slug');

    $request = new WP_REST_Request('GET', '/wp/v2/posts');
    $request->set_param('slug', $slug);

    $response = rest_do_request($request);

    if (!$response->data) {

        $request = new WP_REST_Request('GET', '/wp/v2/pages');
        $request->set_param('slug', $slug);

        $response = rest_do_request($request);
    }

    if (!$response->data) {
        return new WP_Error('wuxt_no_such_slug', __('Slug does not exist'), array('status' => 404));
    }

    $embed = $object->get_param('_embed') !== NULL;
    $data = rest_get_server()->response_to_data($response, $embed);

    return $data[0];
}

?>

More to come …

Hope you found something of the code above useful. Send me your extensions in the comments and I will happily integrate them!

Leave a Reply

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