Skip to main content

Head and SEO

Role

ReactWP supports two head-rendering moments:

  • the initial WordPress page render through wp_head
  • client-side navigation through the route payload returned by the REST endpoint

If you want a custom tag to exist in both cases, it must be generated through the rwp_wp_head filter.

Tags Shipped by reactwp-seo

The bundled plugin contributes these entries when their values are available:

  • document charset
  • X-UA-Compatible: IE=edge
  • viewport with maximum-scale=5.0
  • <title>
  • meta description
  • og:type, og:url, og:site_name, og:title, og:description, and og:image
  • profile:first_name, profile:last_name, and profile:username for author routes
  • article:published_time, article:modified_time, and article:author for normal WordPress posts
  • one 192x192 icon link from the global favicon setting

The plugin does not generate a canonical link, Twitter Card fields, JSON-LD, or an XML sitemap. Add those through project code or another compatible plugin when required. A custom tag that must survive client navigation belongs in rwp_wp_head, not only in a separate wp_head callback.

SEO Value Precedence

For a normalized route context, values resolve in this order:

OutputRoute precedence
titleseo.title_<route.lang>, seo.title, then <pageName> - <site name>
descriptionseo.description_<route.lang>, seo.description, then the normal context/global description fallback
OG titleseo.og_title_<route.lang>, seo.og_title, then the resolved title
OG descriptionseo.og_description_<route.lang>, seo.og_description, then the resolved description
OG imageseo.og_image, then the context/global OG image
OG URLroute.url
OG typeseo.og_type, otherwise article for a post, profile for a user, or website

The canonical route key is lang; the plugin also accepts the older language key when project code constructs a legacy SEO context directly. On normal WordPress rendering without a route context, the plugin reads the current content/user/term seo_* ACF meta first, then the matching Site settings option, then its contextual fallback for searches, 404s, authors, terms, posts, or the site name.

Falsy stored values are treated as absent. Image values may be an ACF image array, an attachment ID, or a URL string.

Robots Policy

The plugin modifies WordPress's wp_robots array:

  • search results are noindex, follow
  • 404 responses are noindex, nofollow
  • a site with Discourage search engines enabled is noindex, nofollow
  • content, author, and taxonomy contexts whose Don't index field is enabled are noindex, nofollow
  • every other context is index, follow
  • every branch sets max-image-preview: large

Robots behavior is produced by WordPress's robots API; it is separate from route.head and the browser head synchronizer.

ACF Fields Added by the Plugin

The content SEO group is registered for posts, pages, all user forms, all taxonomy forms, and any extra public post type selected in the global SEO settings. It is marked show_in_rest = 1 and contains:

  • Don't index (seo_do_not_index in stored ACF meta)
  • OG Image (seo_og_image)
  • for every configured ReactWP language: title, description, OG title, and OG description

The SEO Global Settings group appears on Site settings, is also marked show_in_rest = 1, and contains:

  • extra public post types that should receive the content SEO group
  • a favicon constrained to exactly 192 x 192
  • the default OG image
  • for every configured language: description, OG title, and OG description

The global group intentionally has no separate global title field; the WordPress site name is the title fallback. Language fields are generated from the langs repeater, so saving valid language codes before editing SEO settings determines which translated controls exist.

To avoid constructing large choice lists on unrelated administration screens, content fields are assembled only for frontend/REST/AJAX requests and relevant post, profile, user, or term editors. Global fields are assembled for frontend/REST/AJAX requests and the Site settings screen.

The rwp_wp_head Filter

The filter signature is:

add_filter('rwp_wp_head', function($wp_heads, $context = []){
// ...
return $wp_heads;
}, 10, 2);

$wp_heads is an associative array of HTML strings keyed however you want.

Example:

add_filter('rwp_wp_head', function($wp_heads, $context = []){

$wp_heads['potato'] = '<meta name="potato" content="He is a vegetable.">';

return $wp_heads;

}, 10, 2);

Why The Second Parameter Matters

On a direct page load, WordPress is rendering the page normally, so conditional tags like is_front_page() work as expected.

During a React navigation, the head payload is built from the route resolver through the REST endpoint. In that context, conditional tags such as is_front_page() are not reliable enough to drive your logic.

That is why ReactWP also passes a second argument: $context.

$context Shape

ReactWP currently passes these keys:

  • source
  • object
  • route

source

source tells you where the filter is being executed.

Possible values:

  • wp_head
  • route

object

object is the current WordPress object when one is available.

Examples:

  • WP_Post
  • WP_Term
  • WP_User
  • null

route

route is the normalized ReactWP route payload when the filter is executed from the route resolver.

This is the same kind of payload the frontend uses during client-side navigation.

Useful values include:

  • $context['route']['path']
  • $context['route']['template']
  • $context['route']['pageName']
  • $context['route']['is404']
  • $context['route']['seo']

Front Page Example

This is the recommended pattern when a tag must work on both direct loads and React navigation:

add_filter('rwp_wp_head', function($wp_heads, $context = []){

$is_front = false;

if(($context['source'] ?? null) === 'wp_head'){
$is_front = is_front_page();
} elseif(($context['source'] ?? null) === 'route'){
$is_front = (($context['route']['path'] ?? null) === '/');
}

if($is_front){
$wp_heads['potato'] = '<meta name="vegetable" content="he-is">';
}

return $wp_heads;

}, 10, 2);

Initial Render vs React Navigation

ReactWP uses the same filter in two places:

  • the SEO render layer during wp_head
  • the route resolver when building route.head

That means:

  • direct load: the tag is printed by PHP
  • client-side navigation: the tag is sent in the route payload and synced by the frontend runtime

If you only rely on a WordPress conditional without using $context, the tag may work on direct loads but fail after clicking through the React app.

Frontend Sync

The frontend reads route.head and updates the document head after navigation.

The relevant files are:

  • src/themes/reactwp/js/inc/useDocumentMeta.js
  • src/mu-plugins/plugins/reactwp/template/inc/runtime/RouteResolver.php
  • src/plugins/reactwp-seo/template/inc/render.php

If route.head is present, ReactWP syncs those tags directly. If not, it falls back to a smaller SEO sync based on route.seo.

The browser still applies a strict second boundary. It accepts at most 100 head entries of at most 65536 bytes each and keeps only a text-only title, supported meta forms, and selected HTTP(S) links. A PHP string in route.head is not permission to execute a script in React.

Language-Aware SEO Values

Use the language attached to the current route when reading translated SEO fields yourself:

const Template = ({ route, site }) => {
const language = route.lang || site.language || 'en';
const seo = route.seo || {};
const title = seo[`title_${language}`] || seo.title || route.pageName;

return <h1>{title}</h1>;
};

route.lang follows client navigation. site.language is the initial bootstrap fallback, and site.locale contains the complete WordPress locale.

The bundled SEO plugin also localizes its global option fields as window.RWP_SEO before the main theme script. That global exists for direct access and compatibility, but current React runtime code should prefer route.seo, runtime.seoDefaults, and the standard template props so static rendering, SSR, and hydration all consume the same payload contract.

window.RWP_SEO is emitted only when the rwp-main script handle is enqueued. Its value is the global ACF seo option group, not the current route's merged/fallback result.

PHP SEO Methods

The plugin class exposes these static resolvers for project PHP that intentionally shares its precedence rules:

  • ReactWP\Seo\Seo::site_name()
  • ReactWP\Seo\Seo::title($context = [])
  • ReactWP\Seo\Seo::description($context = [])
  • ReactWP\Seo\Seo::og_type($context = [])
  • ReactWP\Seo\Seo::og_site_name($context = [])
  • ReactWP\Seo\Seo::og_title($context = [])
  • ReactWP\Seo\Seo::og_description($context = [])
  • ReactWP\Seo\Seo::og_image($context = [])
  • ReactWP\Seo\Seo::og_url($context = [])

Pass the same object and route context shape used by rwp_wp_head when calling them outside the normal queried-object request.