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: //tmp/generate-seo-pages.mjs
import fs from 'node:fs/promises';
import path from 'node:path';
import { 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
});

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);
  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)}" loading="lazy" />
      </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 fuer realistische iPhone-Style-Tests.</p>
        <div class="metric-row">
          <span class="metric">Foto #${person.photoId}</span>
          <span class="metric">${person.variantPhotoId ? 'A/B moeglich' : '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">Oeffentliche Rankings ansehen</a>
          </div>
        </div>
        <div class="hero-visual">
          <img class="hero-art" src="${page.heroImage}" alt="${escapeHtml(page.heroAlt)}" />
          <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>Oeffentliche Toplisten starten erst ab 5 Stimmen und 4 unabhaengigen Votern.</p>
        </article>
        <article class="proof-card">
          <strong>Invite first</strong>
          <p>WhatsApp und Telegram liefern meist die schnellsten ersten Stimmen fuer 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 fuer ${escapeHtml(page.title)}</h2>
        <p>Die Beispielbilder stammen aus dem Seed fuer 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 fuer ${escapeHtml(page.title)}" loading="lazy" />
        <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, laesst 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 fuer 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 uebernehmen.</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 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)}" />
          <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 ueberspringst 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 = [
    '/',
    '/upload',
    '/top10',
    `/${seoHub.slug}/`,
    ...seoLandingCatalog.map((item) => `/${item.slug}/`)
  ];
  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">
${routes.map((route) => `  <url>
    <loc>${siteBase}${route}</loc>
    <lastmod>${today}</lastmod>
  </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 },
  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));
  }
  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();