Customise WordPress Excerpt Length and Read More Link with Filters and wp_trim_words

WordPress’s built-in excerpt system — accessed with the_excerpt() or get_the_excerpt() — generates a trimmed version of post content that is useful for archive pages, search results, and card layouts. By default it uses the manual excerpt from the post editor if one is written, or auto-generates a 55-word trim from the post content if not. But the default trim length is rarely what a design needs, the default trailing string is the semantically awkward […], and the auto-generated excerpt includes shortcodes, HTML, and Gutenberg block comments unless specifically cleaned. WordPress provides two filters to customize this: excerpt_length to change the word count and excerpt_more to change the trailing string. For complete control — including returning a character-limited excerpt, stripping specific tags, or applying custom markup — WordPress also provides wp_trim_words() as a standalone utility that can be called anywhere.

Problem: WordPress's default 55-word excerpt with a […] suffix does not match your design. You need a shorter excerpt with a "Read more" link, and clean output without HTML tags or Gutenberg block markup.

Solution: Filter excerpt_length and excerpt_more for global changes. Use wp_trim_words() or a custom excerpt function for per-context control.

<?php
// ── Global excerpt length and "more" string ───────────────────────────
add_filter( 'excerpt_length', function () { return 25; }, 999 );

add_filter( 'excerpt_more', function () {
    return '… <a href="' . esc_url( get_permalink() ) . '" class="read-more">'
         . esc_html__( 'Read more', 'textdomain' ) . '</a>';
} );

// ── Custom excerpt function for use in templates ───────────────────────
/**
 * Return a clean, word-limited excerpt from any post.
 *
 * @param  int    $post_id
 * @param  int    $words      Word limit.
 * @param  string $more_text  Trailing text/HTML appended when trimmed.
 * @return string
 */
function get_custom_excerpt( $post_id, $words = 20, $more_text = '…' ) {
    // Use manual excerpt if set; fall back to post content
    $post    = get_post( $post_id );
    $content = $post->post_excerpt ?: $post->post_content;

    // Strip shortcodes, Gutenberg block comments, and HTML tags
    $content = strip_shortcodes( $content );
    $content = wp_strip_all_tags( $content );

    return wp_trim_words( $content, $words, $more_text );
}

// Usage in a template:
echo esc_html( get_custom_excerpt( get_the_ID(), 20 ) );

If you need character-based (not word-based) limiting — useful for card layouts where text must fit a fixed-height box:

<?php
function get_char_excerpt( $post_id, $chars = 120 ) {
    $post    = get_post( $post_id );
    $content = $post->post_excerpt ?: $post->post_content;
    $content = wp_strip_all_tags( strip_shortcodes( $content ) );

    if ( mb_strlen( $content ) <= $chars ) {
        return $content;
    }

    // Trim to last complete word within the limit
    return mb_substr( $content, 0, mb_strrpos( mb_substr( $content, 0, $chars ), ' ' ) ) . '…';
}

NOTE: The excerpt_length and excerpt_more filters apply globally to all calls to the_excerpt(). If you need different excerpt lengths on different parts of the same page — for example, short excerpts in a sidebar widget and longer ones in the main loop — use get_custom_excerpt() directly in the template rather than relying on the global filters.