← 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
M server.jsM shared/render.js
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 →</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> · <a href="/privacy">Privacy Policy</a> ·
- <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 →</a><br>
- © ${new Date().getFullYear()} ${esc(cfg.siteName)}</div>
-</div></footer>`;
+ <div><a href="/">Home</a> · <a href="/guide">Guide</a> · <a href="/about">About</a> ·
+ <a href="/privacy">Privacy</a> · <a href="/contact">Contact</a></div>
+ <div>${saleLine}© ${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 →