- 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.
246 lines
9.0 KiB
JavaScript
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, '<')
|
|
.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 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);
|
|
});
|
|
}
|