- Introduced a new route for serving ads.txt, providing necessary information for ad verification by Google. - This addition enhances compliance with advertising standards and improves the site's ad management capabilities.
559 lines
24 KiB
JavaScript
559 lines
24 KiB
JavaScript
import { readFileSync, existsSync } from 'fs';
|
||
import { join, resolve } from 'path';
|
||
import { loadFeedback } from './feedback-store.js';
|
||
|
||
const SITE_URL = 'https://www.ypchat.net';
|
||
const DEFAULT_IMAGE = `${SITE_URL}/static/favicon.png`;
|
||
const SEO_LOCALES = [
|
||
{ code: 'de', label: 'Deutsch' },
|
||
{ code: 'en', label: 'English' },
|
||
{ code: 'fr', label: 'Francais' },
|
||
{ code: 'es', label: 'Espanol' },
|
||
{ code: 'it', label: 'Italiano' },
|
||
{ code: 'ja', label: 'Japanese' },
|
||
{ code: 'zh', label: 'Chinese' },
|
||
{ code: 'th', label: 'Thai' },
|
||
{ code: 'tl', label: 'Tagalog' }
|
||
];
|
||
const LOCALE_SEO_META = {
|
||
de: {
|
||
title: 'SingleChat: Kostenloser Single Chat, privat & anonym',
|
||
description: 'Kostenloser Single Chat für private und anonyme Gespräche. Lerne neue Kontakte kennen und teile Bilder sicher online.',
|
||
keywords: 'single chat, kostenloser chat, privat chatten, anonym chat, free chat, private chat, anonymous chat, online chat'
|
||
},
|
||
en: {
|
||
title: 'SingleChat: Free Private & Anonymous Single Chat',
|
||
description: 'Free single chat for private and anonymous conversations. Meet new people and share images safely online.',
|
||
keywords: 'single chat, free chat, private chat, anonymous chat, online chat, meet singles'
|
||
},
|
||
fr: {
|
||
title: 'SingleChat: Chat célibataire gratuit, privé et anonyme',
|
||
description: 'Chat célibataire gratuit pour des conversations privées et anonymes. Rencontrez de nouvelles personnes en toute sécurité.',
|
||
keywords: 'chat célibataire, chat gratuit, chat privé, chat anonyme, rencontre en ligne'
|
||
},
|
||
es: {
|
||
title: 'SingleChat: Chat gratis, privado y anónimo',
|
||
description: 'Chat gratis para solteros con conversaciones privadas y anónimas. Conoce gente nueva y comparte imágenes de forma segura.',
|
||
keywords: 'chat gratis, chat privado, chat anónimo, chat para solteros, conocer gente'
|
||
},
|
||
it: {
|
||
title: 'SingleChat: Chat single gratis, privata e anonima',
|
||
description: 'Chat single gratis per conversazioni private e anonime. Conosci nuove persone e condividi immagini in sicurezza.',
|
||
keywords: 'chat single, chat gratis, chat privata, chat anonima, incontri online'
|
||
},
|
||
ja: {
|
||
title: 'SingleChat: 無料・匿名・プライベートのシングルチャット',
|
||
description: '無料で使えるシングルチャット。匿名かつプライベートに会話でき、画像共有も安全です。',
|
||
keywords: 'シングルチャット, 無料チャット, 匿名チャット, プライベートチャット, オンラインチャット'
|
||
},
|
||
zh: {
|
||
title: 'SingleChat:免费、私密、匿名的单身聊天',
|
||
description: '免费单身聊天,支持私密和匿名交流,安全分享图片并结识新朋友。',
|
||
keywords: '单身聊天, 免费聊天, 私密聊天, 匿名聊天, 在线聊天'
|
||
},
|
||
th: {
|
||
title: 'SingleChat: แชตคนโสดฟรี แบบส่วนตัวและไม่ระบุตัวตน',
|
||
description: 'แชตคนโสดฟรี สำหรับการสนทนาแบบส่วนตัวและไม่ระบุตัวตน พบผู้คนใหม่ ๆ และแชร์รูปได้อย่างปลอดภัย',
|
||
keywords: 'แชตคนโสด, แชตฟรี, แชตส่วนตัว, แชตไม่ระบุตัวตน, แชตออนไลน์'
|
||
},
|
||
tl: {
|
||
title: 'SingleChat: Libreng private at anonymous na single chat',
|
||
description: 'Libreng single chat para sa private at anonymous na usapan. Kumilala ng bagong tao at magbahagi ng larawan nang ligtas.',
|
||
keywords: 'single chat, libreng chat, private chat, anonymous chat, online chat'
|
||
}
|
||
};
|
||
|
||
const seoData = {
|
||
'/': {
|
||
title: 'SingleChat: Kostenloser Single Chat, privat & anonym',
|
||
description: 'Kostenloser Single Chat für private und anonyme Gespräche. Lerne neue Kontakte kennen und teile Bilder sicher online.',
|
||
keywords: 'single chat, kostenloser chat, privat chatten, anonym chat, free chat, private chat, anonymous chat, online chat',
|
||
ogTitle: 'SingleChat: Kostenloser Single Chat, privat & anonym',
|
||
ogDescription: 'Kostenlos chatten, privat bleiben und neue Kontakte kennenlernen - mit sicherem Bildaustausch.',
|
||
ogType: 'website',
|
||
ogUrl: `${SITE_URL}/`,
|
||
ogImage: DEFAULT_IMAGE,
|
||
robots: 'index, follow, max-image-preview:large, max-snippet:-1, max-video-preview:-1',
|
||
schema: {
|
||
'@context': 'https://schema.org',
|
||
'@type': 'WebSite',
|
||
name: 'SingleChat',
|
||
url: `${SITE_URL}/`,
|
||
description: 'Kostenloser Single Chat für private und anonyme Gespräche. Lerne neue Kontakte kennen und tausche Bilder sicher aus.',
|
||
inLanguage: 'de-DE'
|
||
}
|
||
},
|
||
'/partners': {
|
||
title: 'Partner für Single Chat & Community - SingleChat',
|
||
description: 'Partnerseiten rund um Single Chat, Community und Online-Kontakte. Entdecke weitere Angebote und hilfreiche Ressourcen.',
|
||
keywords: 'single chat partner, chat community, kontaktseiten, single-chat links, online dating chat',
|
||
ogTitle: 'Partner für Single Chat & Community - SingleChat',
|
||
ogDescription: 'Befreundete Seiten und Ressourcen rund um Chat, Kontakte und Community.',
|
||
ogType: 'website',
|
||
ogUrl: `${SITE_URL}/partners`,
|
||
ogImage: DEFAULT_IMAGE,
|
||
robots: 'index, follow, max-image-preview:large, max-snippet:-1, max-video-preview:-1',
|
||
schema: {
|
||
'@context': 'https://schema.org',
|
||
'@type': 'CollectionPage',
|
||
name: 'Partner - SingleChat',
|
||
url: `${SITE_URL}/partners`,
|
||
description: 'Partnerseiten rund um Single Chat, Community und Online-Kontakte. Entdecke weitere Angebote und hilfreiche Ressourcen.',
|
||
isPartOf: {
|
||
'@type': 'WebSite',
|
||
name: 'SingleChat',
|
||
url: `${SITE_URL}/`
|
||
},
|
||
inLanguage: 'de-DE'
|
||
}
|
||
},
|
||
'/feedback': {
|
||
title: 'Feedback zur Chat-Plattform - SingleChat',
|
||
description: 'Öffentliches Feedback zu SingleChat: Meinungen, Vorschläge und Erfahrungsberichte für einen besseren privaten Chat.',
|
||
keywords: 'chat feedback, single chat erfahrungen, rückmeldung chat, verbesserungsvorschläge',
|
||
ogTitle: 'Feedback zur Chat-Plattform - SingleChat',
|
||
ogDescription: 'Teile deine Erfahrungen und Verbesserungsvorschläge für SingleChat.',
|
||
ogType: 'website',
|
||
ogUrl: `${SITE_URL}/feedback`,
|
||
ogImage: DEFAULT_IMAGE,
|
||
robots: 'index, follow, max-image-preview:large, max-snippet:-1, max-video-preview:-1',
|
||
schema: {
|
||
'@context': 'https://schema.org',
|
||
'@type': 'CollectionPage',
|
||
name: 'Feedback - SingleChat',
|
||
url: `${SITE_URL}/feedback`,
|
||
description: 'Öffentliches Feedback zu SingleChat: Meinungen, Vorschläge und Erfahrungsberichte für einen besseren privaten Chat.',
|
||
isPartOf: {
|
||
'@type': 'WebSite',
|
||
name: 'SingleChat',
|
||
url: `${SITE_URL}/`
|
||
},
|
||
inLanguage: 'de-DE'
|
||
}
|
||
},
|
||
'/faq': {
|
||
title: 'FAQ: Kostenlos, privat und anonym chatten - SingleChat',
|
||
description: 'FAQ zum kostenlosen Single Chat: anonym chatten, Privatsphäre schützen, Bilder sicher teilen und Nutzer blockieren.',
|
||
keywords: 'single chat faq, kostenlos chatten, anonym chatten, privater chat, safe chat',
|
||
ogTitle: 'FAQ: Kostenlos, privat und anonym chatten - SingleChat',
|
||
ogDescription: 'Antworten auf Fragen zu Sicherheit, Privatsphäre und Funktionen im Single Chat.',
|
||
ogType: 'website',
|
||
ogUrl: `${SITE_URL}/faq`,
|
||
ogImage: DEFAULT_IMAGE,
|
||
robots: 'index, follow, max-image-preview:large, max-snippet:-1, max-video-preview:-1',
|
||
schema: {
|
||
'@context': 'https://schema.org',
|
||
'@type': 'FAQPage',
|
||
name: 'FAQ - SingleChat',
|
||
url: `${SITE_URL}/faq`,
|
||
description: 'FAQ zum kostenlosen Single Chat: anonym chatten, Privatsphäre schützen, Bilder sicher teilen und Nutzer blockieren.',
|
||
isPartOf: {
|
||
'@type': 'WebSite',
|
||
name: 'SingleChat',
|
||
url: `${SITE_URL}/`
|
||
},
|
||
inLanguage: 'de-DE'
|
||
}
|
||
},
|
||
'/regeln': {
|
||
title: 'Chat-Regeln für sicheren Single Chat - SingleChat',
|
||
description: 'Regeln für respektvollen, privaten und sicheren Single Chat. Hinweise zu Verhalten, Spam und verbotenen Inhalten.',
|
||
keywords: 'chat regeln, single chat regeln, sicher chatten, spam vermeiden, community richtlinien',
|
||
ogTitle: 'Chat-Regeln für sicheren Single Chat - SingleChat',
|
||
ogDescription: 'Unsere Richtlinien für respektvolle und sichere Gespräche im Chat.',
|
||
ogType: 'website',
|
||
ogUrl: `${SITE_URL}/regeln`,
|
||
ogImage: DEFAULT_IMAGE,
|
||
robots: 'index, follow, max-image-preview:large, max-snippet:-1, max-video-preview:-1',
|
||
schema: {
|
||
'@context': 'https://schema.org',
|
||
'@type': 'WebPage',
|
||
name: 'Regeln - SingleChat',
|
||
url: `${SITE_URL}/regeln`,
|
||
description: 'Regeln für respektvollen, privaten und sicheren Single Chat. Hinweise zu Verhalten, Spam und verbotenen Inhalten.',
|
||
isPartOf: {
|
||
'@type': 'WebSite',
|
||
name: 'SingleChat',
|
||
url: `${SITE_URL}/`
|
||
},
|
||
inLanguage: 'de-DE'
|
||
}
|
||
},
|
||
'/sicherheit': {
|
||
title: 'Sicherheit & Privatsphäre im privaten Chat - SingleChat',
|
||
description: 'Sicherheitsseite für privaten und anonymen Chat: Privatsphäre, Schutz vor Spam, Blockieren und Melden.',
|
||
keywords: 'privatsphäre chat, anonym chat sicherheit, blockieren melden, private chat safety',
|
||
ogTitle: 'Sicherheit & Privatsphäre im privaten Chat - SingleChat',
|
||
ogDescription: 'So schützt du deine Daten und chattest sicher und anonym.',
|
||
ogType: 'website',
|
||
ogUrl: `${SITE_URL}/sicherheit`,
|
||
ogImage: DEFAULT_IMAGE,
|
||
robots: 'index, follow, max-image-preview:large, max-snippet:-1, max-video-preview:-1',
|
||
schema: {
|
||
'@context': 'https://schema.org',
|
||
'@type': 'WebPage',
|
||
name: 'Sicherheit & Privatsphäre - SingleChat',
|
||
url: `${SITE_URL}/sicherheit`,
|
||
description: 'Sicherheitsseite für privaten und anonymen Chat: Privatsphäre, Schutz vor Spam, Blockieren und Melden.',
|
||
isPartOf: {
|
||
'@type': 'WebSite',
|
||
name: 'SingleChat',
|
||
url: `${SITE_URL}/`
|
||
},
|
||
inLanguage: 'de-DE'
|
||
}
|
||
},
|
||
'/ratgeber': {
|
||
title: 'Ratgeber fuer privaten Single Chat - SingleChat',
|
||
description: 'Praxisnahe Ratgeberartikel zu Chat-Einstieg, Profiloptimierung, Sicherheit und Red Flags im Online-Chat.',
|
||
keywords: 'chat ratgeber, single chat tipps, profil tipps, sicher chatten, online chat red flags',
|
||
ogTitle: 'Ratgeber fuer privaten Single Chat - SingleChat',
|
||
ogDescription: 'Hilfreiche Leitfaeden fuer bessere Gespraeche und mehr Sicherheit im Chat.',
|
||
ogType: 'website',
|
||
ogUrl: `${SITE_URL}/ratgeber`,
|
||
ogImage: DEFAULT_IMAGE,
|
||
robots: 'index, follow, max-image-preview:large, max-snippet:-1, max-video-preview:-1',
|
||
schema: {
|
||
'@context': 'https://schema.org',
|
||
'@type': 'CollectionPage',
|
||
name: 'Ratgeber - SingleChat',
|
||
url: `${SITE_URL}/ratgeber`,
|
||
description: 'Ratgeber mit Tipps zu erster Nachricht, Profilgestaltung, Datenschutz und sicheren Gespraechen im Single Chat.',
|
||
inLanguage: 'de-DE'
|
||
}
|
||
},
|
||
'/ratgeber/erste-nachricht': {
|
||
title: 'Die erste Nachricht im Chat - Tipps & Beispiele',
|
||
description: 'So gelingt die erste Nachricht im Single Chat: konkrete Beispiele, typische Fehler und einfache Vorlagen.',
|
||
keywords: 'erste nachricht chat, anschreiben tipps, chat beispiele, single chat einstieg',
|
||
ogTitle: 'Die erste Nachricht im Chat - Tipps & Beispiele',
|
||
ogDescription: 'Konkrete Beispiele fuer einen natuerlichen und respektvollen Chat-Einstieg.',
|
||
ogType: 'article',
|
||
ogUrl: `${SITE_URL}/ratgeber/erste-nachricht`,
|
||
ogImage: DEFAULT_IMAGE,
|
||
robots: 'index, follow, max-image-preview:large, max-snippet:-1, max-video-preview:-1',
|
||
schema: null
|
||
},
|
||
'/ratgeber/profil-tipps': {
|
||
title: 'Profil verbessern fuer bessere Chats - SingleChat',
|
||
description: 'Verbessere dein Chat-Profil mit klaren, ehrlichen Angaben und starte leichter passende Gespraeche.',
|
||
keywords: 'profil tipps chat, online profil verbessern, single chat profil',
|
||
ogTitle: 'Profil verbessern fuer bessere Chats - SingleChat',
|
||
ogDescription: 'Einfache Schritte fuer ein besseres Profil und passendere Unterhaltungen.',
|
||
ogType: 'article',
|
||
ogUrl: `${SITE_URL}/ratgeber/profil-tipps`,
|
||
ogImage: DEFAULT_IMAGE,
|
||
robots: 'index, follow, max-image-preview:large, max-snippet:-1, max-video-preview:-1',
|
||
schema: null
|
||
},
|
||
'/ratgeber/sicher-chatten': {
|
||
title: 'Sicher chatten und Daten schuetzen - SingleChat',
|
||
description: 'Datenschutz-Tipps fuer anonymes Chatten: Welche Informationen du teilen kannst und welche nicht.',
|
||
keywords: 'sicher chatten, datenschutz chat, anonym chat tipps',
|
||
ogTitle: 'Sicher chatten und Daten schuetzen - SingleChat',
|
||
ogDescription: 'Praktische Regeln fuer mehr Sicherheit und Privatsphaere im Online-Chat.',
|
||
ogType: 'article',
|
||
ogUrl: `${SITE_URL}/ratgeber/sicher-chatten`,
|
||
ogImage: DEFAULT_IMAGE,
|
||
robots: 'index, follow, max-image-preview:large, max-snippet:-1, max-video-preview:-1',
|
||
schema: null
|
||
},
|
||
'/ratgeber/red-flags': {
|
||
title: 'Red Flags im Online-Chat erkennen - SingleChat',
|
||
description: 'Warnzeichen bei Spam und Manipulation im Chat erkennen und richtig reagieren.',
|
||
keywords: 'red flags chat, online chat sicherheit, betrug chat erkennen',
|
||
ogTitle: 'Red Flags im Online-Chat erkennen - SingleChat',
|
||
ogDescription: 'Fruehe Warnsignale im Chat erkennen und sich wirksam schuetzen.',
|
||
ogType: 'article',
|
||
ogUrl: `${SITE_URL}/ratgeber/red-flags`,
|
||
ogImage: DEFAULT_IMAGE,
|
||
robots: 'index, follow, max-image-preview:large, max-snippet:-1, max-video-preview:-1',
|
||
schema: null
|
||
}
|
||
};
|
||
|
||
for (const locale of SEO_LOCALES) {
|
||
const meta = LOCALE_SEO_META[locale.code] || LOCALE_SEO_META.de;
|
||
seoData[`/${locale.code}`] = {
|
||
title: meta.title,
|
||
description: meta.description,
|
||
keywords: meta.keywords,
|
||
ogTitle: meta.title,
|
||
ogDescription: meta.description,
|
||
ogType: 'website',
|
||
ogUrl: `${SITE_URL}/${locale.code}`,
|
||
ogImage: DEFAULT_IMAGE,
|
||
robots: 'index, follow, max-image-preview:large, max-snippet:-1, max-video-preview:-1',
|
||
schema: {
|
||
'@context': 'https://schema.org',
|
||
'@type': 'WebSite',
|
||
name: 'SingleChat',
|
||
url: `${SITE_URL}/${locale.code}`,
|
||
description: meta.description,
|
||
inLanguage: locale.code
|
||
}
|
||
};
|
||
}
|
||
|
||
function buildSitemapXml() {
|
||
const currentDate = new Date().toISOString().split('T')[0];
|
||
const urls = Object.entries(seoData)
|
||
.map(([route, meta]) => {
|
||
const priority = route === '/' ? '1.0' : '0.8';
|
||
const changefreq = route === '/' ? 'daily' : 'weekly';
|
||
return ` <url>
|
||
<loc>${meta.ogUrl}</loc>
|
||
<lastmod>${currentDate}</lastmod>
|
||
<changefreq>${changefreq}</changefreq>
|
||
<priority>${priority}</priority>
|
||
</url>`;
|
||
})
|
||
.join('\n');
|
||
|
||
return `<?xml version="1.0" encoding="UTF-8"?>
|
||
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
||
${urls}
|
||
</urlset>`;
|
||
}
|
||
|
||
function escapeHtml(value = '') {
|
||
return String(value)
|
||
.replace(/&/g, '&')
|
||
.replace(/"/g, '"')
|
||
.replace(/</g, '<')
|
||
.replace(/>/g, '>');
|
||
}
|
||
|
||
function upsertMetaTag(html, name, content, attribute = 'name') {
|
||
const escapedContent = escapeHtml(content);
|
||
const regex = new RegExp(`<meta\\s+${attribute}="${name}"[^>]*>`, 'g');
|
||
const tag = `<meta ${attribute}="${name}" content="${escapedContent}">`;
|
||
|
||
if (regex.test(html)) {
|
||
return html.replace(regex, tag);
|
||
}
|
||
|
||
return html.replace('</head>', ` ${tag}\n</head>`);
|
||
}
|
||
|
||
function upsertLinkTag(html, rel, href) {
|
||
const escapedHref = escapeHtml(href);
|
||
const regex = new RegExp(`<link\\s+rel="${rel}"[^>]*>`, 'g');
|
||
const tag = `<link rel="${rel}" href="${escapedHref}">`;
|
||
|
||
if (regex.test(html)) {
|
||
return html.replace(regex, tag);
|
||
}
|
||
|
||
return html.replace('</head>', ` ${tag}\n</head>`);
|
||
}
|
||
|
||
function upsertJsonLd(html, schema) {
|
||
const tag = schema
|
||
? `<script type="application/ld+json" id="seo-json-ld">${JSON.stringify(schema)}</script>`
|
||
: '<script type="application/ld+json" id="seo-json-ld"></script>';
|
||
|
||
if (html.includes('id="seo-json-ld"')) {
|
||
return html.replace(/<script type="application\/ld\+json" id="seo-json-ld">.*?<\/script>/s, tag);
|
||
}
|
||
|
||
return html.replace('</head>', ` ${tag}\n</head>`);
|
||
}
|
||
|
||
function upsertHreflangLinks(html, route) {
|
||
const cleaned = html.replace(/<link\s+rel="alternate"\s+hreflang="[^"]+"\s+href="[^"]*"\s*>\n?/g, '');
|
||
const links = SEO_LOCALES.map(
|
||
(locale) => ` <link rel="alternate" hreflang="${locale.code}" href="${SITE_URL}/${locale.code}">`
|
||
);
|
||
links.push(` <link rel="alternate" hreflang="x-default" href="${SITE_URL}${route}">`);
|
||
return cleaned.replace('</head>', `${links.join('\n')}\n</head>`);
|
||
}
|
||
|
||
function sanitizeLocalizedHtml(input = '') {
|
||
return String(input)
|
||
.replace(/<script[\s\S]*?>[\s\S]*?<\/script>/gi, '')
|
||
.trim();
|
||
}
|
||
|
||
function loadLocaleSeoSections(__dirname, localeCode) {
|
||
const localesDir = join(__dirname, '../client/src/i18n/locales');
|
||
const filePath = join(localesDir, `${localeCode}.json`);
|
||
if (!existsSync(filePath)) return { welcome: '', intro: '' };
|
||
try {
|
||
const parsed = JSON.parse(readFileSync(filePath, 'utf-8'));
|
||
return {
|
||
welcome: sanitizeLocalizedHtml(parsed.welcome || ''),
|
||
intro: sanitizeLocalizedHtml(parsed.introduction || '')
|
||
};
|
||
} catch (error) {
|
||
console.warn(`[SEO] Locale konnte nicht gelesen werden (${localeCode}): ${error.message}`);
|
||
return { welcome: '', intro: '' };
|
||
}
|
||
}
|
||
|
||
function buildLocalizedLandingContent(route, __dirname) {
|
||
const localeCode = route.replace('/', '') || 'de';
|
||
const locale = SEO_LOCALES.find((entry) => entry.code === localeCode) || SEO_LOCALES[0];
|
||
const sections = loadLocaleSeoSections(__dirname, locale.code);
|
||
if (!sections.welcome && !sections.intro) return '';
|
||
|
||
return `<section lang="${escapeHtml(locale.code)}" style="max-width:960px;margin:24px auto;padding:0 16px;">
|
||
<h2 style="font:600 28px/1.15 sans-serif;color:#18201b;margin:0 0 12px;">${escapeHtml(locale.label)}</h2>
|
||
${sections.welcome}
|
||
${sections.intro}
|
||
</section>`;
|
||
}
|
||
|
||
function generateHTML(route, meta, __dirname) {
|
||
const distIndexPath = join(__dirname, '../docroot/dist/index.html');
|
||
|
||
if (!existsSync(distIndexPath)) {
|
||
console.error('WARNUNG: Gebaute index.html nicht gefunden:', distIndexPath);
|
||
return null;
|
||
}
|
||
|
||
let html = readFileSync(distIndexPath, 'utf-8');
|
||
|
||
html = html.replace(/<title>.*?<\/title>/, `<title>${escapeHtml(meta.title)}</title>`);
|
||
html = upsertMetaTag(html, 'description', meta.description);
|
||
html = upsertMetaTag(html, 'keywords', meta.keywords);
|
||
html = upsertMetaTag(html, 'robots', meta.robots);
|
||
html = upsertMetaTag(html, 'theme-color', '#2f6f46');
|
||
|
||
html = upsertMetaTag(html, 'og:title', meta.ogTitle, 'property');
|
||
html = upsertMetaTag(html, 'og:description', meta.ogDescription, 'property');
|
||
html = upsertMetaTag(html, 'og:type', meta.ogType, 'property');
|
||
html = upsertMetaTag(html, 'og:url', meta.ogUrl, 'property');
|
||
html = upsertMetaTag(html, 'og:image', meta.ogImage, 'property');
|
||
html = upsertMetaTag(html, 'og:site_name', 'SingleChat', 'property');
|
||
html = upsertMetaTag(html, 'og:locale', 'de_DE', 'property');
|
||
|
||
html = upsertMetaTag(html, 'twitter:card', 'summary_large_image');
|
||
html = upsertMetaTag(html, 'twitter:title', meta.ogTitle);
|
||
html = upsertMetaTag(html, 'twitter:description', meta.ogDescription);
|
||
html = upsertMetaTag(html, 'twitter:image', meta.ogImage);
|
||
|
||
html = upsertLinkTag(html, 'canonical', meta.ogUrl);
|
||
html = upsertJsonLd(html, meta.schema);
|
||
html = upsertHreflangLinks(html, route);
|
||
|
||
if (route === '/') {
|
||
const deLanding = buildLocalizedLandingContent('/de', __dirname);
|
||
if (deLanding) {
|
||
html = html.replace('<div id="app"></div>', `<div id="app">${deLanding}</div>`);
|
||
}
|
||
}
|
||
|
||
if (SEO_LOCALES.some((locale) => `/${locale.code}` === route)) {
|
||
const localizedLanding = buildLocalizedLandingContent(route, __dirname);
|
||
if (localizedLanding) {
|
||
html = html.replace('<div id="app"></div>', `<div id="app">${localizedLanding}</div>`);
|
||
}
|
||
}
|
||
|
||
if (route === '/feedback') {
|
||
const feedbackItems = loadFeedback(__dirname)
|
||
.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt))
|
||
.slice(0, 20);
|
||
|
||
const feedbackMarkup = feedbackItems.length > 0
|
||
? feedbackItems.map((item) => {
|
||
const metaLine = [item.country, item.age, item.gender].filter(Boolean).join(' · ');
|
||
return `<article style="border:1px solid #d7dfd9;border-radius:12px;padding:14px 16px;margin-bottom:12px;background:#fff;">
|
||
<strong style="display:block;color:#18201b;">${escapeHtml(item.name || 'Anonym')}</strong>
|
||
${metaLine ? `<div style="font-size:12px;color:#637067;margin-top:4px;">${escapeHtml(metaLine)}</div>` : ''}
|
||
<div style="font-size:12px;color:#637067;margin-top:4px;">${escapeHtml(new Date(item.createdAt).toLocaleString('de-DE'))}</div>
|
||
<p style="margin-top:10px;color:#2c362f;white-space:pre-wrap;">${escapeHtml(item.comment)}</p>
|
||
</article>`;
|
||
}).join('\n')
|
||
: '<p>Noch kein Feedback vorhanden.</p>';
|
||
|
||
const preview = `<section style="max-width:960px;margin:24px auto;padding:0 16px;">
|
||
<h2 style="font:600 28px/1.15 sans-serif;color:#18201b;margin:0 0 10px;">Feedback zu SingleChat</h2>
|
||
<p style="font:400 15px/1.5 sans-serif;color:#4f5d54;margin:0 0 18px;">Oeffentliche Rueckmeldungen und Verbesserungsvorschlaege.</p>
|
||
${feedbackMarkup}
|
||
</section>`;
|
||
|
||
html = html.replace('<div id="app"></div>', `<div id="app">${preview}</div>`);
|
||
}
|
||
|
||
return html;
|
||
}
|
||
|
||
export function setupSEORoutes(app, __dirname) {
|
||
const IS_PRODUCTION = process.env.NODE_ENV === 'production';
|
||
|
||
if (IS_PRODUCTION) {
|
||
const distIndexPath = resolve(__dirname, '../docroot/dist/index.html');
|
||
|
||
Object.entries(seoData).forEach(([route, meta]) => {
|
||
app.get(route, (req, res) => {
|
||
const html = generateHTML(route, meta, __dirname);
|
||
if (html) {
|
||
// Zusätzliches kanonisches Signal (neben link rel= im HTML) – hilft Google bei der Zuordnung.
|
||
res.set('Link', `<${meta.ogUrl}>; rel="canonical"`);
|
||
res.send(html);
|
||
return;
|
||
}
|
||
|
||
if (existsSync(distIndexPath)) {
|
||
res.set('Link', `<${meta.ogUrl}>; rel="canonical"`);
|
||
res.sendFile(distIndexPath);
|
||
return;
|
||
}
|
||
|
||
console.error('FEHLER: Gebaute index.html nicht gefunden:', distIndexPath);
|
||
res.status(500).send('Gebaute index.html nicht gefunden. Bitte führe "npm run build" aus.');
|
||
});
|
||
});
|
||
}
|
||
|
||
app.get('/robots.txt', (req, res) => {
|
||
const robotsTxt = `User-agent: *
|
||
Allow: /
|
||
Allow: /partners
|
||
Allow: /feedback
|
||
Allow: /faq
|
||
Allow: /regeln
|
||
Allow: /sicherheit
|
||
Allow: /ratgeber
|
||
Allow: /ratgeber/erste-nachricht
|
||
Allow: /ratgeber/profil-tipps
|
||
Allow: /ratgeber/sicher-chatten
|
||
Allow: /ratgeber/red-flags
|
||
Disallow: /api/
|
||
Disallow: /static/logs/
|
||
Disallow: /mockup-redesign
|
||
|
||
Sitemap: ${SITE_URL}/sitemap.xml
|
||
`;
|
||
res.type('text/plain');
|
||
res.send(robotsTxt);
|
||
});
|
||
|
||
app.get('/ads.txt', (req, res) => {
|
||
res.type('text/plain');
|
||
res.send('google.com, pub-1104166651501135, DIRECT, f08c47fec0942fa0\n');
|
||
});
|
||
|
||
app.get('/sitemap.xml', (req, res) => {
|
||
try {
|
||
const sitemap = buildSitemapXml();
|
||
// Stabilere Auslieferung fuer Crawler und Reverse-Proxy-Caches.
|
||
res.set('Cache-Control', 'public, max-age=300, s-maxage=300, stale-while-revalidate=600');
|
||
res.type('application/xml');
|
||
res.status(200).send(sitemap);
|
||
} catch (error) {
|
||
console.error('[SEO] Fehler beim Generieren der sitemap.xml:', error);
|
||
// Fallback: niemals 500 fuer die Sitemap ausliefern.
|
||
const fallback = `<?xml version="1.0" encoding="UTF-8"?>
|
||
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
||
<url><loc>${SITE_URL}/</loc></url>
|
||
</urlset>`;
|
||
res.set('Cache-Control', 'no-store');
|
||
res.type('application/xml');
|
||
res.status(200).send(fallback);
|
||
}
|
||
});
|
||
}
|