[object Object]

← back to CelebritySignatures

Pre-built reserved AdSense ad zones (gallery/article/game) — hidden until per-zone slot ids are set via env (ADSENSE_SLOT_GALLERY/ARTICLE/GAME), so zero visual impact pre-approval + zero CLS once live (reserved min-height). assets/ads.js activator + server config injection. Post-approval = paste 3 slot ids into prod .env, done (TK-10181)

cc24002df8ca4c7c05d061c95c9d14d5500fe745 · 2026-08-04 13:13:44 -0700 · Steve Abrams

Files touched

Diff

commit cc24002df8ca4c7c05d061c95c9d14d5500fe745
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Tue Aug 4 13:13:44 2026 -0700

    Pre-built reserved AdSense ad zones (gallery/article/game) — hidden until per-zone slot ids are set via env (ADSENSE_SLOT_GALLERY/ARTICLE/GAME), so zero visual impact pre-approval + zero CLS once live (reserved min-height). assets/ads.js activator + server config injection. Post-approval = paste 3 slot ids into prod .env, done (TK-10181)
---
 public/assets/ads.js | 41 +++++++++++++++++++++++++++++++++++++++++
 public/game.html     |  1 +
 public/index.html    |  1 +
 server.js            | 14 ++++++++++++--
 4 files changed, 55 insertions(+), 2 deletions(-)

diff --git a/public/assets/ads.js b/public/assets/ads.js
new file mode 100644
index 0000000..0548bd1
--- /dev/null
+++ b/public/assets/ads.js
@@ -0,0 +1,41 @@
+/* Reserved ad-zone activator for CelebritySignatures.
+ * Zones sit in the page as <div class="ad-zone" data-ad-zone="gallery|article|game">.
+ * They render NOTHING until their per-zone data-ad-slot id is configured server-side
+ * (window.__ADSLOTS, populated from env once the site is AdSense-approved). This keeps
+ * the pre-approval site clean (no empty gaps) and, once live, reserves height so the
+ * ad load causes zero layout shift (CLS). "Advertisement" label per AdSense policy. */
+(function () {
+  var pub = window.__ADPUB, slots = window.__ADSLOTS || {};
+  if (!pub) return;
+  // reserved minimum heights by zone (auto ads grow past this; the floor kills CLS)
+  var MINH = { gallery: 100, article: 250, game: 120 };
+  // inject scoped styles once
+  var css = document.createElement('style');
+  css.textContent =
+    '.ad-zone{display:none}' +
+    '.ad-zone.ad-ready{display:block;margin:26px auto;max-width:970px;padding:0 16px;text-align:center}' +
+    '.ad-zone.ad-ready>.ad-label{font:600 10px/1 -apple-system,sans-serif;letter-spacing:.12em;text-transform:uppercase;color:#b3ada3;margin-bottom:6px}' +
+    '.ad-zone.ad-ready>ins{display:block}';
+  document.head.appendChild(css);
+
+  function activate(zone) {
+    var key = zone.getAttribute('data-ad-zone');
+    var slot = slots[key];
+    if (!slot || !/^\d{6,}$/.test(slot)) return; // no real slot id yet → stay hidden
+    zone.style.minHeight = (MINH[key] || 100) + 'px';
+    var label = document.createElement('div'); label.className = 'ad-label'; label.textContent = 'Advertisement';
+    var ins = document.createElement('ins');
+    ins.className = 'adsbygoogle';
+    ins.style.display = 'block';
+    ins.setAttribute('data-ad-client', pub);
+    ins.setAttribute('data-ad-slot', slot);
+    ins.setAttribute('data-ad-format', 'auto');
+    ins.setAttribute('data-full-width-responsive', 'true');
+    zone.appendChild(label); zone.appendChild(ins);
+    zone.classList.add('ad-ready');
+    try { (window.adsbygoogle = window.adsbygoogle || []).push({}); } catch (e) { /* loader not ready yet */ }
+  }
+
+  function run() { Array.prototype.forEach.call(document.querySelectorAll('.ad-zone[data-ad-zone]'), activate); }
+  if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', run); else run();
+})();
diff --git a/public/game.html b/public/game.html
index 90d33e6..2468573 100644
--- a/public/game.html
+++ b/public/game.html
@@ -197,6 +197,7 @@
       <a class="sh-btn" id="shTweet" target="_blank" rel="noopener noreferrer">𝕏 Share</a>
     </div>
     <button class="again" id="againBtn">Play again</button>
+    <div class="ad-zone" data-ad-zone="game"></div>
   </div>
 </main>
 <script>
diff --git a/public/index.html b/public/index.html
index 0951a93..163273b 100644
--- a/public/index.html
+++ b/public/index.html
@@ -350,6 +350,7 @@
     <h2>The Collection <span id="count"></span></h2>
     <button class="filters-hint" id="filtersHint">☰ Categories, sort &amp; views</button>
   </div>
+  <div class="ad-zone" data-ad-zone="gallery"></div>
   <div id="gridView"><div class="grid" id="grid"></div><div class="empty" id="empty" hidden>No signatures match these filters.</div></div>
   <div id="collagesView" class="collages" hidden></div>
 </main>
diff --git a/server.js b/server.js
index b28c4c6..fc3d9f2 100644
--- a/server.js
+++ b/server.js
@@ -81,9 +81,18 @@ const ADSENSE_PUB_ID = (envVal('ADSENSE_PUB_ID') || '').trim().replace(/^ca-/, '
 const ADSENSE_TAG = /^pub-\d{10,}$/.test(ADSENSE_PUB_ID)
   ? `<script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-${ADSENSE_PUB_ID}" crossorigin="anonymous"></script>`
   : '';
+// Hand-placed reserved ad ZONES (better fill than Auto ads alone on a dense
+// grid). Each zone stays hidden — zero layout impact — until its per-zone slot
+// id is set via env (post-approval: create the units in AdSense → paste the
+// data-ad-slot numbers here). assets/ads.js reads this config and activates
+// only the zones that have a real slot id, reserving height to avoid CLS.
+const AD_SLOTS = { gallery: (envVal('ADSENSE_SLOT_GALLERY') || '').trim(), article: (envVal('ADSENSE_SLOT_ARTICLE') || '').trim(), game: (envVal('ADSENSE_SLOT_GAME') || '').trim() };
+const ADSENSE_CONFIG = ADSENSE_TAG
+  ? `<script>window.__ADPUB=${JSON.stringify('ca-' + ADSENSE_PUB_ID)};window.__ADSLOTS=${JSON.stringify(AD_SLOTS)};</script><script src="/assets/ads.js" defer></script>`
+  : '';
 const injectAds = html => (ADSENSE_TAG && typeof html === 'string' && html.includes('</head>') && !html.includes('adsbygoogle.js'))
-  ? html.replace('</head>', ADSENSE_TAG + '\n</head>') : html;
-if (ADSENSE_TAG) console.log(`AdSense Auto ads active (${ADSENSE_PUB_ID})`);
+  ? html.replace('</head>', ADSENSE_TAG + ADSENSE_CONFIG + '\n</head>') : html;
+if (ADSENSE_TAG) { const live = Object.entries(AD_SLOTS).filter(([, v]) => v).map(([k]) => k); console.log(`AdSense active (${ADSENSE_PUB_ID}) — reserved zones live: ${live.length ? live.join(', ') : 'none yet (awaiting slot ids)'}`); }
 // Atomic in-process guard against concurrent double-credit for the same paid session
 // (claimed synchronously before any await; the download-ledger is the restart-surviving backstop).
 const USED_SIDS = new Set();
@@ -578,6 +587,7 @@ h1{font:400 32px/1.2 'Playfair Display',serif;margin:6px 0}
 <div class="meta">${esc(life)} · ${esc(r.category)}${why ? ' · ' + esc(why) : ''}</div>
 ${museums.length ? `<div class="sec">In the collections of</div><div class="meta">${esc(museums.join(' · '))}</div>` : ''}
 <div class="sec">License &amp; source</div><div class="meta">${esc(r.image_license || '')} · <a href="${esc(r.wikidata)}" rel="nofollow noopener" target="_blank">Wikidata</a></div>
+<div class="ad-zone" data-ad-zone="article"></div>
 <p style="margin-top:22px"><a href="/?artist=${am[1]}">See ${esc(r.full_name)} in the full gallery →</a></p>
 <a class="cta" href="/murals?name=${encodeURIComponent(r.full_name)}">Order a mural featuring this signature →</a>
 <p style="margin-top:26px"><a href="/">← Browse all ${(await mergedSignatures()).length.toLocaleString()} signatures</a></p>

← 87f0e4a Accept rk_live_ restricted keys for the live Stripe key (lea  ·  back to CelebritySignatures  ·  auto-save: 2026-08-04T13:33:48 (1 files) — public/assets/ads b4ed90d →