loading...
Cover image for Building a Headless WordPress Theme

Building a Headless WordPress Theme

arnonate profile image Nate Arnold ・3 min read

I recently developed a WordPress theme for headless WordPress projects. This post is a simple explanation of the decisions that were made after implementing the theme on a few JAMstack projects.

Highlights

The index route forwards to the /wp-admin login screen so there is no need for any front-end code:

<script type="text/javascript">
    window.location.replace(window.location.protocol + "//" + window.location.hostname + "/wp-admin");
</script>

There is an example Custom Post Type already set up to display in the API as well as in the Graph if you are using WPGraphQL:

  add_action( "init", "create_custom_post_type" );

  function create_custom_post_type() {
    register_post_type("custom_posts", // Register Custom Post Type
      array(
        "labels" => array(
          "name"                => "Custom Posts", // Rename these to suit
          "singular_name"       => "Custom Post",
          "add_new"             => "Add New",
          "add_new_item"        => "Add New Custom Post",
          "edit"                => "Edit",
          "edit_item"           => "Edit Custom Post",
          "new_item"            => "New Custom Post",
          "view"                => "View Custom Post",
          "view_item"           => "View Custom Post",
          "search_items"        => "Search Custom Posts",
          "not_found"           => "No Custom Posts found",
          "not_found_in_trash"  => "No Custom Posts found in Trash"
        ),
        "menu_position"         => 5,
        "menu_icon"             => "dashicons-awards",
        "public"                => true,
        "show_in_rest"          => true,
        "show_ui"               => true,
        "show_in_menu"          => true,
        "publicly_queryable"    => true,
        "capability_type"       => "page",
        "hierarchical"          => false,
        "has_archive"           => true,
        "supports"              => array("title","thumbnail","editor","revisions","excerpt","author"), // Other Options: trackbacks, custom-fields, page-attributes, comments, post-formats
        "can_export"            => true, // Allows export in Tools > Export
        "taxonomies"            => array(), // Add supported taxonomies,
        "show_in_graphql"       => true,
        "graphql_single_name"   => "CustomPost",
        "graphql_plural_name"   => "CustomPosts",
      )
    );
  }

There are example Custom Shortcodes and Custom Taxonomies set up to display in the API as well as in the Graph:

function headless_shortcode( $atts , $content = null ) {
    // Attributes
    $args = shortcode_atts( array(
        "link" => "",
        "target" => "_self",
        "rel" => "",
        "class" => "",
    ), $atts );

    return '<a href="' . $args['link'] . '" target="' . $args['target'] . '" rel="' . $args['rel'] . '" class="btn ' . $args['class'] . '">' . $content . '</a>';

}
add_shortcode( "headless", "headless_shortcode" );
add_filter("acf/format_value/type=textarea", "do_shortcode");

function headless_taxonomy() {
    $labels = array(
      "name"                       => "Taxonomies",
      "singular_name"              => "Taxonomy",
      "menu_name"                  => "Taxonomies",
      "all_items"                  => "All Taxonomies",
      "parent_item"                => "Parent Taxonomy",
      "parent_item_colon"          => "Parent Taxonomy:",
      "new_item_name"              => "New Taxonomy",
      "add_new_item"               => "Add Taxonomy",
      "edit_item"                  => "Edit Taxonomy",
      "update_item"                => "Update Taxonomy",
      "view_item"                  => "View Taxonomy",
      "separate_items_with_commas" => "Separate Taxonomies with commas",
      "add_or_remove_items"        => "Add or remove Taxonomies",
      "choose_from_most_used"      => "Choose from the most used",
      "popular_items"              => "Popular Taxonomies",
      "search_items"               => "Search Taxonomies",
      "not_found"                  => "Not Found",
      "no_terms"                   => "No Taxonomies",
      "items_list"                 => "Taxonomies list",
      "items_list_navigation"      => "Taxonomies list navigation",
    );
    $args = array(
      "labels"                     => $labels,
      "hierarchical"               => false,
      "public"                     => true,
      "show_ui"                    => true,
      "show_in_quick_edit"         => false,
      "meta_box_cb"                => false,
      "show_admin_column"          => false,
      "show_in_nav_menus"          => false,
      "show_tagcloud"              => false,
      "show_in_rest"               => true,
      "show_in_graphql"            => true,
      "graphql_single_name"        => "Taxonomy",
      "graphql_plural_name"        => "Taxonomies",
    );

    register_taxonomy( "taxonomy", array( "page" ), $args );
  }

  add_action( "init", "headless_taxonomy", 0 );

There are some utility functions that help with reorganizing/removing menu items and disabling RSS:

function remove_menus() {
    remove_menu_page( "index.php" ); //Dashboard
    remove_menu_page( "jetpack" ); //Jetpack*
    remove_menu_page( "edit-comments.php" ); //Comments
}

add_action( "admin_menu", "remove_menus" );

function headless_custom_menu_order( $menu_ord ) {
    if ( !$menu_ord ) return true;

    return array(
        "edit.php?post_type=page", // Pages
        "edit.php", // Posts
        "edit.php?post_type=custom_posts", // Custom Post Type
        "separator1", // First separator

        "upload.php", // Media
        "themes.php", // Appearance
        "plugins.php", // Plugins
        "users.php", // Users
        "separator2", // Second separator

        "tools.php", // Tools
        "options-general.php", // Settings
        "separator-last", // Last separator
    );
}
add_filter( "custom_menu_order", "headless_custom_menu_order", 10, 1 );
add_filter( "menu_order", "headless_custom_menu_order", 10, 1 );

All ACF fields that are empty are nullified by default:

// Return `null` if an empty value is returned from ACF.
if (!function_exists("acf_nullify_empty")) {
  function acf_nullify_empty($value, $post_id, $field) {
      if (empty($value)) {
          return null;
      }
      return $value;
  }
}
add_filter("acf/format_value", "acf_nullify_empty", 100, 3);

Headless is available over on GitHub. Download it, peruse it, use it and let me know what you think!

Discussion

pic
Editor guide