[object Object]

← 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

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> &rsaquo; ${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 ? ` &middot; ${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> &middot; <a href="/guide">Guide</a> &middot; <a href="/about">About</a> &middot;
+  <div><a href="/">Home</a> &middot; ${(cfg.content && Array.isArray(cfg.content.articles) && cfg.content.articles.length) ? '<a href="/articles">Articles</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>
@@ -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 →