← back to Dw Domain Fleet
monetize DEPTH: add /articles hub + per-article long-form pages (Article JSON-LD, nav, home cross-links, sitemap) — content plumbing (TK-11322)
64e2a9d7ece6ee7cf09fed0edcd82302d78502d8 · 2026-09-09 16:23:28 -0700 · Steve Abrams
Files touched
M server.jsM shared/render.js
Diff
commit 64e2a9d7ece6ee7cf09fed0edcd82302d78502d8
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Wed Sep 9 16:23:28 2026 -0700
monetize DEPTH: add /articles hub + per-article long-form pages (Article JSON-LD, nav, home cross-links, sitemap) — content plumbing (TK-11322)
---
server.js | 16 +++++++-
shared/render.js | 122 +++++++++++++++++++++++++++++++++++++++++++++++++++++--
2 files changed, 134 insertions(+), 4 deletions(-)
diff --git a/server.js b/server.js
index 5c9ed45..0671ef1 100644
--- a/server.js
+++ b/server.js
@@ -151,13 +151,27 @@ if (MONETIZE) {
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)));
+ // Long-form article hub + per-article pages (content DEPTH — TK-11322).
+ const ARTICLES = (cfg.content && Array.isArray(cfg.content.articles)) ? cfg.content.articles : [];
+ if (ARTICLES.length) {
+ app.get('/articles', (req, res) => res.type('html').send(render.articlesIndex(cfg)));
+ app.get('/articles/:slug', (req, res) => {
+ const key = String(req.params.slug || '').slice(0, 120);
+ const a = ARTICLES.find(x => x.slug === key);
+ if (!a) return res.redirect(301, '/articles');
+ const others = ARTICLES.filter(x => x.slug !== key);
+ res.type('html').send(render.articlePage(cfg, a, others));
+ });
+ }
// 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 artPaths = (cfg.content && Array.isArray(cfg.content.articles) && cfg.content.articles.length)
+ ? ['/articles', ...cfg.content.articles.map(a => '/articles/' + a.slug)] : [];
+ const paths = ['/', '/guide', '/about', '/privacy', '/contact', ...artPaths];
const urls = paths.map(p =>
` <url><loc>https://${cfg.domain}${p}</loc><lastmod>${today}</lastmod></url>`).join('\n');
res.type('application/xml').send(
diff --git a/shared/render.js b/shared/render.js
index b8c4dd0..51cdcb0 100644
--- a/shared/render.js
+++ b/shared/render.js
@@ -1089,6 +1089,7 @@ function monetizeChrome(cfg) {
<a class="m-brand" href="/">${esc(cfg.siteName)}</a>
<nav class="m-nav__links">
<a href="/">Home</a>
+ ${(cfg.content && Array.isArray(cfg.content.articles) && cfg.content.articles.length) ? '<a href="/articles">Articles</a>' : ''}
<a href="/guide">Guide</a>
<a href="/about">About</a>
<a href="/privacy">Privacy</a>
@@ -1174,7 +1175,111 @@ function monetizeCss() {
.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)}`;
+.m-legal a{color:var(--accent)}
+/* home "Read more" article cross-links + /articles index */
+.m-more{margin:44px 0 4px;border-top:1px solid var(--rule);padding-top:30px}
+.m-more h2,.m-index h1{font-family:var(--serif);color:var(--fg)}
+.m-more h2{font-size:clamp(20px,2.4vw,26px);margin-bottom:16px}
+.m-more__list,.m-index__list{list-style:none;display:grid;gap:12px}
+.m-more__list a,.m-index__list a{display:block;padding:16px 20px;border:1px solid var(--rule);border-radius:10px;
+ background:var(--card);transition:border-color .15s,transform .15s}
+.m-more__list a:hover,.m-index__list a:hover{border-color:var(--accent);transform:translateY(-1px)}
+.m-more__t,.m-index__t{display:block;font-family:var(--serif);font-size:19px;color:var(--fg)}
+.m-more__d,.m-index__d{display:block;font-size:14.5px;line-height:1.55;color:var(--muted);margin-top:5px}
+.m-index__meta{display:block;font-size:12px;letter-spacing:.1em;text-transform:uppercase;color:var(--accent);margin-top:8px}
+.m-index{max-width:760px;margin:0 auto;padding:56px 0 30px}
+.m-index h1{font-size:clamp(30px,4vw,46px);margin-bottom:10px}
+.m-index .lede{font-family:var(--serif);font-size:clamp(19px,2.3vw,24px);line-height:1.55;color:var(--fg);margin-bottom:28px}
+/* long-form article page — real reading, prose not collapsed */
+.m-post{max-width:720px;margin:0 auto;padding:52px 0 20px}
+.m-post .crumb{font-size:12px;letter-spacing:.1em;text-transform:uppercase;color:var(--muted);margin-bottom:14px}
+.m-post .crumb a{color:var(--accent)}
+.m-post h1{font-family:var(--serif);font-size:clamp(30px,4.4vw,50px);line-height:1.06;color:var(--fg);margin-bottom:12px;text-wrap:balance}
+.m-post .dek{font-family:var(--serif);font-size:clamp(18px,2.2vw,23px);line-height:1.5;color:var(--muted);margin-bottom:10px}
+.m-post .byline{font-size:12.5px;letter-spacing:.08em;text-transform:uppercase;color:var(--muted);margin-bottom:30px;border-bottom:1px solid var(--rule);padding-bottom:20px}
+.m-post h2{font-family:var(--serif);font-size:clamp(23px,2.8vw,32px);color:var(--fg);margin:36px 0 12px}
+.m-post p{font-size:17px;line-height:1.9;color:var(--fg);margin-bottom:16px}
+.m-post__nav{margin-top:40px;border-top:1px solid var(--rule);padding-top:26px}
+.m-post__nav h3{font-family:var(--serif);font-size:19px;color:var(--fg);margin-bottom:12px}`;
+}
+
+/* Article (BlogPosting) JSON-LD — makes each long-form page eligible for
+ * article rich results and signals a real content property to crawlers. */
+function articleJsonLd(cfg, a) {
+ const ld = {
+ '@context': 'https://schema.org', '@type': 'Article',
+ headline: String(a.title || ''),
+ description: String(a.dek || ''),
+ author: { '@type': 'Organization', name: cfg.siteName },
+ publisher: { '@type': 'Organization', name: cfg.siteName },
+ mainEntityOfPage: 'https://' + cfg.domain + '/articles/' + a.slug,
+ inLanguage: 'en'
+ };
+ return `<script type="application/ld+json">${JSON.stringify(ld).replace(/<\/script/gi, '<\\/script')}</script>`;
+}
+
+/* /articles — index of the site's long-form articles (real internal-linking
+ * hub; a multi-page content property reads as legitimate to AdSense review). */
+function articlesIndex(cfg) {
+ const c = cfg.content || {};
+ const articles = Array.isArray(c.articles) ? c.articles : [];
+ const title = `Articles — ${cfg.siteName}`;
+ const desc = clean(`In-depth articles from ${cfg.siteName} — ${cfg.tagline || ''}`);
+ const lede = c.articlesLede
+ || `In-depth reading on ${clean(cfg.nicheLabel || 'the subject')} — practical guides written to actually help.`;
+ const list = articles.map(a => `
+ <li><a href="/articles/${esc(a.slug)}">
+ <span class="m-index__t">${esc(a.title)}</span>
+ ${a.dek ? `<span class="m-index__d">${esc(a.dek)}</span>` : ''}
+ ${a.readMins ? `<span class="m-index__meta">${esc(String(a.readMins))} min read</span>` : ''}
+ </a></li>`).join('');
+ return head(cfg, title, desc, '/articles')
+ + `<style>${monetizeCss()}</style>`
+ + jsonld(cfg) + monetizeChrome(cfg) + `
+<div class="wrap"><section class="m-index">
+ <h1>Articles</h1>
+ <p class="lede">${esc(lede)}</p>
+ <ul class="m-index__list">${list}</ul>
+</section></div>
+${monetizeFooter(cfg)}
+${script(cfg)}
+</body></html>`;
+}
+
+/* /articles/:slug — one long-form article rendered as real prose (not collapsed
+ * panels), with a breadcrumb, an author/read-time byline, Article JSON-LD, and a
+ * "more articles" internal-link block at the foot. */
+function articlePage(cfg, a, others) {
+ const title = `${a.title} — ${cfg.siteName}`;
+ const desc = clean(a.dek || cfg.metaDesc || cfg.tagline);
+ const sections = Array.isArray(a.sections) ? a.sections : [];
+ const secHtml = sections.map(s =>
+ `<h2>${esc(s.h)}</h2>${
+ String(s.body || '').split(/\n\n+/).map(par => `<p>${esc(par)}</p>`).join('')
+ }`).join('');
+ const moreHtml = (Array.isArray(others) && others.length) ? `
+ <nav class="m-post__nav" aria-label="More articles">
+ <h3>More from ${esc(cfg.siteName)}</h3>
+ <ul class="m-index__list">${others.slice(0, 3).map(o => `
+ <li><a href="/articles/${esc(o.slug)}"><span class="m-index__t">${esc(o.title)}</span>${
+ o.dek ? `<span class="m-index__d">${esc(o.dek)}</span>` : ''
+ }</a></li>`).join('')}
+ </ul>
+ </nav>` : '';
+ return head(cfg, title, desc, '/articles/' + a.slug)
+ + `<style>${monetizeCss()}</style>`
+ + jsonld(cfg) + articleJsonLd(cfg, a) + monetizeChrome(cfg) + `
+<div class="wrap"><article class="m-post">
+ <div class="crumb"><a href="/articles">Articles</a> › ${esc(a.title)}</div>
+ <h1>${esc(a.title)}</h1>
+ ${a.dek ? `<p class="dek">${esc(a.dek)}</p>` : ''}
+ <div class="byline">${esc(cfg.siteName)}${a.readMins ? ` · ${esc(String(a.readMins))} min read` : ''}</div>
+ ${secHtml}
+ ${moreHtml}
+</article></div>
+${monetizeFooter(cfg)}
+${script(cfg)}
+</body></html>`;
}
/* Monetize-mode home: nav + big hero + genuine topical article + guide +
@@ -1215,6 +1320,16 @@ function monetizeHome(cfg) {
<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>` : '';
+ const articles = Array.isArray(c.articles) ? c.articles : [];
+ const articlesHtml = articles.length ? `
+ <section class="m-more" aria-labelledby="more-h">
+ <h2 id="more-h">Read more</h2>
+ <ul class="m-more__list">${articles.map(a => `
+ <li><a href="/articles/${esc(a.slug)}"><span class="m-more__t">${esc(a.title)}</span>${
+ a.dek ? `<span class="m-more__d">${esc(a.dek)}</span>` : ''
+ }</a></li>`).join('')}
+ </ul>
+ </section>` : '';
return head(cfg, title, desc, '/', null, '')
+ `<style>${monetizeCss()}</style>`
+ banner + jsonld(cfg) + faqJsonLd(faq) + monetizeChrome(cfg) + `
@@ -1229,6 +1344,7 @@ function monetizeHome(cfg) {
<article class="m-article">
<p class="lede">${esc(lede)}</p>
<div class="m-panels" id="guide">${secHtml}${guideHtml}${faqHtml}</div>
+ ${articlesHtml}
</article>
</div>
${monetizeFooter(cfg)}
@@ -1249,7 +1365,7 @@ function monetizeFooter(cfg) {
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="/">Home</a> · <a href="/guide">Guide</a> · <a href="/about">About</a> ·
+ <div><a href="/">Home</a> · ${(cfg.content && Array.isArray(cfg.content.articles) && cfg.content.articles.length) ? '<a href="/articles">Articles</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>
@@ -1396,4 +1512,4 @@ ${script(cfg)}
</body></html>`;
}
-module.exports = { homePage, catalogPage, aboutPage, infoPage, productPage, monetizeHome, monetizeAbout, privacyPage, guidePage, contactPage, clean, esc, productSlug, scrubVendor, imgToken, proxyImg };
+module.exports = { homePage, catalogPage, aboutPage, infoPage, productPage, monetizeHome, monetizeAbout, privacyPage, guidePage, contactPage, articlesIndex, articlePage, clean, esc, productSlug, scrubVendor, imgToken, proxyImg };
← bab12e1 fleet: deploy-safe showroom-vendor require — fall back to in
·
back to Dw Domain Fleet
·
fleet Phase 2: scaffold 138 parked monetize domains — config b6088ea →