WordPress wp_trim_words(): Truncate Content at Word Boundaries with a Custom Read More Link

Truncating text to a given length in WordPress is a requirement that appears in card layouts, search result snippets, email previews, and admin list columns. PHP’s native substr() cuts on byte count and can split a multi-byte UTF-8 character in half; mb_substr() fixes the encoding problem but still cuts mid-word. WordPress provides wp_trim_words() which truncates at word boundaries, handles multi-byte characters correctly, optionally appends a custom “more” string, and applies the wp_trim_words filter so themes can override the behaviour globally. For HTML content there is also wp_trim_excerpt() which strips tags first. Knowing which function to use — and how to pair them with read-more links, ellipses, and conditional full-length display — is a daily need in theme and plugin development.

Problem: A card component needs to show the first 25 words of a post's content. Using substr() cuts mid-word and breaks UTF-8 characters in non-English content. The truncated string needs an appended "Read more" link pointing to the full post.

Solution: Use wp_trim_words() with a custom $more string. Strip shortcodes and HTML with strip_shortcodes() and wp_strip_all_tags() before passing to the trimmer to avoid orphaned HTML tags in the output.

<?php
// ── Basic usage — trim to 25 words ────────────────────────────────────
$content = get_the_content();                        // raw post content with HTML
$clean   = wp_strip_all_tags( strip_shortcodes( $content ) ); // plain text only
$trimmed = wp_trim_words( $clean, 25 );              // trims at word boundary
echo esc_html( $trimmed );
// → "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod..."

// ── With a custom read-more link ───────────────────────────────────────
$more_link = sprintf(
    ' <a href="%s" class="read-more">%s</a>',
    esc_url( get_permalink() ),
    esc_html__( 'Read more', 'textdomain' )
);
$trimmed = wp_trim_words( $clean, 25, '…' . $more_link );
echo $trimmed; // ellipsis + link appended after word 25

// ── Reusable helper for card components ──────────────────────────────
function my_card_excerpt( int $post_id = 0, int $words = 20, string $more = '…' ): string {
    $post_id = $post_id ?: get_the_ID();
    $post    = get_post( $post_id );
    if ( ! $post ) return '';

    // Use manual excerpt if set, otherwise fall back to content
    $text = $post->post_excerpt
        ? $post->post_excerpt
        : $post->post_content;

    $text = wp_strip_all_tags( strip_shortcodes( $text ) );
    return wp_trim_words( $text, $words, $more );
}

// In template:
echo esc_html( my_card_excerpt( get_the_ID(), 20 ) );

// ── wp_trim_excerpt() — strips tags then trims (uses excerpt_length option) ─
// This is essentially what the_excerpt() does:
$excerpt = wp_trim_excerpt( get_the_content() );
echo esc_html( $excerpt );

// ── Override word count globally via filter ────────────────────────────
add_filter( 'excerpt_length', fn() => 30, 999 ); // 30 words for all excerpts

// ── Override the more string globally ─────────────────────────────────
add_filter( 'excerpt_more', function ( $more ) {
    return sprintf(
        ' <a class="read-more" href="%s">%s</a>',
        esc_url( get_permalink() ),
        esc_html__( 'Continue reading', 'textdomain' )
    );
}, 10 );

NOTE: wp_trim_words() counts words, not characters — so "25 words" in German (compound words) will produce a much longer string than "25 words" in English. For character-based truncation, use mb_substr() combined with a word-boundary check: trim to the desired character count, then walk backwards to the last space to avoid cutting mid-word. Also note that wp_trim_words() does not strip HTML by default — always call wp_strip_all_tags() first when the input contains markup, or you will get orphaned open tags like <strong> at the end of the truncated string.