Theme wiring for headings
For developers. How a theme opts in to RankGrep's H1 and opening-paragraph repairs, and how to stop it fighting the connector's title and description.
On this page
- Why themes need wiring at all
- 1. Diagnose first
- 2. The two fields that break because of the theme
- Meta description: the theme's tag wins
- SEO title: only a conflict if the theme filters late
- 3. The wiring recipe
- Step 1: functions.php
- Step 2: the templates
- Step 3: what not to wire
- 4. Symptom to cause
- 5. Mistakes that cost real time
- 6. Verify
- 7. Packaging on Windows
RankGrep writes four fields: SEO title, meta description, H1, and opening paragraph. Two of them work on any theme. Two only work if the theme asks for them. And a theme can break the first two by printing its own tags. This page is the checklist, written for whoever maintains the theme.
Why themes need wiring at all
| Field | How the connector delivers it | Needs the theme? |
|---|---|---|
| SEO title | Document-title filter at priority 99 | Only if the theme filters at 100 or higher |
| Meta description | Prints <meta name="description"> on wp_head | Only if the theme prints its own |
| H1 | rankgrep_h1() called from the template | Always |
| Opening paragraph | rankgrep_intro() called from the template | Always |
The H1 and the opening paragraph are printed by the template. There is no WordPress hook to override them, and on a theme-driven site they are not in the page content either. The template has to ask for the value.
1. Diagnose first
Does the connector see the theme?
curl -s https://example.com/wp-json/rankgrep/v1/status
- No
"active":trueat all: the plugin is not installed. Install it first. "supports_headings":false: the theme has not opted in. Section 3, step 1."supports_headings":truebut headings still do not apply: the templates are not calling the helpers. Section 3, step 2.
Does the theme print its own SEO tags?
grep -rn 'name="description"' --include=*.php .
grep -rn 'pre_get_document_title\|document_title_parts\|wp_title' --include=*.php .
Any hit is a conflict that will make repairs verify against the theme's tag and roll themselves back. Section 2.
2. The two fields that break because of the theme
Meta description: the theme's tag wins
The connector prints its description on wp_head at priority 10. Themes often print their own at priority 5, which puts it first in the document, and first is what crawlers read.
Symptom. A repair applies, then rolls itself back: "WordPress accepted the new meta description, but the live page still shows … a theme or SEO plugin is likely overriding it." The write genuinely succeeded; the page just does not show it.
Fix. When RankGrep has a description for the page, the theme must print nothing. Ask the connector:
if ( ! function_exists( 'rankgrep_renders_description' ) || ! rankgrep_renders_description() ) {
// print the theme's own <meta name="description"> here
}
SEO title: only a conflict if the theme filters late
The connector filters the document title at priority 99, which beats any theme filtering at the default 10. Only a theme filtering at priority 100 or higher wins. Lower it.
The other title trap is a theme that builds the <title> from the page name. Without the connector, RankGrep's title write has to go to the page name, and that name also feeds get_the_title(): the H1, the breadcrumb, the JSON-LD all inherit it. Install the connector so the SEO title has its own field. RankGrep guards against this, but the guard is a safety net.
3. The wiring recipe
Replace mytheme with the theme's function prefix.
Step 1: functions.php
Inside the theme's after_setup_theme callback, next to the other add_theme_support() calls:
add_theme_support( 'rankgrep-headings' );
Then, anywhere in functions.php:
/**
* Templates call these rather than the plugin's functions directly, so the
* site keeps rendering if the connector is ever deactivated. An undefined
* function would be a fatal error on every page.
*/
function mytheme_h1( $fallback_html ) {
return function_exists( 'rankgrep_h1' ) ? rankgrep_h1( $fallback_html ) : $fallback_html;
}
function mytheme_intro( $fallback_html ) {
return function_exists( 'rankgrep_intro' ) ? rankgrep_intro( $fallback_html ) : $fallback_html;
}
Step 2: the templates
Pass the theme's existing markup as the fallback. The page renders exactly as before until a repair is applied.
<!-- before -->
<h1 class="hero__title">Emergency Plumber in <span>Fourways</span></h1>
<!-- after -->
<h1 class="hero__title">
<?php echo mytheme_h1( 'Emergency Plumber in <span>Fourways</span>' ); ?>
</h1>
The opening paragraph is the first paragraph with 20 or more words once navigation, header and footer are stripped. Wire that paragraph, usually the hero sub-paragraph. Wiring a different one makes repairs apply and then fail verification.
<p class="hero__sub">
<?php echo mytheme_intro( 'Plumbing emergencies do not wait. Our licensed plumbers reach you in 60 minutes.' ); ?>
</p>
Titles printed with the_title():
<h1><?php echo mytheme_h1( esc_html( get_the_title() ) ); ?></h1>
Step 3: what not to wire
- Archive templates (
index.php,archive.php,category.php). Their H1 is usually the archive title, not a page's heading. - 404, search results, paginated listings.
- Page-builder content (Elementor, Divi, WPBakery). The builder stores content in its own fields, which neither path reaches. This is a known gap.
4. Symptom to cause
| What RankGrep says | What is wrong | Fix |
|---|---|---|
| "…comes from your theme, not its content — install the RankGrep Connector plugin" | The connector is not installed | Install it |
| "…comes from your theme — add RankGrep heading support to the theme" | Plugin installed, theme not opted in or templates not calling the helpers | Section 3 |
| "WordPress accepted the new meta description, but the live page still shows …" | Theme prints its own description tag | Section 2 |
| "Writing this title also changed the page's heading to …" | No connector, so the title went to the page name and the theme decorates it | Install the connector |
| Heading applies but the next audit reopens the issue | Wrong paragraph wired | Section 3, step 2 |
| Fatal error after deactivating the plugin | Templates call rankgrep_h1() directly | Use the wrapper |
5. Mistakes that cost real time
- Never let an SEO title reach the page name. It feeds every place the theme prints the title.
- Decode HTML entities when moving a hardcoded
content="…"intoesc_attr().&passed throughesc_attr()ships as&amp;. The opposite holds for H1 and intro fallbacks: they are echoed as HTML, so leave entities as written. - Never print a description tag when RankGrep has one. Two tags is worse than a stale one.
- Always wrap the plugin calls.
- Watch for a byte-order mark or text before
<?php.php -ldoes not catch it, and it emits output before headers on every request.
6. Verify
# Theme support is live
curl -s https://example.com/wp-json/rankgrep/v1/status
# Exactly one description tag, bypassing caches
curl -s "https://example.com/some-page/?cb=$RANDOM" | grep -c 'name="description"'
# The heading reads exactly as before wiring
curl -s "https://example.com/some-page/?cb=$RANDOM" | tr -d '\n' | grep -o '<h1[^>]*>.\{0,120\}'
Wiring must be a no-op until a repair is applied. If any page reads differently before RankGrep has written anything, a fallback was transcribed wrongly. Then apply one real repair of each type and confirm it verifies rather than rolling back.
7. Packaging on Windows
Do not use PowerShell's Compress-Archive for a theme zip. It writes backslash separators into entry names, which WordPress turns into files literally named assets\css\main.css. Use a zip tool that writes forward slashes, and put the theme contents at the zip root with no wrapper folder.