Files
singlechat/server/routes-seo.js
Torsten Schulz (local) 06182a4a95 Add FAQ, Rules, and Safety pages with corresponding routes and SEO metadata
- Introduced new links in ImprintContainer.vue for FAQ, Rules, and Safety pages.
- Added FaqView, RulesView, and SafetyView components to handle the new routes.
- Implemented SEO metadata for the new pages in routes-seo.js and router/index.js.
- Updated server routes to include the new paths for proper handling in production.

These changes enhance the site's informational resources and improve SEO visibility for user inquiries.
2026-03-27 14:15:37 +01:00

339 lines
12 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 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, '&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 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 === '/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) {
res.send(html);
return;
}
if (existsSync(distIndexPath)) {
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);
}
});
}