DEV Community

Cover image for The WordPress Excerpt Filter Isn't Universal — A Debugging Story
Webequipe for Webequipe

Posted on

The WordPress Excerpt Filter Isn't Universal — A Debugging Story

A support ticket landed recently that looked, on the surface, like a rendering bug: a plugin's search results were showing up as literal HTML tags on the page instead of a nicely formatted result card. <div class="entry-summary"> and friends, printed right there as visible text, as if the browser had simply given up on parsing the markup.

It turned out to be a much more interesting bug than "the HTML is broken." It's a compatibility gap that quietly affects any WordPress plugin that tries to inject rich HTML into a search result excerpt — and it's worth understanding if you've ever built a plugin that hooks into the_excerpt().

The setup

The plugin in question indexes PDF files and injects them into WordPress search results, alongside your regular posts and pages. When a PDF matches a search, it needs to show more than a plain title: a thumbnail preview, the file size, the page count, a highlighted snippet of matching text. None of that fits into what WordPress expects an excerpt to contain, which is, by convention, plain text.

So the plugin does what a lot of plugins do: it hooks get_the_excerpt, the_excerpt, the_content, and wp_trim_words, and on those hooks it builds up a chunk of HTML — a <div> or <span> wrapper with meta info, an <img> preview, <mark> tags around the matched keywords — and returns that HTML as the "excerpt." As long as the active theme calls WordPress's own the_excerpt()/get_the_excerpt() functions to render that region of the page, the browser receives real markup and renders it as intended.

The plugin's changelog showed real engineering effort here — explicit compatibility fixes for Astra, Divi, Avada, and Elementor, plus block-theme support via render_block. Each of those required understanding how that specific theme reaches into the post data to build its search result cards. That's the part that doesn't get talked about enough: "supports WordPress" and "supports every theme's excerpt rendering path" are not the same claim.

Where it broke

One popular multipurpose theme wasn't on that compatibility list, and a customer running it hit exactly the bug described above. Digging into the theme's helper functions turned up the reason, and it's not unique to this theme — it's a pattern worth watching for in general.

Many big, feature-rich WordPress themes don't just call the_excerpt() and trust WordPress's own excerpt pipeline. They ship their own excerpt generator — a theme-specific function that reads post_excerpt or post_content directly, truncates it to a target length, and outputs it. This is usually done for good reasons: consistent excerpt lengths across templates, custom "read more" behavior, support for excerpts on custom post types that don't behave well with the default logic.

The problem is that a custom excerpt generator like this typically assumes its input is plain text. So it does something like:

$raw = get_post_field( 'post_content', $post_id );
$trimmed = wp_trim_words( $raw, $length );
echo esc_html( $trimmed );
Enter fullscreen mode Exit fullscreen mode

That esc_html() call is completely reasonable if the source is plain text. It's a defensive habit — never trust unescaped content into the page. But when the input already contains real HTML markup (because a plugin built it for exactly that purpose), esc_html() converts every < and > into &lt; and &gt;. The server sends valid entities, the browser dutifully renders them as literal text, and you get exactly the symptom from the support ticket: a page full of visible tag soup where a nicely formatted card should be.

Crucially, this bypasses the plugin entirely. The plugin's hooks on get_the_excerpt and the_excerpt never fire, because the theme isn't calling those WordPress functions — it has its own code path. All the careful Astra/Divi/Avada/Elementor compatibility work in the world doesn't help, because those fixes assume the theme is at least going through the standard filter chain somewhere.

The general lesson

If you're building a WordPress plugin that needs to output HTML through what is conventionally a plain-text field — excerpts are the classic example, but this also applies to things like post titles, meta descriptions, or any "summary" field a theme might touch — keep three things in mind:

The excerpt filter chain is a convention, not a contract. get_the_excerpt, the_excerpt, and wp_trim_words are the standard way WordPress themes render excerpts, and most themes use them. But nothing forces a theme to. Popular multipurpose and page-builder-adjacent themes especially tend to reimplement this logic for extra control, and when they do, your filters simply never run.

"It works in theme X" doesn't generalize. Compatibility with WordPress core's rendering pipeline isn't the same as compatibility with a given theme's rendering pipeline. If your plugin injects HTML into what's normally plain-text territory, budget for theme-specific escape hatches — a filter your users (or you) can hook to redirect a theme's custom excerpt function back to the_excerpt(), for example.

When something you expect to be markup shows up as literal tag text on the page, suspect double-escaping before you suspect a rendering bug. The moment you see <div class=...> printed as visible characters rather than parsed as an element, the underlying cause is almost always esc_html() (or an equivalent) being applied to a string that already contains real HTML — not malformed markup, not a browser quirk. Check the page source directly: if you see &lt;div...&gt; in the raw response, that confirms it immediately.

Why this matters beyond one plugin

This class of bug is a nice reminder that WordPress's plugin/theme ecosystem is held together by conventions that are easy to assume are guarantees. The the_excerpt() filter chain has been part of WordPress for well over a decade, and it's tempting to treat it as a stable, universal integration point. For the vast majority of themes, it is. But "vast majority" isn't "all," and the exceptions tend to be exactly the themes with the largest install bases — the ones that ship enough custom functionality to need their own take on something as basic as an excerpt.

If you maintain a plugin that hooks core content filters to inject rich output, it's worth explicitly testing against a handful of the biggest multipurpose themes, not just the block-theme defaults — and building in an extension point so users (or you, later) can patch the gap without waiting for a plugin update.

Have you run into a similar "my HTML got escaped because the theme has its own renderer" bug? I'd be curious to hear which theme did it to you — drop it in the comments.

Top comments (0)