In my previous article, I explained how I initially integrated Google AdSense ad units inside an Angular 18 Single Page Application using a component-based approach. At first glance, the solution worked perfectly. Ads were rendering, the layout looked clean, and everything seemed aligned with how Angular applications are typically structured.
However, after further testing and reviewing Google’s documentation more carefully, I realized that dynamically adding and removing ad elements from the DOM can create problems, especially when Auto Ads are enabled. In some scenarios, this behavior may conflict with Google’s policies or cause ads not to render consistently. That pushed me to rethink the architecture and look for a more stable and policy-friendly implementation.
In this article, I will show you a cleaner and safer approach using an Angular directive. Instead of programmatically injecting ad scripts through a component lifecycle in a way that manipulates the DOM repeatedly, this method replaces a simple placeholder element with the official AdSense push script. The result behaves much closer to traditional static ad placement, which is generally more predictable and compliant.
Why I Switched to a Directive-Based Approach
In Angular, directives are powerful tools that allow us to extend or modify the behavior of existing DOM elements. Unlike components, which encapsulate their own templates and logic, directives can operate directly on a host element and adjust its behavior in a focused way.
When working with advertising scripts such as AdSense, stability and predictability are essential. Google expects ad units to follow a consistent structure. When scripts are injected too dynamically or re-triggered incorrectly, it may result in blank ads, duplicate pushes, or layout instability.
My goal was simple: keep the integration as close as possible to the standard AdSense implementation while still making it reusable inside an Angular SPA.
By using a directive, I can attach a minimal placeholder element in the template and replace it with the required script at the right lifecycle hook. This avoids unnecessary DOM churn and keeps the markup clean, which is also beneficial for crawlers and overall site performance.
Creating the Angular 18 Directive
To begin, I generated a new directive using the Angular CLI:
ng g d directives/ads
This command creates a directive inside the
src/app/directives folder. In Angular 18, standalone directives
make integration even easier because they can be imported directly into
components without being declared in a traditional NgModule.
Conceptually, Angular provides three main types of directives:
Component directives define a template and control a portion
of the UI.
Attribute directives modify the behavior or appearance of
existing elements.
Structural directives change the DOM structure itself, such
as *ngIf and *ngFor.
In this case, I am using an attribute directive because I want to enhance a simple placeholder element and replace it with the required AdSense push script.
Directive Implementation (TypeScript)
Below is the simplified directive code. It listens for the
AfterViewInit lifecycle event and replaces the host element with
a script tag that triggers the AdSense ad render.
import { AfterViewInit, Directive, ElementRef, Inject } from '@angular/core';
import { DOCUMENT } from '@angular/common';
@Directive({
selector: '[appAd]',
standalone: true
})
export class AdDirective implements AfterViewInit {
constructor(
private el: ElementRef,
@Inject(DOCUMENT) private document: Document
) {}
ngAfterViewInit(): void {
const hostElement = this.el.nativeElement as HTMLElement;
if (!hostElement) return;
const script = this.document.createElement('script');
script.type = 'text/javascript';
script.id = 'unique-ad-script-id';
script.text = '(adsbygoogle = window.adsbygoogle || []).push({});';
const parent = hostElement.parentNode;
if (parent) {
parent.replaceChild(script, hostElement);
}
}
}
One important detail is ensuring that the script.id is unique. If
multiple ad units exist on the same page, duplicate IDs can cause unexpected
behavior. In my
previous guide, I explained how to
generate random identifiers safely.
Using the Directive in Your Template
The HTML integration remains very close to the official AdSense format. I keep
the <ins class="adsbygoogle"> element intact and only use
the directive on a simple placeholder div placed immediately after it.
<ins class="adsbygoogle"
style="display:block"
data-ad-client="YOUR_CLIENT_ID"
data-ad-slot="YOUR_AD_SLOT"
data-ad-format="auto"
data-full-width-responsive="true">
</ins>
<div appAd></div>
Include the AdSense loader in your app shell (usually in
<head>). This keeps your integration cleaner and avoids
re-adding the loader per component.
<script async
src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=YOUR_CLIENT_ID"
crossorigin="anonymous"></script>
This ensures that the global adsbygoogle object is available
before the directive attempts to push a new ad unit.
Why This Method Works Better in an Angular SPA
From my experience, this directive-based approach provides a more stable integration in a Single Page Application because it avoids repeated ad DOM mutations. I still get the reusability benefits of Angular, but the ad rendering itself happens in a way that feels closer to traditional server-rendered placement.
Here are the main improvements I noticed after switching to this pattern:
Policy alignment: I replace a static placeholder with the
official AdSense push script. That keeps the markup predictable and reduces
the risk of problematic ad injection patterns.
Cleaner templates: The directive keeps component HTML
readable. That is helpful for maintenance and also improves the text-to-HTML
ratio crawlers see.
Better performance control: Because I control where the push
happens, I can ensure I’m not triggering unnecessary reflows or duplicate
pushes.
In a Single Page Application, predictable rendering and minimal DOM mutation are key to maintaining both performance and advertising stability.
I also recommend reserving space for your ad units to reduce layout shifts. If
you allow the page to “jump” when the ad loads, you can hurt Core Web Vitals,
especially CLS. In practice, I usually give the
ins.adsbygoogle element a minimum height on common breakpoints so
the layout remains stable even before the ad finishes loading.
Common Issues I Avoid With This Pattern
When I used a component-driven injection method, I ran into edge cases where navigation would trigger ad reinitialization in ways that felt unpredictable. With the directive replacement approach, those issues are easier to control because each placement behaves like a “single shot” replacement after the view is initialized.
Quick checklist
1. Load the AdSense script once in your app shell.
2. Keep the
<ins class="adsbygoogle"> markup standard.
3. Use the directive only as a lightweight “push
trigger”.
4. Reserve space for the ad to prevent CLS issues.
If you want to go even further, you can pair this strategy with route-level ad placeholders so you don’t push ads in hidden tabs or collapsed UI sections. I usually only render the ad container when it is visible to the user, which keeps the experience clean and avoids wasted work.
Final Thoughts
After switching to this directive-based approach, my AdSense integration became easier to maintain and more predictable across navigation. The implementation stays close to Google’s standard placement model, while still fitting naturally into an Angular 18 SPA architecture.
If you run into issues such as ads not filling, blank containers, or inconsistent rendering, I recommend double-checking that your loader script is included only once, and that you are not triggering multiple pushes for the same ad slot during navigation.
If you have questions or you want me to review your current Angular setup, leave a comment and include your route structure and where you place your ad containers.