How to Implement Adsterra Ads in React.js / Next.js Projects

How to Implement Adsterra Ads in React.js / Next.js Projects

Implementing Multiple Adsterra Ads in React.js / Next.js Projects

Integrating Adsterra banner ads into React or Next.js applications is notoriously challenging. Unlike WordPress or static HTML sites where you simply paste ad code into widgets, React’s component-based architecture and client-side rendering create unique complications.

This guide documents a real-world implementation for Ultimate Calc Hub, a calculator website built with React, where we successfully implemented 5 different Adsterra banner ads on the same page after trying multiple approaches.

What We’ll Cover

  • Why standard Adsterra integration fails in React
  • 5 different approaches we tried (and why 4 failed)
  • The final working solution with complete code
  • Best practices for production

The Problem with Third-Party Ad Scripts in React

How Adsterra Ads Work

Adsterra provides banner ad codes that look like this:

<script type="text/javascript">
  atOptions = {
    'key': 'YOUR_AD_KEY_HERE',
    'format': 'iframe',
    'height': 50,
    'width': 320,
    'params': {}
  };
</script>
<script type="text/javascript" src="//www.highperformanceformat.com/YOUR_AD_KEY/invoke.js"></script>

The fundamental issue: All Adsterra ads use the same global variable name atOptions. When you try to load multiple ads:

  1. First ad sets window.atOptions = { key: 'ad1' }
  2. Second ad overwrites it: window.atOptions = { key: 'ad2' }
  3. Third ad overwrites again: window.atOptions = { key: 'ad3' }
  4. Result: Only the last ad loads correctly

WordPress vs React

Why it works in WordPress:

  • WordPress renders server-side HTML
  • Each widget is isolated in the DOM when page loads
  • Scripts execute in sequence before any overwriting occurs

Why it fails in React:

  • Components mount nearly simultaneously
  • React re-renders dynamically
  • All components share the same global window object
  • Race conditions and timing issues
Video on: How to Implement Adsterra Ads in React and Next.js Projects

Failed Approaches and Why They Don’t Work

We tried 5 different approaches before finding the solution. Here’s what failed and why:

❌ Approach 1: Direct Script Injection

What we tried:

useEffect(() => {
  (window as any).atOptions = {
    key: 'ad_key_here',
    format: 'iframe',
    height: 50,
    width: 320,
    params: {},
  };

  const script = document.createElement('script');
  script.src = '//www.highperformanceformat.com/ad_key/invoke.js';
  document.body.appendChild(script);
}, []);

Why it failed:

  • All components execute useEffect nearly simultaneously
  • Each overwrites the global atOptions
  • Only the last component’s ad loads
  • No true isolation between ads

Result: Only 1 ad displays (usually the last one to mount)

❌ Approach 2: Staggered Loading with Delays

What we tried:

const delays = {
  'top': 100,
  'sidebar-left': 300,
  'sidebar-right': 500,
  'middle': 700,
  'bottom': 900,
};

setTimeout(() => {
  (window as any).atOptions = config;
  const script = document.createElement('script');
  script.src = `//www.highperformanceformat.com/${config.key}/invoke.js`;
  container.appendChild(script);
}, delays[position]);

Why it failed:

  • Timing is inconsistent across devices
  • Network speed affects script loading order
  • On slow connections, delays don’t prevent conflicts
  • Fast connections might still cause race conditions
  • Not sustainable – requires constant tweaking

Result: Sometimes 2-3 ads load, but inconsistent and unreliable

❌ Approach 3: Shared Configuration Object

What we tried:

const adConfigs = {
  top: { key: 'ad1', width: 320, height: 50 },
  sidebar: { key: 'ad2', width: 160, height: 600 },
};

const AdPlaceholder = ({ position }) => {
  const config = adConfigs[position];
  // Load ad using config
};

Why it failed:

  • Still uses the same global atOptions variable
  • Shared state doesn’t solve the fundamental conflict
  • All components still overwrite each other’s configuration

Result: Same problem – only 1 ad displays

❌ Approach 4: HTML String Parsing

What we tried:

const adHTML = `
  <script>atOptions = {...}</script>
  <script src="invoke.js"></script>
`;

const tempDiv = document.createElement('div');
tempDiv.innerHTML = adHTML;
Array.from(tempDiv.children).forEach((element) => {
  // Clone and append scripts
  containerRef.current?.appendChild(element.cloneNode(true));
});

Why it failed:

  • Scripts still execute in the main window context
  • No isolation from the global scope
  • Same atOptions conflict persists

Result: Slightly better but still unreliable (1-2 ads max)

❌ Approach 5: Separate Component Files

What we tried: Creating individual files for each ad:

/ads
  ├── TopAd.tsx
  ├── SidebarAd.tsx
  ├── MiddleAd.tsx
  └── etc...

Why it failed:

  • File separation doesn’t create scope isolation
  • All components still access the same global window
  • Module boundaries don’t prevent variable conflicts

Result: Same issues – global variable conflicts remain

The Working Solution: iframe srcdoc

Why iframes Work

The only reliable solution is using iframes with the srcdoc attribute. Here’s why:

Complete Isolation: Each iframe has its own window object
No Global Conflicts: atOptions in iframe A ≠ atOptions in iframe B
No Timing Issues: All ads can load simultaneously
Sustainable: No delays, no race conditions, no tweaking needed
WordPress-like Behavior: This is essentially how WordPress isolates widgets

How It Works

const adHTML = `
  <!DOCTYPE html>
  <html>
  <head>
    <style>body { margin: 0; padding: 0; }</style>
  </head>
  <body>
    <script type="text/javascript">
      atOptions = { 'key': 'YOUR_KEY', ... };
    </script>
    <script src="//www.highperformanceformat.com/YOUR_KEY/invoke.js"></script>
  </body>
  </html>
`;

<iframe srcDoc={adHTML} width="320" height="50" />

Each iframe creates a completely isolated JavaScript environment:

  • Iframe 1: iframe1.window.atOptions
  • Iframe 2: iframe2.window.atOptions
  • Main page: window.atOptions (unused)

No conflicts possible!

How to Implement Adsterra Ads in React.js / Next.js Projects

Step-by-Step Implementation

Project Structure

For Ultimate Calc Hub, we organized ads like this:

src/
├── components/
│   ├── Layout.tsx
│   ├── Navigation.tsx
│   ├── Footer.tsx
│   └── ads/
│       ├── TopAd.tsx          (320x50 banner)
│       ├── SidebarAd.tsx  (160x600 skyscraper)
│       ├── MiddleAd.tsx       (300x250 medium rectangle)
│       └── BottomAd.tsx       (468x60 banner)

Step 1: Get Your Adsterra Ad Codes

  1. Log into your Adsterra account
  2. Create separate ad placements for each position
  3. IMPORTANT: Do NOT reuse the same ad code for multiple positions
  4. Note down each ad’s key, width, and height

Example ad codes we used:

  • Top: 7f2c91a0d54b48cfa3e0b19e82c4f7d9 (320×50)
  • Sidebar: e3a5c97f12bd48d0aa48f9136d0b72ce (160×600)
  • Middle: b8f37c29e14a4d60bc28e0ac519873f4 (300×250)
  • Bottom: d2b06f8ac1494dea9c1b7368ef42a0b5 (468×60)

Step 2: Create Individual Ad Components

Each ad gets its own component file with the iframe approach.

Complete Code Examples

TopAd.tsx (320×50 Banner)

import { useState } from 'react';
import { X } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Card } from '@/components/ui/card';

interface TopAdProps {
  className?: string;
}

const TopAd = ({ className = '' }: TopAdProps) => {
  const [isVisible, setIsVisible] = useState(true);

  if (!isVisible) return null;

  const adHTML = `
    <!DOCTYPE html>
    <html>
    <head>
      <style>
        body { margin: 0; padding: 0; overflow: hidden; }
      </style>
    </head>
    <body>
      <script type="text/javascript">
        atOptions = {
          'key': '7f2c91a0d54b48cfa3e0b19e82c4f7d9',
          'format': 'iframe',
          'height': 50,
          'width': 320,
          'params': {}
        };
      </script>
      <script type="text/javascript" src="//www.highperformanceformat.com/7f2c91a0d54b48cfa3e0b19e82c4f7d9/invoke.js"></script>
    </body>
    </html>
  `;

  return (
    <Card 
      className={`relative border-primary/20 bg-gradient-to-r from-primary/5 to-secondary/5 ${className}`} 
      style={{ minWidth: '320px', minHeight: '50px' }}
    >
      <Button
        variant="ghost"
        size="sm"
        className="absolute top-2 right-2 h-6 w-6 p-0 text-muted-foreground hover:text-foreground z-10"
        onClick={() => setIsVisible(false)}
      >
        <X className="h-3 w-3" />
      </Button>
      <div className="flex items-center justify-center w-full h-full p-2" style={{ minWidth: '320px', minHeight: '50px' }}>
        <iframe
          srcDoc={adHTML}
          style={{
            width: '320px',
            height: '50px',
            border: 'none',
            overflow: 'hidden'
          }}
          scrolling="no"
          frameBorder="0"
          title="Top Ad"
        />
      </div>
    </Card>
  );
};

export default TopAd;

SidebarAd.tsx (160×600 Skyscraper)

import { useState } from 'react';
import { X } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Card } from '@/components/ui/card';

interface SidebarLeftAdProps {
  className?: string;
}

const SidebarLeftAd = ({ className = '' }: SidebarLeftAdProps) => {
  const [isVisible, setIsVisible] = useState(true);

  if (!isVisible) return null;

  const adHTML = `
    <!DOCTYPE html>
    <html>
    <head>
      <style>
        body { margin: 0; padding: 0; overflow: hidden; }
      </style>
    </head>
    <body>
      <script type="text/javascript">
        atOptions = {
          'key': 'e3a5c97f12bd48d0aa48f9136d0b72ce',
          'format': 'iframe',
          'height': 600,
          'width': 160,
          'params': {}
        };
      </script>
      <script type="text/javascript" src="//www.highperformanceformat.com/e3a5c97f12bd48d0aa48f9136d0b72ce/invoke.js"></script>
    </body>
    </html>
  `;

  return (
    <Card 
      className={`relative border-primary/20 bg-gradient-to-r from-primary/5 to-secondary/5 ${className}`} 
      style={{ minWidth: '160px', minHeight: '600px' }}
    >
      <Button
        variant="ghost"
        size="sm"
        className="absolute top-2 right-2 h-6 w-6 p-0 text-muted-foreground hover:text-foreground z-10"
        onClick={() => setIsVisible(false)}
      >
        <X className="h-3 w-3" />
      </Button>
      <div className="flex items-center justify-center w-full h-full p-2" style={{ minWidth: '160px', minHeight: '600px' }}>
        <iframe
          srcDoc={adHTML}
          style={{
            width: '160px',
            height: '600px',
            border: 'none',
            overflow: 'hidden'
          }}
          scrolling="no"
          frameBorder="0"
          title="Sidebar Left Ad"
        />
      </div>
    </Card>
  );
};

export default SidebarLeftAd;

Layout.tsx Integration

Here’s how we integrated the ads into Ultimate Calc Hub’s layout:

import { ReactNode, useEffect } from 'react';
import { useLocation } from 'react-router-dom';
import Navigation from './Navigation';
import Footer from './Footer';
import TopAd from './ads/TopAd';
import SidebarAd from './ads/SidebarAd';

interface LayoutProps {
  children: ReactNode;
  showTopAd?: boolean;
}

const Layout = ({ children, showTopAd = true }: LayoutProps) => {
  const location = useLocation();

  // Google Analytics tracking
  useEffect(() => {
    if (window.gtag) {
      window.gtag('config', 'G-BDFTREXX', {
        page_path: location.pathname + location.search,
      });
    }
  }, [location]);

  // Show sidebar ads on /tools/* and /blog/* routes
  const isAdsPage =
    location.pathname.startsWith('/tools/') ||
    location.pathname.startsWith('/blog/');

  return (
    <div className="min-h-screen flex flex-col bg-background">
      <Navigation />
      
      {isAdsPage ? (
        // Layout with sidebar ads
        <div className="flex flex-1 w-full">
          {/* Left Sidebar Ad */}
          <aside className="hidden lg:block w-80 shrink-0 px-4 pt-4 sticky top-24 self-start h-fit">
            <SidebarAd />
          </aside>

          {/* Main Content with Top Ad */}
          <main className="flex-1 px-2 py-4 min-w-0">
            {showTopAd && (
              <div className="max-w-4xl mx-auto mb-4">
                <TopAd />
              </div>
            )}
            <div className="max-w-4xl mx-auto overflow-x-hidden">
              {children}
            </div>
          </main>

          {/* Right Sidebar Ad */}
          <aside className="hidden lg:block w-80 shrink-0 px-4 pt-4 sticky top-24 self-start h-fit">
            <SidebarAd />
          </aside>
        </div>
      ) : (
        // Default layout without sidebar ads
        <>
          {showTopAd && (
            <div className="container mx-auto px-4 py-4">
              <div className="max-w-4xl mx-auto">
                <TopAd />
              </div>
            </div>
          )}
          <main className="flex-1">{children}</main>
        </>
      )}
      
      <Footer />
    </div>
  );
};

export default Layout;

Using Middle and Bottom Ads in Tool Pages

import Layout from '@/components/Layout';
import MiddleAd from '@/components/ads/MiddleAd';
import BottomAd from '@/components/ads/BottomAd';

const ReactionKineticsCalculator = () => {
  return (
    <Layout>
      <h1>Reaction Kinetics Calculator</h1>
      
      {/* Calculator content */}
      <div className="calculator-section">
        {/* Your calculator UI */}
      </div>

      {/* Middle Ad - appears between sections */}
      <MiddleAd className="my-12" />

      {/* More content */}
      <div className="information-section">
        {/* Educational content */}
      </div>

      {/* Bottom Ad - appears at the end */}
      <BottomAd />
    </Layout>
  );
};

export default ReactionKineticsCalculator;

Key Features of Our Implementation

1. Close Button for Better UX

Each ad has a close button that lets users dismiss it:

const [isVisible, setIsVisible] = useState(true);

<Button onClick={() => setIsVisible(false)}>
  <X className="h-3 w-3" />
</Button>

2. Responsive Design

Ads are hidden on mobile to improve user experience:

<aside className="hidden lg:block">
  <SidebarLeftAd />
</aside>

3. Sticky Positioning

Sidebar ads stay visible while scrolling:

position: sticky;
top: 24px;

4. Conditional Ad Display

Show ads only on specific routes:

const isAdsPage =
  location.pathname.startsWith('/tools/') ||
  location.pathname.startsWith('/blog/');

Troubleshooting

Issue: Ads Not Showing Up

Possible causes:

  1. Ad blocker enabled – Test in incognito mode
  2. Adsterra has no inventory – Normal, ads won’t show 100% of the time
  3. Wrong ad key – Double-check your Adsterra dashboard
  4. Domain not approved – Ensure your domain is verified in Adsterra

Solution:

// Add error handling
<iframe
  srcDoc={adHTML}
  onError={() => console.error('Ad failed to load')}
  onLoad={() => console.log('Ad loaded successfully')}
/>

Issue: Only Some Ads Load

Cause: Adsterra doesn’t always have inventory for all placements simultaneously.

Solution: This is normal behavior. Ad networks don’t guarantee 100% fill rate. If you consistently see zero ads, contact Adsterra support.

Issue: Ads Showing Wrong Size

Cause: iframe dimensions don’t match Adsterra ad size.

Solution: Ensure iframe size exactly matches ad dimensions:

style={{
  width: '320px',  // Match Adsterra width
  height: '50px',  // Match Adsterra height
}}

Issue: Ads Blocked by CSP (Content Security Policy)

Cause: Your server’s CSP headers block third-party scripts.

Solution: Add Adsterra domains to your CSP:

Content-Security-Policy: 
  script-src 'self' 'unsafe-inline' *.highperformanceformat.com;
  frame-src 'self' *.highperformanceformat.com;

Best Practices

1. Use Different Ad Codes

Don’t: Reuse the same ad code for multiple positions. However you should notice i use same MiddleAd componnent for several locations in blog post and it works without any issue at all. With the “iframe srcdoc” method reuse is possible in the smart way i have decsribed in my codes.

// BAD - Don't do this!
const adKey = 'same_key_everywhere';

Do: Create separate placements in Adsterra dashboard

// GOOD - Each position has unique code
top: '7f2c91a0d54b48cfa3e0b19e82c4f7d9'
sidebar: '4c91de72b5084f7db39f1a2697ac3be1'

2. Optimize for Mobile

Hide large ads on mobile devices:

<aside className="hidden lg:block">
  <SidebarLeftAd />
</aside>

3. Strategic Ad Placement

  • Top ad: First thing users see
  • Sidebar ads: Non-intrusive, always visible
  • Middle ad: Natural break in content
  • Bottom ad: After user engagement

4. Monitor Performance

Track ad metrics in Adsterra dashboard:

  • Impressions
  • Click-through rate (CTR)
  • Revenue per thousand impressions (RPM)

5. Test Thoroughly

Test on:

  • ✅ Different browsers (Chrome, Firefox, Safari)
  • ✅ Different devices (desktop, tablet, mobile)
  • ✅ With and without ad blockers
  • ✅ Slow and fast internet connections

Performance Considerations

Bundle Size

Each ad component is small (~2KB). Total overhead for 5 ads: ~10KB.

Loading Performance

Iframes add minimal overhead:

  • Initial HTML: ~500 bytes per ad
  • Adsterra scripts load asynchronously
  • No blocking of main thread

SEO Impact

Ads in iframes don’t affect SEO:

  • Content outside iframes is crawlable
  • No duplicate content issues
  • No page speed penalty (async loading)

Alternative Ad Formats

If you need more ads on the same page, consider mixing formats:

Popunder Ads

// Add to index.html or App.tsx
<script>
  var ad_idzone = "YOUR_POPUNDER_ID";
</script>
<script src="//ads.adsterra.com/pop.js"></script>

Social Bar

Non-intrusive sticky bar at bottom of page.

Native Ads

Blend with your content style.

Advantage: Different formats don’t conflict with banner ads!

Comparison: Our Solution vs Other Methods

MethodReliabilityPerformanceMaintenanceVerdict
Direct injection❌ Poor⚡ Fast✅ EasyDon’t use
Staggered delays⚠️ Inconsistent⚡ Fast❌ HardDon’t use
HTML parsing⚠️ Unreliable⚡ Fast⚠️ MediumDon’t use
Separate files❌ Poor⚡ Fast✅ EasyDon’t use
iframe srcdocExcellentFastEasyUSE THIS

Conclusion: Implement Adsterra Ads in React.js

After extensive testing and 5 different approaches, iframe srcdoc is the only reliable, sustainable solution for implementing multiple Adsterra ads in React/Next.js applications.

Key Takeaways

Use iframes with srcdoc for complete isolation
Create separate ad placements in Adsterra
Never reuse ad codes across multiple positions
Avoid delay-based solutions – they’re unreliable
Test thoroughly across devices and browsers

Why This Works

The iframe approach mimics how WordPress handles widgets – complete JavaScript isolation. Each ad runs in its own sandboxed environment with its own window.atOptions, eliminating all conflicts.

Real-World Results

Ultimate Calc Hub now successfully displays:

  • 5 different Adsterra ads simultaneously
  • 100% reliability across all pages
  • Zero conflicts or race conditions
  • Excellent user experience with close buttons
  • Responsive design that works on all devices

Additional Resources

Questions or Issues?

If you encounter problems:

  1. Check Adsterra dashboard – Verify ad codes are active
  2. Test without ad blocker – Disable extensions
  3. Contact Adsterra support – They can verify your setup
  4. Review this guide – Ensure exact implementation

Common mistake: Using the same ad code for multiple positions. Create separate placements!

Author’s Note: This guide is based on real implementation challenges faced while building Ultimate Calc Hub. After trying 5 different approaches over several hours, the iframe solution proved to be the only reliable method. I hope this saves you time and frustration!

Share this:

Similar Posts

Leave a Reply

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