Files
singlechat/server/routes-seo.js
Torsten Schulz (local) 0fcc6878bd Add multilingual SEO metadata and enhance content descriptions
- Introduced localized SEO metadata for multiple languages in routes-seo.js, improving search engine visibility.
- Updated descriptions and keywords across various views (Home, Partners, Feedback, FAQ, Rules, Safety) to better reflect the platform's focus on private and anonymous chatting.
- Enhanced user guidance in FAQ, Partners, Rules, and Safety views with additional context on privacy and chat functionality.

These changes collectively improve the site's SEO performance and user experience by providing clearer, localized content.
2026-04-09 09:49:30 +02:00

482 lines
20 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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'
}
}
};
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, '&amp;')
.replace(/"/g, '&quot;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;');
}
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
Disallow: /api/
Disallow: /static/logs/
Disallow: /mockup-redesign
Sitemap: ${SITE_URL}/sitemap.xml
`;
res.type('text/plain');
res.send(robotsTxt);
});
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);
}
});
}