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.
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.