Categories
Web analytics

Do you use the word “ads” in your code? Get ready for an invisible website or part of it!

You might think the ad- prefix in CSS is harmless. It’s just a couple of letters, clear enough for even a junior dev to spot. But those few letters can turn your frontend into a totally blank page.
Even worse? You’ll only hear about the bug from a furious client. In production. Late Friday afternoon. With GA4 quiet as a mouse because not a single pixel fired.

So if you want to keep your UX (and your sanity) intact, keep reading. Today I’ll show you why ad-blockers hunt anything that smells like advertising, where we most often get burned, and how to avoid it, no gimmicky “anti-ad-block” scripts required.

Audio version of the article

Recording length: 18 minutes

Why does this matter?

EasyList the ammo belt behind most ad-blockers packs thousands of rules that fire before the first pixel of your layout lights up. Buried in there are innocent-looking lines like:

##[id^="ad-"] 
##[class*=" ads-"]

Note: ## is ad-block syntax; the regular CSS selector is the part without it.
This regex machete hides every element whose id starts with ad- or whose class contains a stand-alone “ads”.

Network filters work the same way, killing any request to URLs like /ads/, /banner/, or the sub-domain ads.example.com (easylist.to). The file never loads, your JavaScript throws an error, and you’re left wondering why the component is “only broken for some users.”

Heads-up: It’s not just IDs and classes

Blockers now run “cosmetic” filters on text content too :-abp-contains() will hide <span>Advertisement</span> purely by its inner text (help.adblockplus.org). Simple? Not even close.

How do blockers hunt your selectors?

  1. CSS injection – the extension’s manifest injects a stylesheet with global selectors like the ones above.
  2. URL rewrite & cancel – if the request matches a filter, it dies at the network layer.
  3. Scriptlet hijacking – uBlock Origin can inject JS that selectively nukes or tweaks the DOM.

Watch out, this is where folks slip up most: If the element isn’t in the DOM yet (lazy render) and the ad-blocker injects first, it gets hidden the moment it appears. Your layout re-flows, and your pixel-perfect grid is toast.

Real-world examples

  1. A SaaS project for SMBs had a button labeled “Add-ons” (id="addons_btn"). Harmless, right? They “improved” it by adding class="ads-variant", conversions dropped 32 %. Why? Users with uBlock never saw the button. Debugging that is a nightmare.
  2. I’ve seen server-side GTM endpoints placed on sub-domains named gtm, analytics, etc. and getting blocked even harder than the default installer. Use random, innocuous names like “bread”, “lightbulb”, or “car” anything not linked to analytics or ads.
  3. I know a project that stuffed the word “analytics” into its main domain. Clicking any link to that domain threw up a warning. Not great. The project eventually died.

Where do we mess up most often?

  • Folder structure: /static/**ads**/banner.png
  • Hashed IDs: gtm-ad-1234
  • WordPress themes: theme-adwise, widget_banner
  • Alt and title: alt="Advertisement"
  • Data attributes: data-ad-slot="header"
  • Sub-domain: ads.mydomain.cz

And always the same story: “Works on my machine, 30 % of prod traffic is broken.”

How do you get out of this mess?

1. Name it something else

Use project-specific aliases: instead of ad-slot go with slotHero or slotSidemenu. Inside a word is fine (he*ad*er, ro*ad*map) filters usually need a word boundary.

2. Hash your asset paths

If the CDN forces /img/ads/…, add a reverse-proxy rewrite to /img/a1b2c3/…. One Nginx rule and you’re golden.
From an analytics guy: Detecting ad-blockers used to be as easy as loading a file named “ads.js”, if it didn’t load you knew the user was blocking ads.

3. Separate semantics from layout

If compliance says you need a visible “Sponsored” label, give the element two classes:

<span class="slotA1 sponsorLabel">Sponsored</span> 

When styling, assume sponsorLabel might disappear, let the parent handle size and padding.

4. Detect & fallback

const slot = document.querySelector('#slotHero'); if (!slot || slot.offsetHeight === 0) { console.warn('Ad-block? Switching to fallback.'); renderFallback(slot); } 

No fancy API needed plain DOM does the job.

Start now, future you will thank you.

Pre-release checklist

  1. Name audit – scan build artifacts (grep -R --line-number -E '\\b(ad|banner|promo|sponsor)\\b' dist/).
  2. Local test – install both uBlock Origin and AdGuard; their feeds differ.
  3. Run Lighthouse + ad-block – watch for new JS errors.
  4. Check GA4 hits – still tracking with a blocker on? If not, consider server-side GTM.
  5. E2E test in CI – Cypress + uBlock in headless Chrome.
  6. Monitor broken layout – simple visual-diff screenshot test.
  7. Log fallbacks – when renderFallback fires, push an event to Sentry.
  8. Set alerts – 5 % fallback spike ⇒ Slack ping.
  9. Re-audit on every major frontend refactor.
  10. Document it – so the next dev knows why slotHero exists instead of adHeader.

Personal tips that saved my career

“Name it ugly first, then beautify”

When I draft component IDs, I deliberately slap in an ugly placeholder like XXX_NOT_AD. I only refactor once I see the layout on a dev server. Stops me from reflexively typing “adBox”.

“Mock the ad-blocker at the proxy”

Need to debug without the extension? Have your reverse-proxy drop /ads/ requests. Faster iteration, same result.

“Grep the diff constantly”

Pre-commit hook that rejects commits containing \bads?\b in new files will save you cold sweats later.

Summary

  • Ad-blockers don’t read intent, only regex.
  • Keywords like ad, ads, banner, promo in ID, class, URL, or file names ⇒ element vanishes.
  • One hidden wrapper can nuke your whole layout.
  • The fix is simple: rename and fallback.
  • Automate detection or you’ll forget.

What should you do after reading?

  • Audit your own site right now.
  • Add this rule to your dev docs.

How about you? Got your own horror story with id="adBanner", or did you survive a launch unscathed? Share on social media so next time we’re laughing together instead of crying alone.

Short version I dropped into our docs:

Avoid “ad”, “ads”, “advert”, “banner”… in identifiers and paths

Applies to: id, class, data-* attributes, file & folder names, sub-domains, URL params

Rule

  • Never use those keywords or any shortened/extended variant (ad, ads, adv, advert, banner, promo, sponsor…)
    • …when they’re at the start or after a delimiter (-, _, /, .).
  • Allowed if the string sits inside another word (header, roadmap).

Why

  1. Ad-blockers (uBlock Origin, AdBlock Plus, AdGuard…) run off lists like EasyList.
    • They include global CSS filters that hide elements via selectors like ##[id^="ad-"], ##.ads-banner stackoverflow.com
    • And network filters that kill requests hitting /ads/, /banner/, or domains like ads.example.com adblockplus.org
  2. When a rule hits, the element gets display:none or the request never fires → layout breaks, code fails with zero clues.
  3. Incidents with IDs like ad_holder or themes named “adwise” show how common these silent bugs are.

Consequences of breaking the rule

  • Invisible components (buttons, modals, icons…)
  • Missing JS/CSS files ⇒ JS errors, broken design
  • Hard to debug, many vendors & QA testers don’t run ad-block

Exceptions

Where it’s safeWhy
Visible text (“Advertisement”, “Sponsored”)Filters only apply cosmetic styles; don’t rely on its dimensions
HTML/JS comments, internal variablesBlockers don’t parse them

Examples

❌ Wrong✅ Right
<div id="ad_holder"><div id="slotHero">
/static/ads/logo.svg/static/assets/logo.svg
class="banner_top"class="heroTop"

Following this rule keeps your elements visible and saves hours of chasing “mysteriously disappearing” components.

.