[object Object]

← back to Dw Domain Fleet

monetize template: full AdSense-ready build — Guide+Contact pages, sitemap.xml/robots.txt, FAQ+schema, cookie notice, favicon, og:image, approval-first for-sale phasing (TK-11322)

1681057dfa708b3424bb93c967482073ba5f408e · 2026-09-09 11:00:37 -0700 · Steve Abrams

Steve decisions (2026-09-09): approve-first (prominent for-sale banner OFF until
forSalePhase='live'; subtle neutral 'Domain inquiries' footer link meanwhile) +
full template build. Adds: /guide + /contact pages, /sitemap.xml + /robots.txt,
FAQPage JSON-LD + FAQ panel, dismissible cookie-consent notice, per-site monogram
favicon, og:image from hero. Verified: all 9 routes 200, banner suppressed in
approval phase.

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

Files touched

Diff

commit 1681057dfa708b3424bb93c967482073ba5f408e
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Wed Sep 9 11:00:37 2026 -0700

    monetize template: full AdSense-ready build — Guide+Contact pages, sitemap.xml/robots.txt, FAQ+schema, cookie notice, favicon, og:image, approval-first for-sale phasing (TK-11322)
    
    Steve decisions (2026-09-09): approve-first (prominent for-sale banner OFF until
    forSalePhase='live'; subtle neutral 'Domain inquiries' footer link meanwhile) +
    full template build. Adds: /guide + /contact pages, /sitemap.xml + /robots.txt,
    FAQPage JSON-LD + FAQ panel, dismissible cookie-consent notice, per-site monogram
    favicon, og:image from hero. Verified: all 9 routes 200, banner suppressed in
    approval phase.
    
    Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_01S81Y5KkGKrUHg2mwWQtD96
---
 server.js        |  14 ++++++
 shared/render.js | 147 ++++++++++++++++++++++++++++++++++++++++++++++++++-----
 2 files changed, 150 insertions(+), 11 deletions(-)

diff --git a/server.js b/server.js
index 345521e..7ba5ca6 100644
--- a/server.js
+++ b/server.js
@@ -145,6 +145,20 @@ if (MONETIZE) {
   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('/guide', (req, res) => res.type('html').send(render.guidePage(cfg)));
+  app.get('/contact', (req, res) => res.type('html').send(render.contactPage(cfg)));
+  // robots.txt — allow full crawl, point at the sitemap (SEO + AdSense discovery)
+  app.get('/robots.txt', (req, res) => res.type('text/plain')
+    .send(`User-agent: *\nAllow: /\nSitemap: https://${cfg.domain}/sitemap.xml\n`));
+  // sitemap.xml — the monetize site's real pages
+  app.get('/sitemap.xml', (req, res) => {
+    const today = new Date().toISOString().slice(0, 10);
+    const paths = ['/', '/guide', '/about', '/privacy', '/contact'];
+    const urls = paths.map(p =>
+      `  <url><loc>https://${cfg.domain}${p}</loc><lastmod>${today}</lastmod></url>`).join('\n');
+    res.type('application/xml').send(
+      `<?xml version="1.0" encoding="UTF-8"?>\n<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n${urls}\n</urlset>\n`);
+  });
   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.
diff --git a/shared/render.js b/shared/render.js
index ee399b3..b8c4dd0 100644
--- a/shared/render.js
+++ b/shared/render.js
@@ -489,6 +489,12 @@ height="0" width="0" style="display:none;visibility:hidden"></iframe></noscript>
 function head(cfg, title, desc, path, canonicalOverride, bodyClass) {
   const url = 'https://' + cfg.domain + (path || '');
   const canonical = canonicalOverride || url;
+  const heroAbs = cfg.heroImage
+    ? (/^https?:/.test(cfg.heroImage) ? cfg.heroImage : 'https://' + cfg.domain + cfg.heroImage)
+    : '';
+  const ogImg = heroAbs
+    ? `<meta property="og:image" content="${esc(heroAbs)}"><meta name="twitter:image" content="${esc(heroAbs)}">`
+    : '';
   return `<!doctype html><html lang="en"><head>
 <meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
 <title>${esc(title)}</title>
@@ -500,6 +506,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)}">
+${ogImg}${faviconTag(cfg)}
 ${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>
@@ -1042,6 +1049,11 @@ function adsenseHead(cfg) {
  * 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) {
+  // APPROVAL-FIRST (Steve 2026-09-09): a prominent "for sale" banner reads as a
+  // parked domain to AdSense review, so it stays OFF until forSalePhase === 'live'
+  // (flip per site AFTER AdSense approval). Until then the only sale signal is the
+  // subtle neutral "domain inquiries" line in monetizeFooter.
+  if (cfg.forSalePhase !== 'live') return '';
   if (!cfg.monetize && !cfg.forSale) return '';
   const url = (cfg.brokerUrl || BROKER_URL) + '/?domain=' + encodeURIComponent(cfg.domain);
   return `
@@ -1077,10 +1089,10 @@ function monetizeChrome(cfg) {
   <a class="m-brand" href="/">${esc(cfg.siteName)}</a>
   <nav class="m-nav__links">
     <a href="/">Home</a>
-    <a href="/#guide">Guide</a>
+    <a href="/guide">Guide</a>
     <a href="/about">About</a>
     <a href="/privacy">Privacy</a>
-    <a href="mailto:${esc(cfg.siteEmail)}">Contact</a>
+    <a href="/contact">Contact</a>
     <button class="theme-toggle" id="themeBtn" aria-label="Toggle theme">
       <svg class="icon-sun" width="17" height="17" 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>
@@ -1147,7 +1159,18 @@ function monetizeCss() {
 .m-panel__body ul{list-style:none;display:grid;gap:12px;margin-top:2px}
 .m-panel__body li{display:flex;gap:14px;font-size:15.5px;line-height:1.6;color:var(--fg)}
 .m-panel__body li b{font-family:var(--serif);color:var(--accent);flex:none;min-width:70px}
-.m-legal{max-width:760px;margin:0 auto;padding:30px 0 70px}
+.m-faq{margin:0 0 16px}
+.m-faq h3{font-family:var(--serif);font-size:18px;color:var(--fg);margin-bottom:5px}
+.m-faq p{font-size:15.5px;line-height:1.75;color:var(--muted);margin:0}
+/* cookie-consent notice */
+.cookie-notice{position:fixed;left:16px;right:16px;bottom:16px;z-index:120;max-width:640px;margin:0 auto;
+  display:flex;align-items:center;gap:16px;justify-content:space-between;flex-wrap:wrap;
+  background:var(--card);color:var(--fg);border:1px solid var(--rule);border-radius:10px;
+  padding:14px 18px;box-shadow:0 10px 34px rgba(0,0,0,.18);font-size:13.5px;line-height:1.5}
+.cookie-notice a{color:var(--accent);text-decoration:underline}
+.cookie-notice button{flex:none;background:var(--accent);color:#fff;border:0;border-radius:6px;
+  padding:9px 18px;font-size:12px;letter-spacing:.08em;text-transform:uppercase;cursor:pointer}
+.m-legal{max-width:760px;margin:0 auto;padding:52px 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}
@@ -1186,9 +1209,15 @@ function monetizeHome(cfg) {
       <div class="m-panel__body">
         <ul>${guide.map(g => `<li><b>${esc(g.k)}</b><span>${esc(g.v)}</span></li>`).join('')}</ul>
       </div></details>` : '';
+  const faq = Array.isArray(c.faq) ? c.faq : [];
+  const faqHtml = faq.length ? `
+    <details class="m-panel"><summary>${esc(c.faqTitle || 'Frequently asked')}</summary>
+      <div class="m-panel__body">
+        ${faq.map(f => `<div class="m-faq"><h3>${esc(f.q)}</h3><p>${esc(f.a)}</p></div>`).join('')}
+      </div></details>` : '';
   return head(cfg, title, desc, '/', null, '')
     + `<style>${monetizeCss()}</style>`
-    + banner + jsonld(cfg) + monetizeChrome(cfg) + `
+    + banner + jsonld(cfg) + faqJsonLd(faq) + monetizeChrome(cfg) + `
 <header class="m-hero${heroImg ? ' has-img' : ''}">
   ${heroImg ? `<div class="m-hero__bg" style="background-image:url('${esc(heroImg)}')"></div>` : ''}
   <div class="m-hero__in">
@@ -1199,7 +1228,7 @@ function monetizeHome(cfg) {
 <div class="wrap">
   <article class="m-article">
     <p class="lede">${esc(lede)}</p>
-    <div class="m-panels" id="guide">${secHtml}${guideHtml}</div>
+    <div class="m-panels" id="guide">${secHtml}${guideHtml}${faqHtml}</div>
   </article>
 </div>
 ${monetizeFooter(cfg)}
@@ -1211,14 +1240,20 @@ ${script(cfg)}
  * address block (this is not a DW-branded funnel). */
 function monetizeFooter(cfg) {
   const url = (cfg.brokerUrl || BROKER_URL) + '/?domain=' + encodeURIComponent(cfg.domain);
+  // During approval (forSalePhase !== 'live') the sale signal is a NEUTRAL, subtle
+  // footer line ("Domain inquiries") — not the word "for sale" — so it doesn't read
+  // as a parked domain. Post-approval the prominent banner returns via forSaleBanner.
+  const saleLine = cfg.forSalePhase === 'live'
+    ? `<a href="${esc(url)}" target="_blank" rel="noopener noreferrer nofollow">This domain is for sale &rarr;</a><br>`
+    : `<a href="${esc(url)}" target="_blank" rel="noopener noreferrer nofollow" style="opacity:.75">Domain inquiries</a><br>`;
   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>`;
+  <div><a href="/">Home</a> &middot; <a href="/guide">Guide</a> &middot; <a href="/about">About</a> &middot;
+    <a href="/privacy">Privacy</a> &middot; <a href="/contact">Contact</a></div>
+  <div>${saleLine}&copy; ${new Date().getFullYear()} ${esc(cfg.siteName)}</div>
+</div></footer>
+${cookieNotice(cfg)}`;
 }
 
 /* Privacy Policy — required for AdSense approval. Covers cookies, Google's use
@@ -1271,4 +1306,94 @@ ${script(cfg)}
 </body></html>`;
 }
 
-module.exports = { homePage, catalogPage, aboutPage, infoPage, productPage, monetizeHome, monetizeAbout, privacyPage, clean, esc, productSlug, scrubVendor, imgToken, proxyImg };
+/* Dismissible cookie-consent notice — required for personalized ads to EEA/UK
+ * traffic, good practice everywhere. Links to the Privacy Policy. localStorage
+ * dismiss so it shows once per viewer. Fixed bottom bar, low-key. */
+function cookieNotice(cfg) {
+  if (!cfg.monetize && !cfg.adsense) return '';
+  return `
+<div class="cookie-notice" id="cookieNotice" role="dialog" aria-label="Cookie notice">
+  <span>We use cookies for ads and analytics. By using this site you agree to our
+    <a href="/privacy">Privacy Policy</a>.</span>
+  <button type="button" id="cookieOk">Got it</button>
+</div>
+<script>
+(function(){
+  var k='${cfg.slug}_cookie_ok',n=document.getElementById('cookieNotice');
+  if(!n)return;
+  try{if(localStorage.getItem(k)==='1'){n.style.display='none';return;}}catch(e){}
+  var b=document.getElementById('cookieOk');
+  b&&b.addEventListener('click',function(){n.style.display='none';try{localStorage.setItem(k,'1');}catch(e){}});
+})();
+</script>`;
+}
+
+/* FAQPage JSON-LD — makes the FAQ eligible for rich results. */
+function faqJsonLd(faq) {
+  if (!Array.isArray(faq) || !faq.length) return '';
+  const ld = {
+    '@context': 'https://schema.org', '@type': 'FAQPage',
+    mainEntity: faq.map(f => ({
+      '@type': 'Question', name: String(f.q || ''),
+      acceptedAnswer: { '@type': 'Answer', text: String(f.a || '') }
+    }))
+  };
+  return `<script type="application/ld+json">${JSON.stringify(ld).replace(/<\/script/gi, '<\\/script')}</script>`;
+}
+
+/* Per-site monogram favicon (data-URI SVG) — first letter on the theme accent.
+ * Applies to every page via head(); no server route or asset file needed. */
+function faviconTag(cfg) {
+  const letter = esc((cfg.siteName || '?').trim().charAt(0).toUpperCase() || 'W');
+  const accent = (cfg.theme && cfg.theme.accent) || '#7a2230';
+  const svg = `<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'><rect width='64' height='64' rx='12' fill='${accent}'/><text x='32' y='44' font-family='Georgia,serif' font-size='38' fill='%23fff' text-anchor='middle'>${letter}</text></svg>`;
+  return `<link rel="icon" href="data:image/svg+xml,${encodeURIComponent(svg).replace(/'/g, '%27')}">`;
+}
+
+/* Second content page (/guide) — a real, distinct topical page so the site is
+ * multi-page with substantive content (raises AdSense-approval odds). Content
+ * from cfg.content.guidePage { lede, sections:[{h,body}] }; falls back to the
+ * home guide list if no dedicated guide article was authored. */
+function guidePage(cfg) {
+  const c = cfg.content || {};
+  const g = c.guidePage || null;
+  const title = `Guide — ${cfg.siteName}`;
+  const desc = clean(`A practical guide from ${cfg.siteName} — ${cfg.tagline || ''}`);
+  const lede = (g && g.lede) || c.lede || `A practical guide to ${clean(cfg.nicheLabel || 'the essentials')}.`;
+  const sections = (g && Array.isArray(g.sections) && g.sections.length) ? g.sections
+    : (Array.isArray(c.sections) ? c.sections : []);
+  const secHtml = sections.map((s, i) =>
+    `<details class="m-panel"${i === 0 ? ' open' : ''}><summary>${esc(s.h)}</summary>
+      <div class="m-panel__body">${String(s.body || '').split(/\n\n+/).map(p => `<p>${esc(p)}</p>`).join('')}</div>
+    </details>`).join('');
+  return head(cfg, title, desc, '/guide')
+    + `<style>${monetizeCss()}</style>`
+    + jsonld(cfg) + monetizeChrome(cfg) + `
+<div class="wrap"><article class="m-article" style="padding-top:52px">
+  <h1 style="font-family:var(--serif);font-size:clamp(30px,4vw,46px);margin-bottom:14px">Guide</h1>
+  <p class="lede">${esc(lede)}</p>
+  <div class="m-panels">${secHtml}</div>
+</article></div>
+${monetizeFooter(cfg)}
+${script(cfg)}
+</body></html>`;
+}
+
+/* Contact page (/contact) — a clear contact route AdSense expects. */
+function contactPage(cfg) {
+  const title = `Contact — ${cfg.siteName}`;
+  return head(cfg, title, `Contact ${clean(cfg.siteName)}.`, '/contact')
+    + `<style>${monetizeCss()}</style>`
+    + jsonld(cfg) + monetizeChrome(cfg) + `
+<div class="wrap"><section class="m-legal">
+  <h1>Contact</h1>
+  <p>Questions, feedback, or a correction to something you read here? We would like to hear from you.</p>
+  <p style="font-size:19px;margin:18px 0"><a href="mailto:${esc(cfg.siteEmail)}" style="color:var(--accent)">${esc(cfg.siteEmail)}</a></p>
+  <p>We read every message and reply to genuine inquiries.</p>
+</section></div>
+${monetizeFooter(cfg)}
+${script(cfg)}
+</body></html>`;
+}
+
+module.exports = { homePage, catalogPage, aboutPage, infoPage, productPage, monetizeHome, monetizeAbout, privacyPage, guidePage, contactPage, clean, esc, productSlug, scrubVendor, imgToken, proxyImg };

← 00c6cee monetize: content sections + checklist as collapsed <details  ·  back to Dw Domain Fleet  ·  monetize Phase 1: enable monetize mode + per-domain content b9a98bc →