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.
This commit is contained in:
Torsten Schulz (local)
2026-03-19 15:21:54 +01:00
parent 0205352ae9
commit 47373a27af
13 changed files with 1184 additions and 238 deletions

View File

@@ -1,7 +1,10 @@
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`;
// SEO-Meta-Daten für verschiedene Routen
const seoData = {
'/': {
title: 'SingleChat - Chat, Single-Chat und Bildaustausch',
@@ -10,8 +13,17 @@ const seoData = {
ogTitle: 'SingleChat - Chat, Single-Chat und Bildaustausch',
ogDescription: 'Willkommen auf SingleChat - deine erste Adresse für Chat, Single-Chat und Bildaustausch.',
ogType: 'website',
ogUrl: 'https://ypchat.net/',
ogImage: 'https://ypchat.net/static/favicon.png'
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',
@@ -20,162 +32,214 @@ const seoData = {
ogTitle: 'Partner - SingleChat',
ogDescription: 'Unsere Partner und befreundete Seiten.',
ogType: 'website',
ogUrl: 'https://ypchat.net/partners',
ogImage: 'https://ypchat.net/static/favicon.png'
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'
}
}
};
// HTML-Template für Pre-Rendering
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) {
// Versuche, die gebaute index.html zu lesen
const distIndexPath = join(__dirname, '../docroot/dist/index.html');
console.log('[SEO] Prüfe gebaute index.html:', distIndexPath);
console.log('[SEO] Datei existiert:', existsSync(distIndexPath));
if (!existsSync(distIndexPath)) {
// Fallback: Gebaute index.html nicht gefunden
console.error('WARNUNG: Gebaute index.html nicht gefunden:', distIndexPath);
return null;
}
// Verwende die gebaute index.html (mit korrekten Asset-Pfaden von Vite)
let baseHTML = readFileSync(distIndexPath, 'utf-8');
console.log('[SEO] Gebaute HTML geladen, Länge:', baseHTML.length);
console.log('[SEO] Enthält Script-Tags:', baseHTML.includes('<script'));
// Ersetze Meta-Tags in der gebauten HTML
baseHTML = baseHTML.replace(/<title>.*?<\/title>/, `<title>${meta.title}</title>`);
// Ersetze oder füge description hinzu
if (baseHTML.includes('<meta name="description"')) {
baseHTML = baseHTML.replace(/<meta name="description"[^>]*>/g, `<meta name="description" content="${meta.description}">`);
} else {
baseHTML = baseHTML.replace('</head>', ` <meta name="description" content="${meta.description}">\n</head>`);
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>`);
}
// Ersetze oder füge keywords hinzu
if (baseHTML.includes('<meta name="keywords"')) {
baseHTML = baseHTML.replace(/<meta name="keywords"[^>]*>/g, `<meta name="keywords" content="${meta.keywords}">`);
} else {
baseHTML = baseHTML.replace('</head>', ` <meta name="keywords" content="${meta.keywords}">\n</head>`);
}
// Ersetze oder füge Open Graph Tags hinzu
const ogTags = `
<meta property="og:title" content="${meta.ogTitle}">
<meta property="og:description" content="${meta.ogDescription}">
<meta property="og:type" content="${meta.ogType}">
<meta property="og:url" content="${meta.ogUrl}">
<meta property="og:image" content="${meta.ogImage}">
<meta name="twitter:card" content="summary">
<meta name="twitter:title" content="${meta.ogTitle}">
<meta name="twitter:description" content="${meta.ogDescription}">
<meta name="twitter:image" content="${meta.ogImage}">
<link rel="canonical" href="${meta.ogUrl}">`;
// Entferne alte OG/Twitter/Canonical Tags falls vorhanden (nur Meta-Tags, keine Script-Tags!)
baseHTML = baseHTML.replace(/<meta property="og:[^>]*>/g, '');
baseHTML = baseHTML.replace(/<meta name="twitter:[^>]*>/g, '');
baseHTML = baseHTML.replace(/<link rel="canonical"[^>]*>/g, '');
// Füge neue Tags vor </head> ein (aber NACH den Script-Tags!)
// Finde die Position von </head> und füge die Tags davor ein
const headEndIndex = baseHTML.indexOf('</head>');
if (headEndIndex !== -1) {
baseHTML = baseHTML.substring(0, headEndIndex) + ogTags + '\n' + baseHTML.substring(headEndIndex);
}
// Füge robots meta hinzu falls nicht vorhanden
if (!baseHTML.includes('<meta name="robots"')) {
const headEndIndex2 = baseHTML.indexOf('</head>');
if (headEndIndex2 !== -1) {
baseHTML = baseHTML.substring(0, headEndIndex2) + ` <meta name="robots" content="index, follow">\n` + baseHTML.substring(headEndIndex2);
}
}
console.log('[SEO] HTML nach Manipulation, Länge:', baseHTML.length);
console.log('[SEO] Enthält Script-Tags nach Manipulation:', baseHTML.includes('<script'));
return baseHTML;
return html;
}
export function setupSEORoutes(app, __dirname) {
// Pre-Rendering für SEO-relevante Routen (nur in Production)
// In Development wird die normale index.html verwendet
const IS_PRODUCTION = process.env.NODE_ENV === 'production';
if (IS_PRODUCTION) {
const distIndexPath = resolve(__dirname, '../docroot/dist/index.html');
// Pre-Rendering für Hauptseite
app.get('/', (req, res) => {
const meta = seoData['/'];
const html = generateHTML('/', meta, __dirname);
if (html) {
res.send(html);
} else {
// Fallback: Verwende die gebaute index.html direkt (ohne Meta-Tag-Anpassung)
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);
} else {
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.');
return;
}
}
});
// Pre-Rendering für Partners-Seite
app.get('/partners', (req, res) => {
const meta = seoData['/partners'];
const html = generateHTML('/partners', meta, __dirname);
if (html) {
res.send(html);
} else {
// Fallback: Verwende die gebaute index.html direkt (ohne Meta-Tag-Anpassung)
if (existsSync(distIndexPath)) {
res.sendFile(distIndexPath);
} else {
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.');
}
}
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.');
});
});
}
// robots.txt
app.get('/robots.txt', (req, res) => {
const robotsTxt = `User-agent: *
Allow: /
Allow: /partners
Allow: /feedback
Disallow: /api/
Disallow: /static/logs/
Disallow: /mockup-redesign
Sitemap: https://ypchat.net/sitemap.xml
Sitemap: ${SITE_URL}/sitemap.xml
`;
res.type('text/plain');
res.send(robotsTxt);
});
// sitemap.xml
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">
<url>
<loc>https://ypchat.net/</loc>
<lastmod>${new Date().toISOString().split('T')[0]}</lastmod>
<changefreq>daily</changefreq>
<priority>1.0</priority>
</url>
<url>
<loc>https://ypchat.net/partners</loc>
<lastmod>${new Date().toISOString().split('T')[0]}</lastmod>
<changefreq>weekly</changefreq>
<priority>0.8</priority>
</url>
${urls}
</urlset>`;
res.type('application/xml');
res.send(sitemap);
});
}