NNicheStackTutorials
← All tutorials
SvelteKit

Adsterra Integration in SvelteKit Layout (Without Killing Core Web Vitals)

Lazy-load ad scripts after LCP, slot placement in +layout.svelte, and the CSP headers that broke my first attempt.

·11 min read·SvelteKit
Adsterra Integration in SvelteKit Layout (Without Killing Core Web Vitals) cover

Tested on: SvelteKit 2.16, Adsterra banner 468x60 Before ads: LCP 1.4s, CLS 0.02. Bad integration: LCP 3.8s, CLS 0.34.

Wrong way (what I did first)

app.html head:

<script src="//www.adsterra.com/.../invoke.js"></script>

Ad script blocks parsing. LCP image loads after ad network DNS + JS. Revenue: $0.12/day. Not worth it.

Right way — lazy inject after load

src/lib/components/AdBanner.svelte:

<script lang="ts">
  import { onMount } from 'svelte';

  let { slotId, width = 468, height = 60 } = $props();
  let container: HTMLDivElement;
  let loaded = $state(false);

  onMount(() => {
    const observer = new IntersectionObserver(
      ([entry]) => {
        if (!entry.isIntersecting || loaded) return;
        loaded = true;
        observer.disconnect();

        const ins = document.createElement('ins');
        ins.className = `adsterra-${slotId}`;
        ins.style.display = 'inline-block';
        ins.style.width = `${width}px`;
        ins.style.height = `${height}px`;
        container.appendChild(ins);

        const script = document.createElement('script');
        script.async = true;
        script.src = `https://www.adsterra.com/invoke.js?key=${slotId}`;
        document.body.appendChild(script);
      },
      { rootMargin: '200px' }
    );
    observer.observe(container);
    return () => observer.disconnect();
  });
</script>

<div bind:this={container} class="ad-slot" style="min-height: {height}px; min-width: {width}px;">
  {#if !loaded}<span class="sr-only">Advertisement</span>{/if}
</div>

Reserve space with min-height — prevents CLS when ad fills in.

Drop into layout

src/routes/+layout.svelte:

<script lang="ts">
  import AdBanner from '$lib/components/AdBanner.svelte';
  let { children } = $props();
</script>

<header><!-- nav --></header>
<main>{@render children()}</main>
<aside class="hidden lg:block">
  <AdBanner slotId="YOUR_KEY_HERE" />
</aside>
<footer></footer>

CSP error that blocked ads

Refused to load the script 'https://www.adsterra.com/invoke.js'
because it violates Content-Security-Policy directive: script-src 'self'

svelte.config.js or hosting headers — add:

kit: {
  csp: {
    mode: 'auto',
    directives: {
      'script-src': ["'self'", 'https://www.adsterra.com'],
      'frame-src': ['https://www.adsterra.com']
    }
  }
}

Or set CSP in Cloudflare/Vercel headers panel.

Disable ads on tool pages

src/routes/tools/+layout.svelte — no <AdBanner>. Better UX on interactive pages.

ads.txt

static/ads.txt:

adsterra.com, YOUR_PUBLISHER_ID, DIRECT

Verify: curl https://yoursite.com/ads.txt

Result

LCP with lazy ads: 1.6s (was 3.8s)
CLS: 0.04 (reserved slot height)
RPM: $1.80 on 2k daily pageviews
Ad block rate: ~32% (unchanged)

Never put ad scripts in <head>. Reserve dimensions. Load on intersection.

Tags: SvelteKitAdsterraMonetizationCore Web Vitals