← back to Consulting Designerwallcoverings Com

scripts/aeo-render.mjs

58 lines

#!/usr/bin/env node
// TK-20 — render an AEO draft (docs/aeo-drafts/<slug>.md) into the publishable
// HTML block: direct-answer paragraph + visible FAQ accordion (<details>).
// The visible FAQ text mirrors the FAQPage JSON-LD verbatim (Google match rule).
// Steve rulings baked in (approve-all 2026-07-26): "wallpaper" allowed ONLY in
// H1/FAQ questions (search-intent surfaces); internal links to not-yet-live
// pages are stripped; JSON-LD ships separately via theme snippet, NOT body_html.
import { readFileSync } from 'node:fs';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';

const HERE = dirname(fileURLToPath(import.meta.url));
const slug = process.argv[2];
if (!slug) { console.error('usage: aeo-render.mjs <draft-slug>'); process.exit(1); }
const md = readFileSync(join(HERE, '..', 'docs', 'aeo-drafts', slug + '.md'), 'utf8');

const esc = s => s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
// Inline markdown: bold, italics, strip links to not-yet-live internals, plain links kept as text.
const inline = s => esc(s.trim())
  .replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>')
  .replace(/\*([^*]+)\*/g, '<em>$1</em>')
  .replace(/\[([^\]]+)\]\([^)]*\)/g, '$1');

// H1
const h1 = (md.match(/^# (.+)$/m) || [])[1] || slug;

// Direct answer: first blockquote after the "Direct answer" heading (skip editorial-note quotes).
const daSection = md.split(/## Direct answer[^\n]*\n/)[1] || '';
const daLines = [];
for (const line of daSection.split('\n')) {
  if (line.startsWith('>')) { const t = line.replace(/^>\s?/, ''); if (/editorial note/i.test(t)) break; daLines.push(t); }
  else if (daLines.length) break;
}
const directAnswer = inline(daLines.join(' ').trim());

// FAQ pairs: "**Q: ...**" then "A: ..." (answer runs to next Q or section break).
const faqSection = md.split(/## FAQ[^\n]*\n/)[1] || '';
const faq = [];
const qRe = /\*\*Q:\s*([^*]+)\*\*\s*\n(?:A:\s*)?([\s\S]*?)(?=\n\*\*Q:|\n## |\n---|$)/g;
let m;
while ((m = qRe.exec(faqSection)) !== null) {
  const q = m[1].trim();
  let a = m[2].replace(/^A:\s*/, '').trim().replace(/\n+/g, ' ');
  a = a.replace(/\*\(Editor:[^)]*\)\*/gi, '').trim(); // strip editor-only notes
  if (q && a) faq.push([q, a]);
}

const faqHtml = faq.map(([q, a]) =>
  `<details class="dw-aeo-faq"><summary><strong>${inline(q)}</strong></summary><p>${inline(a)}</p></details>`).join('\n');

const block = `<div class="dw-aeo-block" data-aeo="${esc(slug)}">
<p class="dw-aeo-answer">${directAnswer}</p>
<h2>Frequently asked questions</h2>
${faqHtml}
</div>`;

console.log(JSON.stringify({ slug, h1, faq_count: faq.length, html: block }));