Insert AdSense Ads Inside WordPress Loops (Any Position, No Plugin)

Insert AdSense (or any ad network) at a specific position inside WordPress archive and query loops. One filter on the_posts splices a pseudo-post into the array at the index you choose. Surgical control, no ad-injection plugin.

The block-editor ad plugins all do the same thing badly — iframe inside the first post, full-width slot before the footer, or a 90 KB settings panel to configure basic positioning. This snippet gives you surgical control. Swap the AdSense block for any HTML you want — newsletter box, affiliate CTA, related-posts embed — and the pattern is identical.

What this snippet does

  • Injects an ad after the Nth post in the main loop on archive pages only
  • Works with any ad network code (AdSense, Ezoic, Mediavine, direct sponsors)
  • Scopes to page 1 so you do not burn impressions on deeper pagination
  • No plugin, no settings page — position is a PHP constant you set once
  • GDPR/consent-ready — wrap the ad HTML in a consent conditional
  • Alternative no-code path via Ad Inserter or Advanced Ads plugins for readers who want a UI

Install and use

Paste into your child theme functions.php. Edit GT_AD_POSITION — 1-indexed, so 3 places the ad after the 3rd post. Replace the placeholder AdSense snippet inside gt_render_ad_slot() with your real ad code. For multiple positions, call the splice logic twice with different indexes and different ad slots.

<?php
/**
 * Inject an AdSense ad after the Nth post on archive pages.
 * GT_AD_POSITION is 1-indexed (2 = between 2nd and 3rd post).
 */
defined( 'GT_AD_POSITION' ) or define( 'GT_AD_POSITION', 3 );

add_filter( 'the_posts', function ( $posts, $query ) {
    if ( is_admin() || ! $query->is_main_query() ) return $posts;
    if ( $query->get( 'paged' ) > 1 ) return $posts;
    if ( ! $query->is_archive() && ! $query->is_home() ) return $posts;
    if ( count( $posts ) < GT_AD_POSITION ) return $posts;

    $ad             = new stdClass();
    $ad->ID         = 0;
    $ad->post_type  = 'gt_ad_slot';
    $ad->post_title = '';
    $ad->post_content = gt_render_ad_slot();
    $ad->post_status = 'publish';

    array_splice( $posts, GT_AD_POSITION, 0, array( $ad ) );
    return $posts;
}, 10, 2 );

function gt_render_ad_slot() {
    ob_start(); ?>
    <div class="gt-ad-slot" aria-label="Advertisement">
        <ins class="adsbygoogle"
             style="display:block"
             data-ad-client="ca-pub-XXXXXXXXXXXXXXXX"
             data-ad-slot="1234567890"
             data-ad-format="auto"
             data-full-width-responsive="true"></ins>
        <script>(adsbygoogle = window.adsbygoogle || []).push({});</script>
    </div>
    <?php
    return ob_get_clean();
}

How it works

The the_posts filter fires after WP_Query builds its results array but before the template iterates. We gate on four conditions: not in admin, main query only, page 1 only, archive or home only. If all pass, we build a stub post object with a fake post_type of gt_ad_slot whose post_content is the ad HTML. array_splice inserts it at the requested index. The loop treats it like any other post, your theme renders its card or teaser, and the ad HTML flows through the content. Because the stub is never stored in the database, pagination, counts, and the REST API stay correct.

Download and source

FAQs

Does this violate AdSense placement policy?

No. AdSense allows programmatic placement as long as the ad is distinguishable from content and you stay under three ads per viewport. The aria-label="Advertisement" and the spacing between ads keep you compliant.

Will Core Web Vitals get hit?

Yes — any third-party ad script hurts CLS and LCP. This is true of every ad network, not this snippet. Mitigate with a min-height on .gt-ad-slot to reserve space, and load the ad script with async. The snippet itself adds zero overhead.

Can I gate this behind GDPR consent?

Yes. Wrap the ad HTML in a $_COOKIE["gt_consent"] check, or call your CMP server-side consent API before echoing. The stub post is always injected; only the ad HTML respects consent.

Does it work on custom post type archives?

Yes. is_archive() returns true for all CPT archives, including /snippets/, /lessons/, and /deals/. Restrict further with is_post_type_archive("post") if you only want it on the blog.

How do I insert multiple ads per page?

Call array_splice multiple times. Splice from the end first to avoid index drift, or refactor into a loop that splices at positions 3, 6, 9.

Why not use AdSense Auto Ads?

Auto Ads picks placements via Google’s ML, which consistently inserts ads in layout-breaking positions (mid-paragraph, over images, under the H1). Manual placement gives you measurable control and a better RPM.

Leave a Comment