How to inject content with the the_content filter in PHP in WordPress

Contents

Overview: What the_content filter does and when to use it

The the_content filter in WordPress is the primary hook used to alter post content right before it is displayed on the front end. It receives the rendered content (HTML) and returns the modified string. Use it for adding banners, affiliate blocks, related posts, inline scripts/styles (carefully), tracking pixels, short-form transformations, or any HTML you want injected into post output.

Key concepts and cautions

  • Runs on rendered content: the_content receives content after WordPress has applied core formatting and shortcodes (depending on priorities). That means you usually receive HTML, not raw post_content.
  • Priority matters: Filters have numeric priority. A higher number runs later. Choose a priority intentionally so your injected HTML interacts properly with other filters (e.g., shortcodes, wpautop).
  • Avoid infinite loops: Calling apply_filters(the_content, something) inside a the_content callback will re-enter the same filter and cause recursion. Use remove_filter before calling apply_filters or use functions that dont re-run the_content.
  • Context checks: Use is_main_query(), in_the_loop(), is_admin(), is_feed(), and REST_REQUEST to avoid injecting content in admin screens, feeds, AJAX/REST responses, or in side queries.
  • Sanitize output: Escape or sanitize HTML via wp_kses_post, esc_html, esc_attr, esc_url, etc. Treat any dynamic data as untrusted.
  • Performant code only: Heavy database queries, remote API calls, or expensive DOM operations on every page load will kill performance. Cache heavy results with transients or object cache.
  • Gutenberg/blocks: If you need to change block markup at render time, consider render_block or register_block_type server-side render callback. the_content still receives final rendered HTML.

Standard examples

1) Simple append (safe checks for admin, feed, REST, and only for main single posts)


        extra .= 

Related

extra .=

Some related content or ad markup here.

extra .=
// Sanitize/allowed tags example (if content includes user data) // extra = wp_kses( extra, wp_kses_allowed_html( post ) ) return content . extra } return content } add_filter( the_content, my_append_content, 20 )

2) Prepend content

Intro banner or notice
        return banner . content
    }

    return content
}
add_filter( the_content, my_prepend_content, 15 )

3) Insert after the first paragraph

This common pattern splits the rendered HTML by closing paragraph tags and injects after the first paragraph. Be careful: this approach assumes wpautop or blocks produced ltpgt wrappers. Use case-insensitive split and rebuild safely.


    parts = preg_split( #(closing_p)i#, content, 2, PREG_SPLIT_DELIM_CAPTURE )
    if ( ! parts  count( parts ) < 3 ) {
        // Not enough paragraphs — fall back to append
        return content . 
Injected content
} // parts[0] = content before first

// parts[1] = the closing

// parts[2] = rest of content first_paragraph = parts[0] . parts[1] rest = parts[2] injection =
My injected block
return first_paragraph . injection . rest } add_filter( the_content, insert_after_first_paragraph, 20 )

4) Using DOMDocument (robust HTML manipulation)

If you need to work with the document tree rather than string splits, DOMDocument is safer. It requires libxml and is heavier cache the result if used often.

 . content . 

    libxml_use_internal_errors( true )
    dom = new DOMDocument()
    dom->loadHTML(  . wrapped )
    libxml_clear_errors()

    xpath = new DOMXPath( dom )
    // Example: target the first paragraph node
    p_nodes = xpath->query( //div[@class=root-wrapper]//p )
    if ( p_nodes->length ) {
        first_p = p_nodes->item(0)
        new_div = dom->createElement( div )
        new_div->setAttribute( class, injected-via-dom )
        new_div->nodeValue = Injected using DOMDocument
        if ( first_p->nextSibling ) {
            first_p->parentNode->insertBefore( new_div, first_p->nextSibling )
        } else {
            first_p->parentNode->appendChild( new_div )
        }
    }

    // Extract inner HTML of wrapper
    wrapper = dom->getElementsByTagName( div )->item(0)
    output = 
    foreach ( wrapper->childNodes as child ) {
        output .= dom->saveHTML( child )
    }

    return output
}
add_filter( the_content, dom_insert_block, 20 )

Avoiding infinite recursion and correct use of apply_filters

If your function needs to generate HTML by calling apply_filters(the_content, some_text) or get_the_content() which triggers the_content, you must temporarily remove your filter to avoid recursion. Remove it with the same priority and arguments, then restore it after.

Rendered through the_content for consistency

) // re-add filter with same priority add_filter( the_content, append_with_apply, 20 ) return content . other } return content } add_filter( the_content, append_with_apply, 20 )

Working with Gutenberg blocks

Example: modifying a single block type at render time

Added to every paragraph block
    }
    return block_content
}
add_filter( render_block, my_render_block_filter, 10, 2 )

Security and sanitization best practices

Performance tips

Conditional injection checklist

Context Check
Admin screens is_admin()
REST API requests defined(REST_REQUEST) REST_REQUEST
Feeds is_feed()
Main query only is_main_query()
Inside the loop in_the_loop()
Logged-in users is_user_logged_in()

Advanced patterns

1) Only inject for certain post types or taxonomies

 . esc_html( meta ) . 
        }
    }
    return content
}
add_filter( the_content, cpt_injector, 20 )

2) Enqueue scripts/styles only when content is injected

Instead of printing inline scripts inside the_content, enqueue assets conditionally. Use a flag variable or check conditions early (is_singular etc.) and enqueue in wp_enqueue_scripts.


3) Use transients to cache heavy injected content

Generated content at  . date( c ) . 

    // Cache for 12 hours
    set_transient( cache_key, built, 12  HOUR_IN_SECONDS )

    return content . built
}
add_filter( the_content, expensive_injection, 20 )

Testing your filters

factory->post->create( array( post_content => Hello world ) )
        post = get_post( post_id )
        setup_postdata( post )

        // Ensure filter is active
        add_filter( the_content, my_append_content, 20 )

        out = apply_filters( the_content, post->post_content )
        this->assertStringContainsString( Some related content or ad markup here, out )

        wp_reset_postdata()
    }
}

Common pitfalls and how to avoid them

Quick cheat-sheet: function template

Use this canonical structure as a starting point:

 . wp_kses_post( HTML or sanitized text here ) . 

    // Optionally cache heavy builds here

    return content . injected // or injected . content
}
add_filter( the_content, safe_content_injector, 20 )

References and where to read more

Final checklist before deploying to production

  1. Have you restricted injection to the contexts you intend (is_admin, REST, feeds)?
  2. Have you tested with multiple themes and plugins (shortcodes, caching plugins)?
  3. Have you sanitized and escaped all dynamic output?
  4. Have you added caching or transients for heavy builds?
  5. Will your injected HTML break accessibility or SEO? Use semantic markup and ARIA where needed.
  6. Are scripts and styles enqueued properly rather than printed inline when possible?


Acepto donaciones de BAT's mediante el navegador Brave 🙂



Leave a Reply

Your email address will not be published. Required fields are marked *