[object Object]

← back to Dw Domain Fleet

monetize mode: AdSense Auto Ads + for-sale banner + content site + ads.txt/privacy for parked SEO domains (TK Task#2)

197eeadcacac3f67d201601865410e816e2446a5 · 2026-09-09 10:08:43 -0700 · Steve Abrams

Adds cfg.monetize path: standalone topical content site (nav + hero + article +
guide + Privacy) with AdSense Auto Ads (pub-5278231299883833), /ads.txt, and a
greendomainbrokers for-sale banner. Strips the DW catalog/buy funnel on these
sites. Funnel sites unchanged. CSP widened for Google ad hosts only when monetize.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S81Y5KkGKrUHg2mwWQtD96

Files touched

Diff

commit 197eeadcacac3f67d201601865410e816e2446a5
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Wed Sep 9 10:08:43 2026 -0700

    monetize mode: AdSense Auto Ads + for-sale banner + content site + ads.txt/privacy for parked SEO domains (TK Task#2)
    
    Adds cfg.monetize path: standalone topical content site (nav + hero + article +
    guide + Privacy) with AdSense Auto Ads (pub-5278231299883833), /ads.txt, and a
    greendomainbrokers for-sale banner. Strips the DW catalog/buy funnel on these
    sites. Funnel sites unchanged. CSP widened for Google ad hosts only when monetize.
    
    Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_01S81Y5KkGKrUHg2mwWQtD96
---
 server.js        |  44 ++++++++++-
 shared/render.js | 230 ++++++++++++++++++++++++++++++++++++++++++++++++++++++-
 2 files changed, 269 insertions(+), 5 deletions(-)

diff --git a/server.js b/server.js
index f0caf0d..345521e 100644
--- a/server.js
+++ b/server.js
@@ -88,19 +88,37 @@ const app = express();
 // trust proxy = 1 trusts exactly the first hop (nginx), so req.ip is the real client.
 app.set('trust proxy', 1);
 app.disable('x-powered-by');
+// AdSense Auto Ads pull scripts, frames, images and beacons from Google's ad
+// hosts — a monetize site must widen the CSP for those or every ad is blocked
+// (and AdSense review fails). Non-monetize funnel sites keep the tight CSP.
+const MONETIZE = cfg.monetize === true || cfg.adsense === true;
+const AD_SCRIPT = MONETIZE ? [
+  'https://pagead2.googlesyndication.com', 'https://*.googlesyndication.com',
+  'https://partner.googleadservices.com', 'https://tpc.googlesyndication.com',
+  'https://www.google.com', 'https://adservice.google.com'
+] : [];
+const AD_FRAME = MONETIZE ? [
+  'https://googleads.g.doubleclick.net', 'https://tpc.googlesyndication.com',
+  'https://www.google.com'
+] : [];
+const AD_CONNECT = MONETIZE ? [
+  'https://pagead2.googlesyndication.com', 'https://*.googlesyndication.com',
+  'https://googleads.g.doubleclick.net', 'https://*.g.doubleclick.net',
+  'https://www.google.com'
+] : [];
 app.use(helmet({
   contentSecurityPolicy: {
     directives: {
       defaultSrc: ["'self'"],
       // 'unsafe-inline' required by GTM and inline theme/promo scripts.
       // www.googletagmanager.com serves gtm.js + gtag/js (GA4 + GTM).
-      scriptSrc: ["'self'", "'unsafe-inline'", 'https://www.googletagmanager.com'],
+      scriptSrc: ["'self'", "'unsafe-inline'", 'https://www.googletagmanager.com', ...AD_SCRIPT],
       styleSrc: ["'self'", "'unsafe-inline'", 'https://fonts.googleapis.com'],
       imgSrc: ["'self'", 'data:', 'https:'],
       fontSrc: ["'self'", 'https://fonts.gstatic.com', 'data:'],
-      connectSrc: ["'self'", 'https://www.google-analytics.com', 'https://analytics.google.com'],
+      connectSrc: ["'self'", 'https://www.google-analytics.com', 'https://analytics.google.com', ...AD_CONNECT],
       // GTM noscript <iframe src="https://www.googletagmanager.com/ns.html?id=...">
-      frameSrc: ['https://www.googletagmanager.com'],
+      frameSrc: ['https://www.googletagmanager.com', ...AD_FRAME],
       frameAncestors: ["'none'"], objectSrc: ["'none'"], baseUri: ["'self'"],
       formAction: ["'self'"], upgradeInsecureRequests: []
     }
@@ -116,6 +134,26 @@ app.use(rateLimit({ windowMs: 60000, max: 240, standardHeaders: 'draft-7', legac
 
 const PER_PAGE = 60;
 
+// ---- MONETIZE MODE (parked / for-sale SEO domain) ----------------------------
+// A cfg.monetize site is a standalone content site (no DW catalog / buy funnel):
+// topical home + About + Privacy + AdSense Auto Ads + ads.txt + for-sale banner.
+// We register only these routes and stop (top-level return skips the funnel
+// routes below). The catalog pool above builds but is never served here.
+if (MONETIZE) {
+  app.get('/ads.txt', (req, res) => res.type('text/plain')
+    .send('google.com, pub-5278231299883833, DIRECT, f08c47fec0942fa0\n'));
+  app.get('/', (req, res) => res.type('html').send(render.monetizeHome(cfg)));
+  app.get('/about', (req, res) => res.type('html').send(render.monetizeAbout(cfg)));
+  app.get('/privacy', (req, res) => res.type('html').send(render.privacyPage(cfg)));
+  app.get('/health', (req, res) =>
+    res.json({ ok: true, site: SITE, domain: cfg.domain, mode: 'monetize' }));
+  // No DW catalog on a for-sale domain — legacy funnel paths 301 home.
+  app.get(['/catalog', '/info', '/product/:handle', '/buy/:slug'], (req, res) => res.redirect(301, '/'));
+  app.use((req, res) => res.status(404).type('html').send(render.monetizeAbout(cfg)));
+  app.listen(PORT, () => console.log(`[${SITE}] ${cfg.domain} live on :${PORT} (monetize)`));
+  return;
+}
+
 app.get('/', (req, res) => {
   res.type('html').send(render.homePage(cfg, HERO_IMGS, FEATURED));
 });
diff --git a/shared/render.js b/shared/render.js
index 644f695..41adb55 100644
--- a/shared/render.js
+++ b/shared/render.js
@@ -500,7 +500,7 @@ function head(cfg, title, desc, path, canonicalOverride, bodyClass) {
 <meta property="og:url" content="${esc(url)}">
 <meta name="twitter:card" content="summary_large_image">
 <meta name="twitter:title" content="${esc(title)}"><meta name="twitter:description" content="${esc(desc)}">
-${gtagSnippet(cfg.ga4)}${gtmHeadSnippet(cfg.gtm)}<style>:root{--top-fg:#fff}</style>
+${gtagSnippet(cfg.ga4)}${gtmHeadSnippet(cfg.gtm)}${adsenseHead(cfg)}<style>:root{--top-fg:#fff}</style>
 <script>(function(){var s=localStorage.getItem('${cfg.slug}_theme');if(s)document.documentElement.dataset.theme=s;})();</script>
 <style>${css(cfg)}</style>
 </head><body${bodyClass ? ` class="${esc(bodyClass)}"` : ''}>${gtmNoscript(cfg.gtm)}`;
@@ -1014,4 +1014,230 @@ ${script(cfg)}
 </body></html>`;
 }
 
-module.exports = { homePage, catalogPage, aboutPage, infoPage, productPage, clean, esc, productSlug, scrubVendor, imgToken, proxyImg };
+/* ============================================================================
+ * MONETIZE MODE  (Task #2 — parked / for-sale SEO domains only)
+ * A site with cfg.monetize === true is NOT a DW funnel. It renders a standalone,
+ * genuinely-written single-topic content site (nav + hero + article + guide +
+ * Privacy) so it passes Google AdSense review, then carries AdSense Auto Ads
+ * (publisher pub-5278231299883833) and a dismissible "domain for sale" banner
+ * that routes make-an-offer traffic to the greendomainbrokers broker.
+ * The DW catalog / buy-sample funnel is deliberately absent here — those sites
+ * keep the untouched funnel template above. Nothing here is customer-facing DW.
+ * ==========================================================================*/
+const ADSENSE_PUB = 'pub-5278231299883833';
+const BROKER_URL = 'https://greendomainbrokers.com';
+
+/* AdSense Auto Ads loader — injected into <head> only when cfg.monetize (or an
+ * explicit cfg.adsense) is set. Auto Ads place units automatically, so no manual
+ * <ins> slots are needed. Google also requires a matching /ads.txt (served by
+ * server.js) and a Privacy Policy page (privacyPage below). */
+function adsenseHead(cfg) {
+  if (!cfg.monetize && !cfg.adsense) return '';
+  return `<!-- Google AdSense Auto Ads -->
+<script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-${ADSENSE_PUB}" crossorigin="anonymous"></script>
+<!-- /Google AdSense -->`;
+}
+
+/* Dismissible "this domain is for sale" banner → greendomainbrokers make-offer.
+ * Reuses the .promo-strip visual contract (white ink on --accent). Sits as the
+ * first in-flow element (body.promo-on offsets the fixed chrome). */
+function forSaleBanner(cfg) {
+  if (!cfg.monetize && !cfg.forSale) return '';
+  const url = (cfg.brokerUrl || BROKER_URL) + '/?domain=' + encodeURIComponent(cfg.domain);
+  return `
+<div class="promo-strip forsale-strip" id="forSaleStrip" role="region" aria-label="Domain for sale">
+  <div class="promo-track">
+    <a class="promo-item on" href="${esc(url)}" target="_blank" rel="noopener noreferrer nofollow">
+      <span class="promo-tag">For Sale</span>
+      <span class="promo-dot" aria-hidden="true">&middot;</span>
+      <span class="promo-name">${esc(cfg.domain)} is available &mdash; make an offer</span>
+      <span class="promo-arrow" aria-hidden="true">&rarr;</span></a>
+  </div>
+  <button class="promo-x" id="forSaleX" type="button" aria-label="Dismiss for-sale banner">&times;</button>
+</div>
+<script>
+(function(){
+  var k='${cfg.slug}_forsale_dismiss',s=document.getElementById('forSaleStrip');
+  if(!s)return;
+  try{if(localStorage.getItem(k)==='1'){s.style.display='none';document.body.classList.remove('promo-on');return;}}catch(e){}
+  var x=document.getElementById('forSaleX');
+  x&&x.addEventListener('click',function(){s.style.display='none';document.body.classList.remove('promo-on');
+    try{localStorage.setItem(k,'1');}catch(e){}});
+})();
+</script>`;
+}
+
+/* Monetize-mode nav — no DW catalog / memo / trade links (this is not a DW
+ * funnel). Home / Guide / About / Privacy / Contact only. */
+function monetizeChrome(cfg) {
+  return `
+<div class="topbar">
+  <a class="brand" href="/">${esc(cfg.siteName)}</a>
+  <div style="display:flex;gap:14px;align-items:center">
+    <button class="theme-toggle" id="themeBtn" aria-label="Toggle theme">
+      <svg class="icon-sun" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
+        <circle cx="12" cy="12" r="5"/><path d="M12 1v2M12 21v2M4.2 4.2l1.4 1.4M18.4 18.4l1.4 1.4M1 12h2M21 12h2M4.2 19.8l1.4-1.4M18.4 5.6l1.4-1.4"/></svg>
+      <svg class="icon-moon" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
+        <path d="M21 12.8A9 9 0 1 1 11.2 3a7 7 0 0 0 9.8 9.8z"/></svg>
+    </button>
+  </div>
+</div>
+<button class="gucci-menu" id="menuBtn" aria-expanded="false" aria-controls="navpanel">
+  <span class="gucci-menu__bars" aria-hidden="true"><span></span><span></span><span></span></span>
+  <span class="gucci-menu__label">Menu</span>
+</button>
+<nav class="navpanel" id="navpanel">
+  <a href="/">Home</a>
+  <a href="/#guide">Guide</a>
+  <a href="/about">About</a>
+  <a href="/privacy">Privacy</a>
+  <a href="mailto:${esc(cfg.siteEmail)}">Contact</a>
+</nav>`;
+}
+
+/* Monetize-mode CSS — a real content-site hero + article layout, isolated from
+ * the funnel template's catalog CSS. Uses the site theme vars already in css(). */
+function monetizeCss() {
+  return `
+.m-hero{position:relative;min-height:clamp(460px,78vh,860px);display:flex;align-items:center;
+  justify-content:center;text-align:center;overflow:hidden;
+  background:linear-gradient(135deg,var(--accent) 0%,var(--bg) 62%);}
+[data-theme=dark] .m-hero{background:linear-gradient(135deg,var(--accent) 0%,#0c0a08 70%);}
+.m-hero::before{content:"";position:absolute;inset:0;opacity:.10;
+  background-image:radial-gradient(circle at 20% 30%,#fff 0,transparent 42%),radial-gradient(circle at 80% 70%,#fff 0,transparent 42%);}
+.m-hero__in{position:relative;z-index:2;max-width:820px;padding:40px 28px}
+.m-hero h1{font-family:var(--serif);font-size:clamp(44px,7vw,92px);font-weight:500;line-height:1.02;
+  letter-spacing:.01em;color:#fff;text-shadow:0 2px 30px rgba(0,0,0,.28);text-wrap:balance}
+.m-hero p{color:rgba(255,255,255,.92);font-size:clamp(16px,2vw,21px);margin-top:18px;line-height:1.6;
+  max-width:60ch;margin-left:auto;margin-right:auto}
+.m-article{max-width:760px;margin:0 auto;padding:64px 0 30px}
+.m-article .lede{font-family:var(--serif);font-size:clamp(20px,2.4vw,26px);line-height:1.55;
+  color:var(--fg);margin-bottom:32px}
+.m-sec{margin:0 0 40px}
+.m-sec h2{font-family:var(--serif);font-size:clamp(24px,3vw,34px);margin-bottom:14px;color:var(--fg)}
+.m-sec p{font-size:16.5px;line-height:1.85;color:var(--fg);margin-bottom:14px}
+.m-guide{background:var(--card);border:1px solid var(--rule);border-radius:10px;padding:34px 32px;margin:20px 0 10px}
+.m-guide h2{font-family:var(--serif);font-size:clamp(22px,2.6vw,30px);margin-bottom:18px}
+.m-guide ul{list-style:none;display:grid;gap:14px}
+.m-guide li{display:flex;gap:12px;font-size:15.5px;line-height:1.6;color:var(--fg)}
+.m-guide li b{font-family:var(--serif);color:var(--accent);flex:none}
+.m-legal{max-width:760px;margin:0 auto;padding:30px 0 70px}
+.m-legal h1{font-family:var(--serif);font-size:clamp(30px,4vw,44px);margin-bottom:8px}
+.m-legal h2{font-family:var(--serif);font-size:22px;margin:26px 0 8px}
+.m-legal p{font-size:15px;line-height:1.8;color:var(--fg);margin-bottom:12px}
+.m-legal a{color:var(--accent)}`;
+}
+
+/* Monetize-mode home: nav + big hero + genuine topical article + guide +
+ * Privacy/Contact footer. Content comes from cfg.content (per-site, real copy):
+ *   cfg.content = { lede, sections:[{h,body}], guide:[{k,v}] }
+ * Falls back to niche-derived copy so a site without a content block still
+ * renders a legitimate (if generic) page. */
+function monetizeHome(cfg) {
+  const c = cfg.content || {};
+  const title = `${cfg.siteName} — ${clean(cfg.tagline)}`;
+  const desc = clean(cfg.metaDesc || cfg.tagline);
+  const niche = clean(cfg.nicheLabel || 'design');
+  const lede = c.lede || `${clean(cfg.siteName)} is a guide to ${niche} — what to look for, how to choose, and the ideas worth borrowing.`;
+  const sections = Array.isArray(c.sections) && c.sections.length ? c.sections : [
+    { h: `About ${clean(cfg.siteName)}`, body: clean(cfg.aboutCopy || cfg.tagline) },
+  ];
+  const guide = Array.isArray(c.guide) ? c.guide : [];
+  const banner = forSaleBanner(cfg);
+  const secHtml = sections.map(s =>
+    `<section class="m-sec"><h2>${esc(clean(s.h))}</h2>${
+      String(s.body || '').split(/\n\n+/).map(par => `<p>${esc(clean(par))}</p>`).join('')
+    }</section>`).join('');
+  const guideHtml = guide.length ? `
+  <div class="m-guide" id="guide">
+    <h2>${esc(clean(c.guideTitle || 'A quick guide'))}</h2>
+    <ul>${guide.map(g => `<li><b>${esc(clean(g.k))}</b><span>${esc(clean(g.v))}</span></li>`).join('')}</ul>
+  </div>` : '';
+  return head(cfg, title, desc, '/', null, banner ? 'promo-on' : '')
+    + `<style>${monetizeCss()}</style>`
+    + banner + jsonld(cfg) + monetizeChrome(cfg) + `
+<header class="m-hero">
+  <div class="m-hero__in">
+    <h1>${esc(cfg.heroHeadline || cfg.siteName)}</h1>
+    <p>${esc(clean(cfg.tagline))}</p>
+  </div>
+</header>
+<div class="wrap">
+  <article class="m-article">
+    <p class="lede">${esc(clean(lede))}</p>
+    ${secHtml}
+    ${guideHtml}
+  </article>
+</div>
+${monetizeFooter(cfg)}
+${script(cfg)}
+</body></html>`;
+}
+
+/* Monetize-mode footer — Privacy/About/Contact links + for-sale line. No DW
+ * address block (this is not a DW-branded funnel). */
+function monetizeFooter(cfg) {
+  const url = (cfg.brokerUrl || BROKER_URL) + '/?domain=' + encodeURIComponent(cfg.domain);
+  return `<footer><div class="wrap">
+  <div><div class="fb">${esc(cfg.siteName)}</div>
+    <div style="margin-top:6px">${esc(clean(cfg.tagline))}</div></div>
+  <div><a href="/about">About</a> &middot; <a href="/privacy">Privacy Policy</a> &middot;
+    <a href="mailto:${esc(cfg.siteEmail)}">Contact</a></div>
+  <div><a href="${esc(url)}" target="_blank" rel="noopener noreferrer nofollow">This domain is for sale &rarr;</a><br>
+    &copy; ${new Date().getFullYear()} ${esc(cfg.siteName)}</div>
+</div></footer>`;
+}
+
+/* Privacy Policy — required for AdSense approval. Covers cookies, Google's use
+ * of the DoubleClick/ad cookie, third-party vendors, and opt-out links. */
+function privacyPage(cfg) {
+  const title = `Privacy Policy — ${cfg.siteName}`;
+  return head(cfg, title, `Privacy policy for ${clean(cfg.siteName)}.`, '/privacy')
+    + `<style>${monetizeCss()}</style>`
+    + (cfg.monetize || cfg.forSale ? '' : '') + jsonld(cfg) + monetizeChrome(cfg) + `
+<div style="height:96px"></div>
+<div class="wrap"><section class="m-legal">
+  <h1>Privacy Policy</h1>
+  <p>Last updated ${new Date().toLocaleDateString('en-US',{year:'numeric',month:'long',day:'numeric'})}. This policy explains how ${esc(clean(cfg.siteName))} (&ldquo;we&rdquo;, &ldquo;this site&rdquo;) handles information when you visit ${esc(cfg.domain)}.</p>
+
+  <h2>Information we collect</h2>
+  <p>This is a content website. We do not ask you to create an account and we do not knowingly collect personal information you do not choose to send us. If you email us, we receive the address and message you send so we can reply.</p>
+
+  <h2>Cookies &amp; advertising</h2>
+  <p>Third-party vendors, including Google, use cookies to serve ads based on your prior visits to this and other websites. Google&rsquo;s use of advertising cookies enables it and its partners to serve ads to you based on your visit to this site and/or other sites on the internet.</p>
+  <p>You may opt out of personalized advertising by visiting <a href="https://www.google.com/settings/ads" target="_blank" rel="noopener noreferrer">Google Ads Settings</a>. You can also opt out of a third-party vendor&rsquo;s use of cookies for personalized advertising at <a href="https://www.aboutads.info/choices" target="_blank" rel="noopener noreferrer">aboutads.info/choices</a>.</p>
+
+  <h2>Analytics</h2>
+  <p>We may use privacy-respecting analytics to understand aggregate traffic (pages viewed, general region, device type). This data is not used to identify you personally.</p>
+
+  <h2>Your choices</h2>
+  <p>Most browsers let you refuse or delete cookies through their settings. Doing so will not prevent you from reading this site.</p>
+
+  <h2>Contact</h2>
+  <p>Questions about this policy? Email <a href="mailto:${esc(cfg.siteEmail)}">${esc(cfg.siteEmail)}</a>.</p>
+</section></div>
+${monetizeFooter(cfg)}
+${script(cfg)}
+</body></html>`;
+}
+
+/* Monetize-mode About — standalone, no DW funnel copy. */
+function monetizeAbout(cfg) {
+  const c = cfg.content || {};
+  const title = `About — ${cfg.siteName}`;
+  const body = c.about || clean(cfg.aboutCopy || cfg.tagline);
+  return head(cfg, title, clean(cfg.metaDesc || cfg.tagline), '/about')
+    + `<style>${monetizeCss()}</style>`
+    + jsonld(cfg) + monetizeChrome(cfg) + `
+<div style="height:96px"></div>
+<div class="wrap"><section class="m-legal">
+  <h1>${esc(cfg.siteName)}</h1>
+  ${String(body).split(/\n\n+/).map(p => `<p>${esc(clean(p))}</p>`).join('')}
+  <p>Questions or ideas? Email <a href="mailto:${esc(cfg.siteEmail)}">${esc(cfg.siteEmail)}</a>.</p>
+</section></div>
+${monetizeFooter(cfg)}
+${script(cfg)}
+</body></html>`;
+}
+
+module.exports = { homePage, catalogPage, aboutPage, infoPage, productPage, monetizeHome, monetizeAbout, privacyPage, clean, esc, productSlug, scrubVendor, imgToken, proxyImg };

← c1f8120 fix(deploy): no-clobber nginx vhost install so certbot :443  ·  back to Dw Domain Fleet  ·  monetize: render authored content copy verbatim (skip DW wal e10d68c →