<message> - Introduced a new SEO_LOCALES array to support multiple languages for SEO content. - Implemented a function to build multilingual SEO content based on available locale files. - Enhanced the generateHTML function to include multilingual sections for the home route, improving accessibility for diverse audiences. These changes enhance the site's SEO capabilities by providing localized content for better search engine indexing and user engagement.
399 lines
15 KiB
JavaScript
399 lines
15 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 seoData = {
|
||
'/': {
|
||
title: 'SingleChat - Chat, Single-Chat und Bildaustausch',
|
||
description: 'Willkommen auf SingleChat - deine erste Adresse für Chat, Single-Chat und Bildaustausch. Chatte mit Menschen aus aller Welt, finde neue Kontakte und teile Erinnerungen sicher und komfortabel.',
|
||
keywords: 'Chat, Single-Chat, Bildaustausch, Online-Chat, Singles, Kontakte, Community',
|
||
ogTitle: 'SingleChat - Chat, Single-Chat und Bildaustausch',
|
||
ogDescription: 'Willkommen auf SingleChat - deine erste Adresse für Chat, Single-Chat und 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: 'Willkommen auf SingleChat - deine erste Adresse für Chat, Single-Chat und Bildaustausch.',
|
||
inLanguage: 'de-DE'
|
||
}
|
||
},
|
||
'/partners': {
|
||
title: 'Partner - SingleChat',
|
||
description: 'Unsere Partner und befreundete Seiten. Entdecke weitere interessante Angebote und Communities.',
|
||
keywords: 'Partner, Links, befreundete Seiten, Community',
|
||
ogTitle: 'Partner - SingleChat',
|
||
ogDescription: 'Unsere Partner und befreundete Seiten.',
|
||
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: 'Unsere Partner und befreundete Seiten. Entdecke weitere interessante Angebote und Communities.',
|
||
isPartOf: {
|
||
'@type': 'WebSite',
|
||
name: 'SingleChat',
|
||
url: `${SITE_URL}/`
|
||
},
|
||
inLanguage: 'de-DE'
|
||
}
|
||
},
|
||
'/feedback': {
|
||
title: 'Feedback - SingleChat',
|
||
description: 'Oeffentliche Rueckmeldungen, Meinungen und Verbesserungsvorschlaege zu SingleChat.',
|
||
keywords: 'SingleChat Feedback, Kommentare, Rueckmeldungen, Verbesserungsvorschlaege',
|
||
ogTitle: 'Feedback - SingleChat',
|
||
ogDescription: 'Oeffentliche Rueckmeldungen, Meinungen und Verbesserungsvorschlaege zu 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: 'Oeffentliche Rueckmeldungen, Meinungen und Verbesserungsvorschlaege zu SingleChat.',
|
||
isPartOf: {
|
||
'@type': 'WebSite',
|
||
name: 'SingleChat',
|
||
url: `${SITE_URL}/`
|
||
},
|
||
inLanguage: 'de-DE'
|
||
}
|
||
},
|
||
'/faq': {
|
||
title: 'FAQ - SingleChat',
|
||
description: 'Häufige Fragen zu SingleChat: Einstieg, Privatsphäre, Bildaustausch, Blockieren und mehr.',
|
||
keywords: 'SingleChat FAQ, Hilfe, Privatsphäre, Blockieren, Bilder, Chat',
|
||
ogTitle: 'FAQ - SingleChat',
|
||
ogDescription: 'Antworten auf häufige Fragen rund um SingleChat.',
|
||
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: 'Häufige Fragen zu SingleChat: Einstieg, Privatsphäre, Bildaustausch, Blockieren und mehr.',
|
||
isPartOf: {
|
||
'@type': 'WebSite',
|
||
name: 'SingleChat',
|
||
url: `${SITE_URL}/`
|
||
},
|
||
inLanguage: 'de-DE'
|
||
}
|
||
},
|
||
'/regeln': {
|
||
title: 'Regeln - SingleChat',
|
||
description: 'Chat-Regeln für ein respektvolles und sicheres Miteinander auf SingleChat.',
|
||
keywords: 'SingleChat Regeln, Chat Regeln, Community, Sicherheit',
|
||
ogTitle: 'Regeln - SingleChat',
|
||
ogDescription: 'Chat-Regeln für ein respektvolles und sicheres Miteinander.',
|
||
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: 'Chat-Regeln für ein respektvolles und sicheres Miteinander auf SingleChat.',
|
||
isPartOf: {
|
||
'@type': 'WebSite',
|
||
name: 'SingleChat',
|
||
url: `${SITE_URL}/`
|
||
},
|
||
inLanguage: 'de-DE'
|
||
}
|
||
},
|
||
'/sicherheit': {
|
||
title: 'Sicherheit & Privatsphäre - SingleChat',
|
||
description: 'Hinweise zu Privatsphäre, Blockieren/Melden und sicherer Nutzung von SingleChat.',
|
||
keywords: 'SingleChat Sicherheit, Privatsphäre, Blockieren, Melden',
|
||
ogTitle: 'Sicherheit & Privatsphäre - SingleChat',
|
||
ogDescription: 'Hinweise zu Privatsphäre, Blockieren/Melden und sicherer Nutzung.',
|
||
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: 'Hinweise zu Privatsphäre, Blockieren/Melden und sicherer Nutzung von SingleChat.',
|
||
isPartOf: {
|
||
'@type': 'WebSite',
|
||
name: 'SingleChat',
|
||
url: `${SITE_URL}/`
|
||
},
|
||
inLanguage: 'de-DE'
|
||
}
|
||
}
|
||
};
|
||
|
||
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 sanitizeLocalizedHtml(input = '') {
|
||
return String(input)
|
||
.replace(/<script[\s\S]*?>[\s\S]*?<\/script>/gi, '')
|
||
.trim();
|
||
}
|
||
|
||
function buildMultilingualSeoContent(__dirname) {
|
||
const localesDir = join(__dirname, '../client/src/i18n/locales');
|
||
const blocks = [];
|
||
|
||
for (const locale of SEO_LOCALES) {
|
||
const filePath = join(localesDir, `${locale.code}.json`);
|
||
if (!existsSync(filePath)) continue;
|
||
|
||
try {
|
||
const parsed = JSON.parse(readFileSync(filePath, 'utf-8'));
|
||
const welcome = sanitizeLocalizedHtml(parsed.welcome || '');
|
||
const intro = sanitizeLocalizedHtml(parsed.introduction || '');
|
||
if (!welcome && !intro) continue;
|
||
|
||
blocks.push(`<section lang="${escapeHtml(locale.code)}" style="margin-bottom:20px;">
|
||
<h2 style="font:600 22px/1.2 sans-serif;color:#18201b;margin:0 0 10px;">${escapeHtml(locale.label)}</h2>
|
||
${welcome}
|
||
${intro}
|
||
</section>`);
|
||
} catch (error) {
|
||
console.warn(`[SEO] Locale konnte nicht gelesen werden (${locale.code}): ${error.message}`);
|
||
}
|
||
}
|
||
|
||
if (blocks.length === 0) return '';
|
||
|
||
return `<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 12px;">Mehrsprachige Inhalte</h2>
|
||
<p style="font:400 15px/1.5 sans-serif;color:#4f5d54;margin:0 0 18px;">Diese Texte sind serverseitig eingebettet, damit Suchmaschinen die Inhalte in allen verfügbaren Sprachen direkt erfassen können.</p>
|
||
${blocks.join('\n')}
|
||
</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);
|
||
|
||
if (route === '/') {
|
||
const multilingual = buildMultilingualSeoContent(__dirname);
|
||
if (multilingual) {
|
||
html = html.replace('<div id="app"></div>', `<div id="app">${multilingual}</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);
|
||
}
|
||
});
|
||
}
|