Highlight Search Terms in WordPress Search Results

WordPress search results show a list of titles and excerpts, but not why each post matched. Highlighting the search terms in WordPress search results lets visitors scan the list and pick the right post faster.

This snippet wraps every searched word in a <mark> element in the post titles and excerpts on the search page. It works with your existing search template: classic themes, block themes with the Post Title and Post Excerpt blocks, and page builders that print titles and excerpts the WordPress way. No custom query, no template to rebuild.

The snippet

<?php
/**
 * Highlight the search words in post titles and excerpts on the WordPress search results page.
 * Works with classic themes, block themes (Post Title / Post Excerpt blocks) and page builders.
 */
add_filter( 'the_title', 'dpc_search_highlight_title', 20, 2 );
add_filter( 'get_the_excerpt', 'dpc_search_highlight_excerpt', 20, 2 );
// The Post Excerpt block trims the excerpt with wp_trim_words(), which strips tags: highlight its output instead.
add_filter( 'render_block_core/post-excerpt', 'dpc_search_highlight_excerpt_block', 10, 3 );

function dpc_search_highlight_title( $title, $post_id = 0 ) {
	return dpc_search_highlight_applies( (int) $post_id ) ? dpc_search_highlight( (string) $title ) : $title;
}

function dpc_search_highlight_excerpt( $excerpt, $post = null ) {
	$post = get_post( $post );
	return $post && dpc_search_highlight_applies( $post->ID ) ? dpc_search_highlight( (string) $excerpt ) : $excerpt;
}

function dpc_search_highlight_excerpt_block( $content, $parsed_block, $block ) {
	$post_id = isset( $block->context['postId'] ) ? (int) $block->context['postId'] : 0;
	return dpc_search_highlight_applies( $post_id ) ? dpc_search_highlight( (string) $content ) : $content;
}

/** Only on the front-end search page, only for posts in the results, never inside <head> (SEO meta). */
function dpc_search_highlight_applies( int $post_id ): bool {
	global $wp_query;

	if ( ! $post_id || is_admin() || is_feed() || ! is_search() || doing_action( 'wp_head' ) || ! $wp_query instanceof WP_Query ) {
		return false;
	}

	return in_array( $post_id, array_map( 'intval', wp_list_pluck( (array) $wp_query->posts, 'ID' ) ), true );
}

/** Wraps every search word in <mark>, in text only: HTML tags and entities stay untouched. */
function dpc_search_highlight( string $text ): string {
	// The words WordPress really searched for: quoted phrases kept, stopwords and -exclusions left out.
	$terms = array();
	foreach ( (array) get_query_var( 'search_terms' ) as $term ) {
		$term = trim( (string) $term );
		if ( mb_strlen( $term ) > 1 && '-' !== $term[0] ) {
			$terms[] = preg_quote( $term, '/' );
		}
	}

	if ( ! $terms || '' === $text || false !== strpos( $text, 'dpc-search-highlight' ) ) {
		return $text;
	}

	// Longest first, so "apples" wins over "apple".
	usort( $terms, static fn( $a, $b ) => strlen( $b ) <=> strlen( $a ) );
	$pattern = '/(' . implode( '|', $terms ) . ')/iu';

	$parts = preg_split( '/(<[^>]*>|&(?:[a-z][a-z0-9]*|#[0-9]+|#x[0-9a-f]+);)/i', $text, -1, PREG_SPLIT_DELIM_CAPTURE );
	if ( false === $parts ) {
		return $text;
	}

	foreach ( $parts as $i => $part ) {
		// Even keys are text, odd keys are the tags and entities preg_split() captured.
		if ( 0 === $i % 2 && '' !== $part ) {
			$marked = preg_replace( $pattern, '<mark class="dpc-search-highlight">$1</mark>', $part );
			$parts[ $i ] = null === $marked ? $part : $marked;
		}
	}

	return implode( '', $parts );
}

Add it with Scripts Organizer

In Scripts Organizer go to Code Blocks → Add New and give the block a name. Set Trigger location to Everywhere, tick PHP under Script location, paste the code into the PHP editor and publish. Leave Action/Hook empty.

The older version of this post was a shortcode-style code block with its own search loop. It is now a set of filters on purpose: they change the titles and excerpts your theme already prints, so the search page keeps its design, pagination and post types, and you do not place anything in the content. The filters check for the search page themselves, so Everywhere is the right trigger. The same code works unchanged in a small plugin or an mu-plugin.

How it works

  • the_title and get_the_excerpt run the highlighter only on the front-end search page, and only for posts that are in the search results. Menus, widgets and other lists on the page stay unchanged.
  • The Post Excerpt block shortens the excerpt with wp_trim_words(), which removes tags, so the snippet highlights that block’s rendered HTML through render_block_core/post-excerpt instead.
  • The search words come from WordPress itself (search_terms), so quoted phrases stay together, stopwords like “the” are skipped and -excluded words are not highlighted.
  • Every word goes through preg_quote(). In the older version a search for c++ or ( broke the regular expression, and an empty search matched everything.
  • The text is split into HTML tags, entities and plain text, and only the plain text gets <mark> tags, so links, attributes and entities like &#038; are never broken. Nothing from the search query is printed: the snippet only wraps text that is already in the title or excerpt.
  • It does nothing while wp_head runs, so SEO plugins that build meta descriptions from the excerpt never get <mark> tags.

Customise it

  • Browsers show <mark> with a yellow background by default. To change it, add CSS such as .dpc-search-highlight{background:#fde68a;color:inherit;padding:0 .1em} to your theme or a Scripts Organizer Header block (Header Mode: CSS).
  • Highlight in the content too: add add_filter( 'the_content', 'dpc_search_highlight_excerpt', 20 ); only if your search template shows full content.
  • Limit the search itself to posts with Limit the Search result to only POSTS.

Gotchas

  • A theme that puts get_the_title() into an attribute with esc_attr() (for example an aria-label) would show the <mark> tags as text there. Themes that use the_title_attribute() are fine, because it strips tags.
  • Words are matched inside longer words, so “apple” also marks the start of “apples”, the same way WordPress search matches them.
  • Live or AJAX search plugins do not use the search page, so their results are not highlighted.

In the Scripts Organizer library Import it with one click from Scripts Organizer.

// newsletter

Three tested WordPress snippets a week

Blocks, security, performance, Tailwind, PHP and React. Copy, paste, ship. You’ll also get DPlugins product updates. No spam, and you can unsubscribe anytime.

// newsletter

Three tested WordPress snippets a week

Blocks, security, performance, Tailwind, PHP and React. Copy, paste, ship. You'll also get DPlugins product updates. No spam, and you can unsubscribe anytime.