HEX
Server: LiteSpeed
System: Linux houston.panomity.com 6.8.0-100-generic #100-Ubuntu SMP PREEMPT_DYNAMIC Tue Jan 13 16:40:06 UTC 2026 x86_64
User: nudepix (1011)
PHP: 7.4.33
Disabled: pcntl_alarm,pcntl_fork,pcntl_waitpid,pcntl_wait,pcntl_wifexited,pcntl_wifstopped,pcntl_wifsignaled,pcntl_wifcontinued,pcntl_wexitstatus,pcntl_wtermsig,pcntl_wstopsig,pcntl_signal,pcntl_signal_get_handler,pcntl_signal_dispatch,pcntl_get_last_error,pcntl_strerror,pcntl_sigprocmask,pcntl_sigwaitinfo,pcntl_sigtimedwait,pcntl_exec,pcntl_getpriority,pcntl_setpriority,pcntl_async_signals,pcntl_unshare,
Upload Files
File: //proc/self/root/tmp/bewertemich-generate-hubs.mjs
import fs from 'node:fs/promises';
import path from 'node:path';
import { audienceHubs, featuredSeoLinks, seoHub, seoLandingCatalog } from '../src/modules/seo/catalog.js';

const publicDir = path.resolve('/home/bewertemich.de/web/public');
const manifestPath = path.join(publicDir, 'landing-media', 'seed-manifest.json');
const seoManifestPath = path.join(publicDir, 'seo-manifest.json');
const siteBase = 'https://bewertemich.de';

const escapeHtml = (value) => String(value || '')
  .replace(/&/g, '&')
  .replace(/</g, '&lt;')
  .replace(/>/g, '&gt;')
  .replace(/\"/g, '&quot;');

const escapeAttr = (value) => String(value || '')
  .replace(/&/g, '&amp;')
  .replace(/</g, '&lt;')
  .replace(/>/g, '&gt;')
  .replace(/\"/g, '&quot;')
  .replace(/'/g, '&#039;');

const seoTrackAttributes = (eventName, target = '', position = '') =>
  `data-seo-track-event="${escapeAttr(eventName)}" data-seo-track-target="${escapeAttr(target)}" data-seo-track-position="${escapeAttr(position)}"`;

const slugifyEntry = (slug) => slug.replace(/-/g, '_');

const renderFaqSchema = (page) => ({
  '@context': 'https://schema.org',
  '@type': 'FAQPage',
  mainEntity: (page.faqs || []).map((item) => ({
    '@type': 'Question',
    name: item.q,
    acceptedAnswer: {
      '@type': 'Answer',
      text: item.a
    }
  }))
});

const renderPageSchema = (page, canonical) => ({
  '@context': 'https://schema.org',
  '@type': 'Article',
  headline: page.heading,
  description: page.metaDescription,
  inLanguage: 'de-DE',
  about: page.tags || [],
  mainEntityOfPage: canonical,
  isAccessibleForFree: true,
  image: page.heroImage ? siteBase + page.heroImage : undefined,
  author: {
    '@type': 'Organization',
    name: 'bewertemich.de',
    url: siteBase
  },
  publisher: {
    '@type': 'Organization',
    name: 'bewertemich.de',
    url: siteBase
  }
});

const readManifest = async () => {
  const raw = await fs.readFile(manifestPath, 'utf8');
  return JSON.parse(raw);
};

const buildMaps = (manifest) => ({
  peopleBySlug: new Map((manifest.people || []).map((item) => [item.slug, item])),
  comparisonBySlug: new Map((manifest.comparisons || []).map((item) => [item.slug, item]))
});

const buildRelatedMap = () => {
  const map = new Map();
  for (const item of seoLandingCatalog) map.set(item.slug, item);
  for (const item of audienceHubs) map.set(item.slug, item);
  map.set(seoHub.slug, seoHub);
  return map;
};

const renderSampleCards = (page, peopleBySlug) => {
  const people = (page.sampleSlugs || [])
    .map((slug) => peopleBySlug.get(slug))
    .filter(Boolean)
    .slice(0, 3);
  return people.map((person) => `
    <article class="card photo-card">
      <a href="${person.url}">
        <img class="card-media" src="${person.imageUrl}" alt="${escapeHtml(person.title || person.displayName)}" width="480" height="600" loading="lazy" decoding="async" />
      </a>
      <div class="photo-card-body">
        <span class="eyebrow">${escapeHtml(person.displayName)} · ${escapeHtml(person.city)}</span>
        <h3>${escapeHtml(person.title || person.displayName)}</h3>
        <p>Fiktives Seed-Foto aus Deutschland, geeignet für realistische iPhone-Style-Tests.</p>
        <div class="metric-row">
          <span class="metric">Foto #${person.photoId}</span>
          <span class="metric">${person.variantPhotoId ? 'A/B möglich' : 'Einzelbild'}</span>
        </div>
      </div>
    </article>
  `).join('');
};

const renderRelatedLinks = (page, relatedMap) => {
  const items = (page.related || [])
    .map((slug) => relatedMap.get(slug))
    .filter(Boolean)
    .slice(0, 3);
  return items.map((item) => `
    <a class="related-link" href="/${item.slug}/" ${seoTrackAttributes('seo_related_click', item.slug, 'related-links')}>
      <strong>${escapeHtml(item.title || item.heading)}</strong>
      <span>${escapeHtml(item.subtitle || item.intro || '')}</span>
    </a>
  `).join('');
};

const trackingPixel = (slug, surface) => `
<script>
(function () {
  const pageSlug = ${JSON.stringify(slug)};
  const surface = ${JSON.stringify(surface)};
  const toJson = (value) => {
    try {
      return JSON.stringify(value);
    } catch {
      return '{}';
    }
  };

  const postEvent = (eventName, meta) => {
    const payload = {
      event: eventName,
      source: 'seo_static',
      surface,
      entryPoint: pageSlug,
      path: window.location.pathname,
      referrer: window.document.referrer || '',
      meta: meta || {}
    };
    const body = toJson(payload);
    const endpoint = '/api/public/events';

    if (navigator && typeof navigator.sendBeacon === 'function') {
      const blob = typeof Blob === 'function' ? new Blob([body], { type: 'application/json;charset=utf-8' }) : null;
      if (blob && navigator.sendBeacon(endpoint, blob)) return;
    }

    fetch(endpoint, {
      method: 'POST',
      headers: { 'content-type': 'application/json' },
      body,
      keepalive: true,
      credentials: 'same-origin'
    }).catch(() => {});
  };

  postEvent('seo_landing_view', { position: 'page-load' });

  for (const element of document.querySelectorAll('[data-seo-track-event]')) {
    element.addEventListener('click', () => {
      const eventName = element.getAttribute('data-seo-track-event') || 'seo_upload_click';
      postEvent(eventName, {
        target: element.getAttribute('data-seo-track-target') || '',
        position: element.getAttribute('data-seo-track-position') || ''
      });
    });
  }
})();
</script>`;

const renderPage = (page, maps, relatedMap) => {
  const canonical = `${siteBase}/${page.slug}/`;
  const comparison = maps.comparisonBySlug.get(page.compareSlug);
  const comparisonImage = comparison ? `${siteBase}/s/compare/${comparison.id}/og.jpg` : `${siteBase}${page.heroImage}`;
  const comparisonUrl = comparison ? `/compare/${comparison.id}` : `/upload?entry=seo_static_${slugifyEntry(page.slug)}`;
  const comparisonLabel = comparison ? (comparison.slug || 'compare') : 'upload';
  const sampleCards = renderSampleCards(page, maps.peopleBySlug);
  const relatedLinks = renderRelatedLinks(page, relatedMap);
  const schema = [renderPageSchema(page, canonical), renderFaqSchema(page)];
  return `<!doctype html>
<html lang="de">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width,initial-scale=1" />
    <title>${escapeHtml(page.title)} | bewertemich.de</title>
    <meta name="description" content="${escapeHtml(page.metaDescription)}" />
    <link rel="canonical" href="${canonical}" />
    <meta property="og:type" content="website" />
    <meta property="og:title" content="${escapeHtml(page.title)}" />
    <meta property="og:description" content="${escapeHtml(page.metaDescription)}" />
    <meta property="og:url" content="${canonical}" />
    <meta property="og:image" content="${siteBase}${page.heroImage}" />
    <meta name="twitter:card" content="summary_large_image" />
    <meta name="twitter:title" content="${escapeHtml(page.title)}" />
    <meta name="twitter:description" content="${escapeHtml(page.metaDescription)}" />
    <meta name="twitter:image" content="${siteBase}${page.heroImage}" />
    <link rel="stylesheet" href="/landing.css" />
    <script type="application/ld+json">${JSON.stringify(schema)}</script>
    ${trackingPixel(page.slug, 'seo_landing')}
  </head>
  <body>
    <main class="shell">
      <section class="hero hero-split">
        <div class="hero-copy">
          <div class="pill-row">
            <span class="pill">${escapeHtml(page.tags?.[0] || 'Profilbild')}</span>
            <span class="pill">Invite first</span>
            <span class="pill">DE Longtail</span>
          </div>
          <h1>${escapeHtml(page.heading)}</h1>
          <p>${escapeHtml(page.subtitle)}</p>
          <p>${escapeHtml(page.intro)}</p>
          <div class="actions">
            <a class="btn btn-primary" href="/upload?entry=seo_static_${slugifyEntry(page.slug)}" ${seoTrackAttributes('seo_upload_click', page.slug, 'hero')}>${escapeHtml(page.ctaLabel)}</a>
            <a class="btn btn-secondary" href="/top10">Öffentliche Rankings ansehen</a>
          </div>
        </div>
        <div class="hero-visual">
          <img class="hero-art" src="${page.heroImage}" alt="${escapeHtml(page.heroAlt)}" width="960" height="720" fetchpriority="high" decoding="async" />
          <div class="hero-note">
            <strong>${escapeHtml(page.proofTitle)}</strong>
            <p>${escapeHtml(page.proofText)}</p>
          </div>
        </div>
      </section>

      <section class="proof-strip">
        <article class="proof-card">
          <strong>Belastbare Rankings</strong>
          <p>Öffentliche Toplisten starten erst ab 5 Stimmen und 4 unabhängigen Votern.</p>
        </article>
        <article class="proof-card">
          <strong>Invite first</strong>
          <p>WhatsApp und Telegram liefern meist die schnellsten ersten Stimmen für ein klares Signal.</p>
        </article>
        <article class="proof-card">
          <strong>Vergleich statt Chaos</strong>
          <p>Wenn zwei Bilder eng sind, entscheidet der A/B-Modus schneller als jede Textdiskussion.</p>
        </article>
      </section>

      <section class="grid cols-3">
        ${page.bullets.map((item) => `
          <article class="card">
            <span class="eyebrow">${escapeHtml(item.kicker)}</span>
            <h2>${escapeHtml(item.title)}</h2>
            <p>${escapeHtml(item.text)}</p>
          </article>
        `).join('')}
      </section>

      <section class="section-header">
        <h2>Beispielbilder für ${escapeHtml(page.title)}</h2>
        <p>Die Beispielbilder stammen aus dem Seed für bewertemich.de und zeigen realistische Smartphone-Aufnahmen statt austauschbarer Stockfotos.</p>
      </section>
      <section class="grid cols-3">
        ${sampleCards}
      </section>

      <section class="media-panel">
        <img src="${comparisonImage}" alt="Share- oder Vergleichskarte für ${escapeHtml(page.title)}" width="960" height="720" loading="lazy" decoding="async" />
        <div class="card">
          <span class="eyebrow">Vergleichsmodus</span>
          <h2>Wenn zwei Bilder nahe beieinander liegen, gewinnt der direkte Test</h2>
          <p>Genau hier kippt die Entscheidung oft. Statt beide Fotos irgendwie okay zu finden, lässt du sie gegeneinander antreten und schaust auf den klaren Sieger.</p>
          <ol class="list">
            ${page.steps.map((item) => `<li><strong>${escapeHtml(item.title)}:</strong> ${escapeHtml(item.text)}</li>`).join('')}
          </ol>
          <div class="actions">
            <a class="btn btn-primary" href="${comparisonUrl}" ${seoTrackAttributes('seo_related_click', comparisonLabel, 'media-comparison')}>Beispiel ansehen</a>
            <a class="btn btn-secondary dark" href="/upload?entry=seo_static_${slugifyEntry(page.slug)}" ${seoTrackAttributes('seo_upload_click', page.slug, 'media-cta')}>Eigenes Foto testen</a>
          </div>
        </div>
      </section>

      <section class="card">
        <div class="section-header compact">
          <h2>FAQ zu ${escapeHtml(page.title)}</h2>
        </div>
        <div class="faq-list">
          ${(page.faqs || []).map((item) => `
            <article class="faq-item">
              <h3>${escapeHtml(item.q)}</h3>
              <p>${escapeHtml(item.a)}</p>
            </article>
          `).join('')}
        </div>
      </section>

      <section class="card">
        <div class="section-header compact">
          <h2>Verwandte Themen</h2>
          <p>Interne DE-Longtails für weitere Suchintentionen rund um Profilbild, Foto-Test und Vergleich.</p>
        </div>
        <div class="related-grid">
          ${relatedLinks}
        </div>
      </section>

      <section class="cta">
        <h2>${escapeHtml(page.heading)} direkt ausprobieren</h2>
        <p>Vom Suchintent direkt in den Flow: hochladen, Invite-Link teilen, Siegerbild übernehmen.</p>
        <div class="footer-actions">
          <a class="btn btn-primary" href="/upload?entry=seo_static_${slugifyEntry(page.slug)}" ${seoTrackAttributes('seo_upload_click', page.slug, 'footer')}>${escapeHtml(page.ctaLabel)}</a>
          <a class="btn btn-secondary" href="/profilbild-ratgeber/">Zum Ratgeber-Hub</a>
        </div>
      </section>

      <div class="footer">bewertemich.de · ${escapeHtml(page.title)} · Deutschland</div>
    </main>
  </body>
</html>`;
};

const renderAudienceHub = (hub, relatedMap) => {
  const canonical = `${siteBase}/${hub.slug}/`;
  const childPages = (hub.childSlugs || []).map((slug) => relatedMap.get(slug)).filter(Boolean);
  const schema = [
    {
      '@context': 'https://schema.org',
      '@type': 'CollectionPage',
      name: hub.heading,
      description: hub.intro,
      url: canonical,
      inLanguage: 'de-DE',
      image: siteBase + hub.heroImage,
      mainEntity: childPages.map((item, index) => ({
        '@type': 'ListItem',
        position: index + 1,
        name: item.title || item.heading,
        url: `${siteBase}/${item.slug}/`
      }))
    },
    {
      '@context': 'https://schema.org',
      '@type': 'BreadcrumbList',
      itemListElement: [
        { '@type': 'ListItem', position: 1, name: 'Start', item: siteBase + '/' },
        { '@type': 'ListItem', position: 2, name: seoHub.title, item: `${siteBase}/${seoHub.slug}/` },
        { '@type': 'ListItem', position: 3, name: hub.title, item: canonical }
      ]
    }
  ];
  return `<!doctype html>
<html lang="de">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width,initial-scale=1" />
    <title>${escapeHtml(hub.title)} | bewertemich.de</title>
    <meta name="description" content="${escapeHtml(hub.subtitle)}" />
    <link rel="canonical" href="${canonical}" />
    <meta property="og:type" content="website" />
    <meta property="og:title" content="${escapeHtml(hub.title)}" />
    <meta property="og:description" content="${escapeHtml(hub.subtitle)}" />
    <meta property="og:url" content="${canonical}" />
    <meta property="og:image" content="${siteBase}${hub.heroImage}" />
    <meta name="twitter:card" content="summary_large_image" />
    <meta name="twitter:title" content="${escapeHtml(hub.title)}" />
    <meta name="twitter:description" content="${escapeHtml(hub.subtitle)}" />
    <meta name="twitter:image" content="${siteBase}${hub.heroImage}" />
    <link rel="stylesheet" href="/landing.css" />
    <script type="application/ld+json">${JSON.stringify(schema)}</script>
    ${trackingPixel(hub.slug, 'audience_hub')}
  </head>
  <body>
    <main class="shell">
      <section class="hero hero-split">
        <div class="hero-copy">
          <div class="pill-row">
            <span class="pill">${escapeHtml(hub.accent)}</span>
            <span class="pill">Community-Test</span>
            <span class="pill">Upload-Route</span>
          </div>
          <h1>${escapeHtml(hub.heading)}</h1>
          <p>${escapeHtml(hub.subtitle)}</p>
          <p>${escapeHtml(hub.intro)}</p>
          <div class="actions">
            <a class="btn btn-primary" href="/upload?entry=${escapeAttr(hub.uploadEntry)}" ${seoTrackAttributes('seo_upload_click', hub.slug, 'audience-hero')}>${escapeHtml(hub.ctaLabel)}</a>
            <a class="btn btn-secondary" href="/top10">Community Top 10 ansehen</a>
          </div>
        </div>
        <div class="hero-visual">
          <img class="hero-art" src="${hub.heroImage}" alt="${escapeHtml(hub.heroAlt)}" width="960" height="720" fetchpriority="high" decoding="async" />
          <div class="hero-note">
            <strong>Direkt von der Suchintention in den Test</strong>
            <p>Dieser Hub bündelt passende Ratgeber, Beispiele und Upload-Routen, damit Besucher nicht in Text stecken bleiben.</p>
          </div>
        </div>
      </section>

      <section class="proof-strip">
        ${hub.proof.map((item) => `
          <article class="proof-card">
            <strong>${escapeHtml(item.title)}</strong>
            <p>${escapeHtml(item.text)}</p>
          </article>
        `).join('')}
      </section>

      <section class="section-header">
        <h2>Passende Tests und Ratgeber für ${escapeHtml(hub.accent)}</h2>
        <p>Starte mit dem konkretesten Thema oder lade direkt ein Foto hoch. Jede Unterseite führt wieder in Upload, Invite-Link und Community-Bewertung.</p>
      </section>
      <section class="hub-grid">
        ${childPages.map((item) => `
          <a class="hub-link" href="/${item.slug}/" ${seoTrackAttributes('seo_related_click', item.slug, 'audience-hub-grid')}>
            <span class="eyebrow">${escapeHtml((item.tags || [hub.accent])[0])}</span>
            <strong>${escapeHtml(item.title || item.heading)}</strong>
            <span>${escapeHtml(item.subtitle || item.intro || '')}</span>
          </a>
        `).join('')}
      </section>

      <section class="media-panel">
        <img src="${hub.heroImage}" alt="${escapeHtml(hub.heroAlt)}" width="960" height="720" loading="lazy" decoding="async" />
        <div class="card">
          <span class="eyebrow">Nächster Schritt</span>
          <h2>Der beste Hub-Besuch endet nicht beim Lesen, sondern beim Test</h2>
          <p>Bewertemich.de funktioniert als Kreislauf: Foto hochladen, 5 Kontakte einladen, Community-Stimmen sammeln und bei Unsicherheit eine zweite Variante testen.</p>
          <ol class="list">
            <li><strong>Foto auswählen:</strong> Nimm ein Bild, das du wirklich einsetzen würdest.</li>
            <li><strong>Invite-Link teilen:</strong> Direkte Kontakte liefern die schnellste erste Tendenz.</li>
            <li><strong>Signal nutzen:</strong> Gewinner übernehmen oder mit einer Alternative vergleichen.</li>
          </ol>
          <div class="actions">
            <a class="btn btn-primary" href="/upload?entry=${escapeAttr(hub.uploadEntry)}" ${seoTrackAttributes('seo_upload_click', hub.slug, 'audience-media')}>${escapeHtml(hub.ctaLabel)}</a>
            <a class="btn btn-secondary dark" href="/rate/random">Erst 5 Fotos mitbewerten</a>
          </div>
        </div>
      </section>

      <section class="card">
        <div class="section-header compact">
          <h2>Weitere Einstiege</h2>
          <p>Wenn du noch nicht sicher bist, welcher Kontext passt, starte über den zentralen Ratgeber oder die öffentliche Topliste.</p>
        </div>
        <div class="related-grid">
          <a class="related-link" href="/${seoHub.slug}/"><strong>Profilbild Ratgeber</strong><span>Alle Longtails und Entscheidungshilfen an einem Ort.</span></a>
          <a class="related-link" href="/bestes-profilbild-finden/"><strong>Bestes Profilbild finden</strong><span>Wenn du zwischen mehreren Bildern festhängst.</span></a>
          <a class="related-link" href="/top10"><strong>Community Top 10</strong><span>Öffentliche Rankings als Social-Proof-Einstieg.</span></a>
        </div>
      </section>

      <section class="cta">
        <h2>${escapeHtml(hub.ctaLabel)} und echte Stimmen sammeln</h2>
        <p>Vom Hub direkt in den Produkt-Loop: Upload, Invite-Link, Community-Feedback.</p>
        <div class="footer-actions">
          <a class="btn btn-primary" href="/upload?entry=${escapeAttr(hub.uploadEntry)}" ${seoTrackAttributes('seo_upload_click', hub.slug, 'audience-footer')}>Upload starten</a>
          <a class="btn btn-secondary" href="/profilbild-ratgeber/">Zum Ratgeber-Hub</a>
        </div>
      </section>
      <div class="footer">bewertemich.de · ${escapeHtml(hub.title)} · Deutschland</div>
    </main>
  </body>
</html>`;
};

const renderHubPage = (relatedMap) => {
  const canonical = `${siteBase}/${seoHub.slug}/`;
  const cards = seoLandingCatalog.map((item) => `
    <a class="hub-link" href="/${item.slug}/" ${seoTrackAttributes('seo_related_click', item.slug, 'hub-grid')}>
      <span class="eyebrow">${escapeHtml(item.tags?.[0] || 'Profilbild')}</span>
      <strong>${escapeHtml(item.title)}</strong>
      <span>${escapeHtml(item.subtitle)}</span>
    </a>
  `).join('');
  const schema = [{
    '@context': 'https://schema.org',
    '@type': 'CollectionPage',
    name: seoHub.heading,
    description: seoHub.intro,
    url: canonical
  }];
  return `<!doctype html>
<html lang="de">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width,initial-scale=1" />
    <title>${escapeHtml(seoHub.title)} | bewertemich.de</title>
    <meta name="description" content="${escapeHtml(seoHub.intro)}" />
    <link rel="canonical" href="${canonical}" />
    <meta property="og:type" content="website" />
    <meta property="og:title" content="${escapeHtml(seoHub.title)}" />
    <meta property="og:description" content="${escapeHtml(seoHub.intro)}" />
    <meta property="og:url" content="${canonical}" />
    <meta property="og:image" content="${siteBase}${seoHub.heroImage}" />
    <link rel="stylesheet" href="/landing.css" />
    <script type="application/ld+json">${JSON.stringify(schema)}</script>
    ${trackingPixel(seoHub.slug, 'seo_hub')}
  </head>
  <body>
    <main class="shell">
      <section class="hero hero-split">
        <div class="hero-copy">
          <div class="pill-row">
            <span class="pill">Hub</span>
            <span class="pill">DE-SEO</span>
            <span class="pill">Profilbild</span>
          </div>
          <h1>${escapeHtml(seoHub.heading)}</h1>
          <p>${escapeHtml(seoHub.subtitle)}</p>
          <p>${escapeHtml(seoHub.intro)}</p>
          <div class="actions">
            <a class="btn btn-primary" href="/upload?entry=seo_static_${slugifyEntry(seoHub.slug)}" ${seoTrackAttributes('seo_upload_click', seoHub.slug, 'hub-hero')}>Jetzt Foto testen</a>
            <a class="btn btn-secondary" href="/top10">Toplisten ansehen</a>
          </div>
        </div>
        <div class="hero-visual">
          <img class="hero-art" src="${seoHub.heroImage}" alt="${escapeHtml(seoHub.heroAlt)}" width="960" height="720" fetchpriority="high" decoding="async" />
          <div class="hero-note">
            <strong>Skalierbarer DE-Longtail-Hub</strong>
            <p>Alle relevanten Themen rund um Profilbilder, Dating-Fotos, Business-Portraits und Vergleiche laufen hier zusammen.</p>
          </div>
        </div>
      </section>
      <section class="section-header">
        <h2>Alle Ratgeberseiten</h2>
        <p>Von der Suchanfrage direkt in passende Tests und Vergleiche. Die vier Kernseiten bleiben prominent, der Rest deckt spezifische Longtails ab.</p>
      </section>
      <section class="hub-grid">
        ${cards}
      </section>
      <section class="card">
        <div class="section-header compact">
          <h2>Direkteinstiege</h2>
        </div>
        <div class="related-grid">
          ${featuredSeoLinks.map((item) => `
            <a class="related-link" href="/${item.slug}/" ${seoTrackAttributes('seo_related_click', item.slug, 'featured')}> 
              <strong>${escapeHtml(item.title)}</strong>
              <span>Zu diesem Thema direkt in die optimierte Landingpage springen.</span>
            </a>
          `).join('')}
        </div>
      </section>
      <section class="cta">
        <h2>Lieber direkt testen?</h2>
        <p>Dann überspringst du die Theorie und startest sofort mit deinem Profilbild oder Vergleich.</p>
        <div class="footer-actions">
          <a class="btn btn-primary" href="/upload?entry=seo_static_${slugifyEntry(seoHub.slug)}" ${seoTrackAttributes('seo_upload_click', seoHub.slug, 'hub-footer')}>Upload starten</a>
          <a class="btn btn-secondary" href="/top10">Toplisten ansehen</a>
        </div>
      </section>
      <div class="footer">bewertemich.de · ${escapeHtml(seoHub.title)} · Deutschland</div>
    </main>
  </body>
</html>`;
};

const renderSitemap = () => {
  const routes = [
    { path: '/', priority: '1.0' },
    { path: '/upload', priority: '0.9' },
    { path: '/top10', priority: '0.8' },
    { path: `/${seoHub.slug}/`, priority: '0.9', image: seoHub.heroImage, imageTitle: seoHub.title },
    ...audienceHubs.map((item) => ({ path: `/${item.slug}/`, priority: '0.9', image: item.heroImage, imageTitle: item.title })),
    ...seoLandingCatalog.map((item) => ({ path: `/${item.slug}/`, priority: '0.8', image: item.heroImage, imageTitle: item.title }))
  ];
  const today = new Date().toISOString().slice(0, 10);
  return `<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:image="http://www.google.com/schemas/sitemap-image/1.1">
${routes.map((route) => `  <url>
    <loc>${siteBase}${route.path}</loc>
    <lastmod>${today}</lastmod>
    <changefreq>weekly</changefreq>
    <priority>${route.priority}</priority>${route.image ? `
    <image:image>
      <image:loc>${siteBase}${route.image}</image:loc>
      <image:title>${escapeHtml(route.imageTitle || 'bewertemich.de')}</image:title>
    </image:image>` : ''}
  </url>`).join('\n')}
</urlset>`;
};

const writePage = async (slug, html) => {
  const dir = path.join(publicDir, slug);
  await fs.mkdir(dir, { recursive: true });
  await fs.writeFile(path.join(dir, 'index.html'), html, 'utf8');
};

const buildSeoManifest = () => ({
  generatedAt: new Date().toISOString(),
  siteBase,
  hub: { slug: seoHub.slug, title: seoHub.title, intro: seoHub.intro },
  audienceHubs: audienceHubs.map((hub) => ({ slug: hub.slug, title: hub.title, tags: hub.tags || [], childSlugs: hub.childSlugs || [] })),
  featuredLinks: featuredSeoLinks,
  landingPages: seoLandingCatalog.map((page) => ({
    slug: page.slug,
    title: page.title,
    tags: page.tags || [],
    faqCount: (page.faqs || []).length
  }))
});

const main = async () => {
  const manifest = await readManifest();
  const maps = buildMaps(manifest);
  const relatedMap = buildRelatedMap();
  for (const page of seoLandingCatalog) {
    await writePage(page.slug, renderPage(page, maps, relatedMap));
  }
  for (const hub of audienceHubs) {
    await writePage(hub.slug, renderAudienceHub(hub, relatedMap));
  }
  await writePage(seoHub.slug, renderHubPage(relatedMap));
  await fs.writeFile(path.join(publicDir, 'sitemap.xml'), renderSitemap(), 'utf8');
  await fs.writeFile(seoManifestPath, JSON.stringify(buildSeoManifest(), null, 2), 'utf8');
};

await main();