Files
singlechat/server/routes-seo.js
Torsten Schulz (local) 47373a27af Enhance SEO and feedback features across the application
- Updated index.html with improved meta tags for SEO, including author and theme color.
- Added a feedback dialog in ImprintContainer.vue for user feedback submission.
- Refactored LoginForm.vue to utilize a utility for cookie management, simplifying profile persistence.
- Introduced new routes and schemas for feedback in the router and server, enhancing SEO and user experience.
- Improved ChatView.vue with better error handling and command table display.
- Implemented feedback API endpoints in server routes for managing user feedback submissions and admin access.

These changes collectively improve the application's SEO, user interaction, and feedback management capabilities.
2026-03-19 15:21:54 +01:00

246 lines
9.0 KiB
JavaScript

import { readFileSync, existsSync } from 'fs';
import { join, resolve } from 'path';
import { loadFeedback } from './feedback-store.js';
const SITE_URL = 'https://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'
}
}
};
function escapeHtml(value = '') {
return String(value)
.replace(/&/g, '&')
.replace(/"/g, '"')
.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
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) => {
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');
const sitemap = `<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
${urls}
</urlset>`;
res.type('application/xml');
res.send(sitemap);
});
}