Skip to content

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.

Updated 17 Sept 20265 min read

On this page
  1. Why themes need wiring at all
  2. 1. Diagnose first
  3. 2. The two fields that break because of the theme
  4. Meta description: the theme's tag wins
  5. SEO title: only a conflict if the theme filters late
  6. 3. The wiring recipe
  7. Step 1: functions.php
  8. Step 2: the templates
  9. Step 3: what not to wire
  10. 4. Symptom to cause
  11. 5. Mistakes that cost real time
  12. 6. Verify
  13. 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

FieldHow the connector delivers itNeeds the theme?
SEO titleDocument-title filter at priority 99Only if the theme filters at 100 or higher
Meta descriptionPrints <meta name="description"> on wp_headOnly if the theme prints its own
H1rankgrep_h1() called from the templateAlways
Opening paragraphrankgrep_intro() called from the templateAlways

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?

bash
curl -s https://example.com/wp-json/rankgrep/v1/status
  • No "active":true at all: the plugin is not installed. Install it first.
  • "supports_headings":false: the theme has not opted in. Section 3, step 1.
  • "supports_headings":true but headings still do not apply: the templates are not calling the helpers. Section 3, step 2.

Does the theme print its own SEO tags?

bash
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:

php
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:

php
add_theme_support( 'rankgrep-headings' );

Then, anywhere in functions.php:

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.

php
<!-- 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.

php
<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():

php
<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 saysWhat is wrongFix
"…comes from your theme, not its content — install the RankGrep Connector plugin"The connector is not installedInstall it
"…comes from your theme — add RankGrep heading support to the theme"Plugin installed, theme not opted in or templates not calling the helpersSection 3
"WordPress accepted the new meta description, but the live page still shows …"Theme prints its own description tagSection 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 itInstall the connector
Heading applies but the next audit reopens the issueWrong paragraph wiredSection 3, step 2
Fatal error after deactivating the pluginTemplates call rankgrep_h1() directlyUse 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="…" into esc_attr(). &amp; passed through esc_attr() ships as &amp;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 -l does not catch it, and it emits output before headers on every request.

6. Verify

bash
# 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.