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, andog:imageprofile:first_name,profile:last_name, andprofile:usernamefor author routesarticle:published_time,article:modified_time, andarticle:authorfor normal WordPress posts- one
192x192icon 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:
| Output | Route precedence |
|---|---|
| title | seo.title_<route.lang>, seo.title, then <pageName> - <site name> |
| description | seo.description_<route.lang>, seo.description, then the normal context/global description fallback |
| OG title | seo.og_title_<route.lang>, seo.og_title, then the resolved title |
| OG description | seo.og_description_<route.lang>, seo.og_description, then the resolved description |
| OG image | seo.og_image, then the context/global OG image |
| OG URL | route.url |
| OG type | seo.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_indexin 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:
sourceobjectroute
source
source tells you where the filter is being executed.
Possible values:
wp_headroute
object
object is the current WordPress object when one is available.
Examples:
WP_PostWP_TermWP_Usernull
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.jssrc/mu-plugins/plugins/reactwp/template/inc/runtime/RouteResolver.phpsrc/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.