In this article, you'll learn how to add advertisements (ads) to your Higher Logic Vanilla (Vanilla) with Custom HTML widgets and Widget Builder.
Overview
You can add ads to your community in several ways. The best method depends on where the ad should display, whether the ad provider needs to find a container in the main page DOM, and whether the provider requires site-wide scripts or page-view handling.
In general, use:
- Custom HTML widgets for simple, self-contained ad placements, such as sponsor banners, iframe embeds, affiliate blocks, or static promotional content.
- Widget Builder for reusable ad components that need unique container IDs, configurable settings, controlled script loading, or cleanup when the page changes.
- Style Guide JavaScript tab for site-wide ad behavior, such as loading an external provider script, initializing global ad logic, or responding to page navigation.
- Content Security Domains and, when needed, enable the Allow Third-Party Scripts setting to permit trusted ad-provider scripts to load.
Choose the right method
Method | Best for | Strengths | Watch-outs |
|---|
Custom HTML widget | Simple sponsor blocks, iframe ads, image ads, affiliate links, lightweight embeds | Easy to place in Layout Editor; simple for community managers to reposition; supports the Advertisement setting | HTML is isolated inside Shadow DOM; scripts in the HTML tab are ignored; many ad-provider scripts will not work if they need access to a top-level page container |
Widget Builder | Programmatic ad placements, Google Ad Manager slots, reusable widgets, multi-instance ads | Renders in the normal page DOM, not inside Shadow DOM; supports lifecycle control, unique IDs, configuration, cleanup, and reusable layouts | Requires developer access and testing; ad scripts still need CSP allowlisting |
Style Guide JavaScript tab | Site-wide script loading, ad initialization, SPA navigation hooks, page-view refresh logic | Runs in the main document context; can affect the whole page | Write JavaScript directly; do not paste <script> tags here |
Theme-level Scripts asset, where available | External provider libraries that must load globally | Loads remote script URLs as theme assets | Usually requires theme/developer access; not a placement location |
Style Guide Header/Footer | Global display HTML, such as branded headers or promotional markup | Good for static global HTML | Not for scripts; Header/Footer content is self-contained and should not be used to load ad libraries |
Two platform behaviors to understand first
Shadow DOM isolation
Custom HTML widgets are self-contained. Their HTML and CSS are isolated from the rest of the page. This is helpful for stability, but it matters for ads because many ad providers expect to find a placement by calling something like:
document.getElementById("my-ad-slot");
If that container is inside the Custom HTML widget’s Shadow DOM, the provider script may not be able to find it from the main page context.
This means:
- A simple iframe, image, affiliate link, or sponsor block can work well in a Custom HTML widget.
- A script that expects to find or manage a top-level page container is usually better implemented with Widget Builder.
<script> tags placed directly in the Custom HTML widget HTML tab are ignored.- If Widget Builder is unavailable, the Custom HTML widget JavaScript tab can create a container in the main document as an advanced workaround.
Vanilla page navigation
Vanilla provides window.onPageView() on modern Vanilla pages. This hook fires on every page view, including in-app navigation, and is the primary tool for SPA-aware ad refresh or page-view tracking.
This matters because some community navigation updates the page without a full browser reload. Code that runs only once on the initial load may not automatically re-run when a user navigates to another discussion, category, or custom layout.
For ad integrations, this can cause:
- Ads that load on refresh but disappear after in-app navigation.
- Duplicate ads if initialization runs repeatedly without cleanup.
- Empty slots if the ad provider runs before the placement container exists.
Use:
window.onVanillaReady() for one-time initialization after Vanilla is ready.window.onPageView() for logic that should run on every page view, including in-app navigation.
Example:
window.onVanillaReady(function (vanilla) {
// One-time setup after Vanilla is ready.
console.log("Vanilla is ready.");
});
window.onPageView(function (vanilla) {
// Runs on the initial page view and subsequent in-app page views.
console.log("Page view:", window.location.pathname);
});
TIP: When onPageView() fires, the new page’s React content may still be mounting. If your ad logic targets a container rendered by Widget Builder, use the provider’s queueing mechanism, a short delay, or a container-existence check before displaying or refreshing the ad.
Before you begin
Before adding ad code, collect the following from your ad provider or ad operations team:
- Publisher ID, network code, placement ID, ad unit path, zone ID, or affiliate tracking ID.
- Supported ad sizes, such as
300x250, 728x90, 320x50, or responsive. - Whether the provider requires a global script.
- Whether the provider requires a container with a specific ID.
- Whether the provider requires a render call, such as
display(), showTag(), register(), or push({}). - Whether the provider dynamically loads additional scripts.
- Whether the provider requires an
ads.txt file. - Which domains must be allowlisted.
- Consent, privacy, and regional compliance requirements.
Also confirm:
- Whether the production domain is approved by the ad provider.
- Whether the staging domain can serve ads.
- Whether the ad provider can serve on authenticated or private pages.
- Whether the implementation must support an ad-free user group or membership tier.
Consent and privacy
Ad implementations may involve cookies, tracking pixels, device identifiers, personalization, and third-party data sharing. If your community serves users in regulated regions, involve your legal, privacy, or InfoSec team before launch.
A typical consent-aware flow is:
- Load the consent management platform first.
- Check consent before calling the ad provider’s display or refresh function.
- If consent has not been granted yet, wait for the CMP’s consent event.
- Initialize or refresh ads only after the required consent state is available.
The exact implementation depends on your CMP and provider. Your ad provider’s documentation should specify which consent signals are required.
Security and allowlisting
Ad providers often load scripts, iframes, images, tracking pixels, click redirects, and additional child scripts from multiple domains. Vanilla security settings determine what can load.
Content Security Domains
Use Content Security Domains for external JavaScript domains. If an ad-provider script is blocked by CSP, adding the domain to Trusted Domains alone is not enough.
Typical examples include domains such as:
securepubads.g.doubleclick.net
pagead2.googlesyndication.com
servedbyadbutler.com
adsdk.microsoft.com
IMPORTANT: Use the exact domains required by your provider. Avoid broad wildcards unless your security team approves them.
Trusted Domains
Use Trusted Domains for domains your community considers safe for trusted external content, embed behavior, or related external-domain handling.
Do not confuse this with Content Security Domains. If the blocked resource is JavaScript, the relevant setting is Content Security Domains.
Allow Third-Party Script Execution
Some providers load additional scripts dynamically after the first script is trusted. Vanilla’s Allow Third-Party Script Execution setting adds strict-dynamic to the CSP policy, which can help with services such as AdSense or Google Tag Manager.
This setting increases security responsibility, so test without it first and consult InfoSec or Vanilla Support if needed.
Recommended workflow:
- Add only the domains visible in the provider’s snippet.
- Test in an incognito/private browser window.
- Check DevTools → Console for CSP violations.
- Add only the domains that are actually blocked and required.
- Enable Allow Third-Party Script Execution only if the provider’s dynamically loaded child scripts are still blocked after allowlisting the known domains.
Custom HTML widgets
Use a Custom HTML widget in the Layout Editor for simple, self-contained placements.
Good examples include:
- Sponsor banners.
- Static image ads.
- Affiliate links.
- Self-contained iframes.
- Promotional HTML that does not need a global provider script to find a container by ID.
What is available in the JavaScript tab?
The Custom HTML widget JavaScript tab runs in the main document context, not inside the Shadow DOM. The code is wrapped so it does not leak variables globally.
Two useful variables are available:
customHtmlRoot — the Shadow DOM host element for this widget.vanilla — a Vanilla helper object with utilities such as:vanilla.apiv2vanilla.translate()vanilla.getCurrentUser()vanilla.currentUserHasPermission()vanilla.getCurrentLocale()
To access content from the widget’s HTML tab, use:
const element = customHtmlRoot.shadowRoot.querySelector(".my-element");
This is useful for reading or updating your own widget content. However, remember that many third-party ad providers still cannot find containers inside the Shadow DOM using standard document-level selectors.
Option 1: Add a self-contained iframe ad with a Custom HTML widget
Use this pattern when the provider gives you an iframe or when you are displaying sponsor content that does not need a provider script.
HTML tab
<aside class="vanillaAd" aria-label="Advertisement">
<div class="vanillaAd-label">Advertisement</div>
<iframe
src="https://sponsor.example.com/embed/12345"
width="728"
height="90"
title="Sponsor advertisement"
loading="lazy"
style="max-width: 100%; border: 0;"
></iframe>
</aside>
CSS tab
.vanillaAd {
box-sizing: border-box;
width: 100%;
min-height: 90px;
padding: 10px;
text-align: center;
}
.vanillaAd-label {
margin-bottom: 6px;
font-size: 12px;
line-height: 1.4;
color: #6b7280;
text-transform: uppercase;
letter-spacing: 0.04em;
}
JavaScript tab
No JavaScript is needed for this pattern.
TIP: If the Custom HTML widget has an Advertisement setting, enable it for ad placements so users with ad-free permissions do not see the widget.
Option 2: Use a Custom HTML widget JavaScript tab to create a main-document container
Use this as an advanced workaround when Widget Builder is unavailable and the provider requires a container in the main document.
This pattern creates the ad container next to the Custom HTML widget host, rather than inside the widget’s Shadow DOM.
HTML tab
<aside class="vanillaAd" aria-label="Advertisement">
<div class="vanillaAd-label">Advertisement</div>
<div class="vanillaAd-placeholder">
Advertisement loading…
</div>
</aside>
CSS tab
.vanillaAd {
box-sizing: border-box;
width: 100%;
min-height: 90px;
padding: 10px;
text-align: center;
}
.vanillaAd-label {
margin-bottom: 6px;
font-size: 12px;
line-height: 1.4;
color: #6b7280;
text-transform: uppercase;
letter-spacing: 0.04em;
}
.vanillaAd-placeholder {
min-height: 90px;
display: grid;
place-items: center;
color: #6b7280;
font-size: 13px;}
JavaScript tab
(function () {
const slotID = "vanilla-ad-slot-" + Math.random().toString(36).slice(2);
const mainDocumentContainer = document.createElement("div");
mainDocumentContainer.id = slotID;
mainDocumentContainer.setAttribute("aria-label", "Advertisement");
mainDocumentContainer.style.minHeight = "90px";
mainDocumentContainer.style.textAlign = "center";
// Insert the real ad container after the Custom HTML widget host.
// This puts it in the normal page DOM, not inside the widget Shadow DOM.
customHtmlRoot.insertAdjacentElement("afterend", mainDocumentContainer);
// Hide the visual placeholder inside the Shadow DOM after creating the real container.
const placeholder = customHtmlRoot.shadowRoot.querySelector(".vanillaAd-placeholder");
if (placeholder) {
placeholder.textContent = "";
placeholder.style.display = "none";
}
// Replace this with your provider's actual display/render call.
// Example:
// window.exampleAdProvider.display(slotID);
if (window.exampleAdProvider && typeof window.exampleAdProvider.display === "function") {
window.exampleAdProvider.display(slotID); }
}
)();
This workaround is useful, but it should be tested carefully. If the provider needs lifecycle cleanup, repeated page-view refresh, or multiple unique placements, Widget Builder is usually more maintainable.
Widget Builder
Use Widget Builder when the provider needs a visible page container, unique slot IDs, lifecycle cleanup, or configurable options.
Widget Builder components render in the normal page DOM, not inside a Shadow DOM. This is a key advantage for ad integrations because ad providers can find containers using standard document-level selectors such as document.getElementById().
Widget Builder is recommended for:
- Google Ad Manager / Google Publisher Tag placements.
- Microsoft Advertising / Xandr AST placements.
- Ad server zones that need unique placement IDs.
- Multiple ad slots on the same page.
- Configurable ad unit paths, sizes, labels, and min-height values.
- Cleanup when the widget unmounts.
A typical Widget Builder ad component should:
- Render a container with a unique ID.
- Load or wait for the provider script.
- Define the slot after the container exists.
- Reserve space with
min-height. - Destroy or clean up the slot if the provider supports it.
- Provide an editor-only placeholder for admins if the widget has no visible live content until configured.
Suggested Widget Builder custom options:
{
"adUnitPath": "Ad unit path or zone ID",
"slotID": "Unique slot/container ID",
"sizes": "Allowed ad sizes",
"label": "Advertisement label",
"minHeight": "Reserved height to reduce layout shift",
"showEditorPlaceholder": "Show helper text to admins when not configured"
}
Style Guide JavaScript tab
Use the Style Guide JavaScript tab for logic that should run across the community. This includes loading external libraries programmatically, initializing site-wide ad settings, setting page-level targeting, and responding to page navigation.
Do not paste this:
<script src="https://example.com/ad-script.js"></script>
Instead, write JavaScript that creates a script element:
function loadExternalScriptOnce(src, id) {
if (id && document.getElementById(id)) {
return;
}
const script = document.createElement("script");
script.src = src;
script.async = true;
if (id) {
script.id = id;
}
document.head.appendChild(script);
}
window.onVanillaReady(function () {
loadExternalScriptOnce(
"https://securepubads.g.doubleclick.net/tag/js/gpt.js",
"vanilla-gpt-script"
);
});
Use onPageView() for navigation-aware behavior:
let lastAdRefreshTime = 0;
const minimumRefreshInterval = 30000; // 30 seconds.
window.onPageView(function () {
const now = Date.now();
if (now - lastAdRefreshTime < minimumRefreshInterval) {
return;
}
lastAdRefreshTime = now;
// Replace this with provider-approved refresh logic.
if (window.googletag && window.googletag.cmd) {
window.googletag.cmd.push(function () {
// Example only:
// window.googletag.pubads().refresh();
});
}
});
TIP: Confirm refresh rules with your ad provider. Some networks restrict how frequently ads can refresh or require refreshed inventory to be declared.
Common provider patterns
Google AdSense
AdSense commonly uses two patterns:
- Auto ads, where the AdSense code is placed across the site and Google chooses placements.
- Manual ad units, where you place ad unit code where the ad should appear.
For Vanilla, manual ad units are usually more predictable than Auto ads because you control where the placement appears. If you use Auto ads, test full page refreshes, in-app navigation, signed-in views, signed-out views, desktop, and mobile.
AdSense and Google Tag Manager may dynamically load child scripts. If CSP blocks those scripts after you have added the known domains to Content Security Domains, review whether Allow Third-Party Script Execution is required.
Google Ad Manager / Google Publisher Tag
Google Publisher Tag is commonly used with Google Ad Manager. The standard pattern is:
- Load
gpt.js. - Create
window.googletag = window.googletag || { cmd: [] }. - Define a slot with
googletag.defineSlot(). - Add the slot to
googletag.pubads(). - Enable services.
- Display the matching container ID.
This pattern is a strong fit for Widget Builder because the ad container can be rendered in the normal DOM with a unique ID.
Important GPT notes:
- The container ID in the widget must match the ID used in
defineSlot(). - Each slot ID must be unique on the page.
- Use
googletag.cmd.push() so slot logic waits until GPT is ready. - Do not call refresh too frequently.
- If using consent management, separate slot registration from ad content loading so you can wait for the correct consent state.
Microsoft Advertising / Xandr Seller Tag
Microsoft Advertising’s Seller Tag, also known as AST, is a JavaScript SDK used to manage ad placements.
The typical AST pattern is:
- Load the AST library.
- Set page options.
- Define tags with placement IDs, sizes, and target IDs.
- Load the tags.
- Show the matching tag in the page body.
Use Widget Builder when AST needs to manage specific placement containers on the page.
AdButler
AdButler uses zone tags. A zone tag is a snippet placed on a publisher site that tells AdButler to serve an ad for a specific placement.
AdButler supports multiple zone tag types. For modern Vanilla pages, prefer provider-recommended asynchronous tags and test the exact tag type. Avoid legacy patterns that rely on document.write() where possible.
Amazon Associates Native Shopping Ads
Amazon Native Shopping Ads are affiliate-style units rather than a traditional ad server. They can be useful for product-focused communities.
Use this pattern when:
- The community content is product-focused.
- Affiliate disclosure requirements are understood.
- You are comfortable with Amazon Associates policy and tracking behavior.
- You have tested the placement in authenticated and unauthenticated views.
Common pitfalls and fixes
Implementation checklist
Use this checklist for each ad implementation.
- Determine whether the ad is self-contained, provider-managed, or site-wide.
- Choose Custom HTML widget, Widget Builder, Style Guide JavaScript, or a combination.
- Confirm the production domain is approved by the provider.
- Confirm whether staging can serve ads.
- Confirm whether
ads.txt is required or recommended. - Confirm consent and privacy requirements.
- Add required script domains to Content Security Domains.
- Add required embed or iframe domains to Trusted Domains where applicable.
- Test without Allow Third-Party Script Execution first.
- Enable Allow Third-Party Script Execution only if dynamic child scripts are blocked and the risk has been reviewed.
- Use unique IDs for every ad container.
- Reserve height for each placement to prevent layout shift.
- Use Widget Builder for providers that need top-level DOM containers.
- Use the Style Guide JavaScript tab for site-wide script loading and page-view logic.
- Do not put
<script> tags in the Style Guide Header, Style Guide JavaScript tab, or Custom HTML widget HTML tab. - Use
window.onPageView() for SPA-aware ad refresh or page-view logic. - Test with hard refresh and in-app navigation.
- Test signed-in and signed-out views.
- Test desktop and mobile layouts.
- Check DevTools Console for CSP errors.
- Hand off day-to-day placement changes to the community manager after the technical setup is stable.
Quick reference
What you are adding | Where to put it |
|---|
Static sponsor HTML, image, or iframe | Custom HTML widget |
Custom HTML widget styling | Custom HTML widget CSS tab |
JavaScript for a Custom HTML widget | Custom HTML widget JS tab |
Main-document ad container without Widget Builder | Custom HTML widget JS tab, advanced workaround |
Provider-managed ad container | Widget Builder |
Reusable configurable ad placement | Widget Builder |
Global ad library loading | Style Guide JavaScript tab, or theme-level Scripts asset if available |
Site-wide ad initialization | Style Guide JavaScript tab |
SPA page-view handling | Style Guide JavaScript tab with window.onPageView() |
Global display HTML | Style Guide Header/Footer |
Script provider domains | Content Security Domains |
Trusted embed/link domains | Trusted Domains |