feat(SEO, Sitemap, Routing): enhance SEO and sitemap for new features

- Updated update-sitemap.sh to include new URLs for Vereinssoftware, Mitgliederverwaltung, Trainingsplanung, and Turniersoftware with appropriate lastmod dates and change frequencies.
- Enhanced server.js and seo.js with SEO configurations for the new pages, ensuring proper indexing and descriptions.
- Added new routes in router.js for the additional features, improving navigation and user access.
- Updated Home.vue to include links to the new features, enhancing user engagement and visibility.
This commit is contained in:
Torsten Schulz (local)
2026-03-27 11:42:11 +01:00
parent a7d3e5b094
commit 9d023b534d
11 changed files with 685 additions and 6 deletions

View File

@@ -62,6 +62,10 @@ import HttpError from './exceptions/HttpError.js';
const app = express();
const port = process.env.PORT || 3005;
const PUBLIC_SITE_URL = process.env.PUBLIC_SITE_URL || 'https://tt-tagebuch.de';
const publicSiteOrigin = new URL(PUBLIC_SITE_URL).origin;
const publicSiteHost = new URL(PUBLIC_SITE_URL).host;
const publicSiteProtocol = new URL(PUBLIC_SITE_URL).protocol.replace(':', '');
function captureRawBody(req, res, buf, encoding) {
if (!buf || buf.length === 0) return;
@@ -83,6 +87,26 @@ const SEO_ROUTE_CONFIG = {
description: SEO_DEFAULTS.description,
robots: 'index,follow',
},
'/vereinssoftware-tischtennis': {
title: 'Vereinssoftware für Tischtennisvereine | Trainingstagebuch',
description: 'Webbasierte Vereinssoftware für Tischtennisvereine mit Mitgliederverwaltung, Trainingsplanung, Mannschaftsorganisation, Turnieren und Auswertungen.',
robots: 'index,follow',
},
'/mitgliederverwaltung-verein': {
title: 'Mitgliederverwaltung für Vereine | Trainingstagebuch',
description: 'Mitgliederverwaltung für Vereine mit Stammdaten, Rollen, Gruppenbezug und organisatorischer Verbindung zu Training und Vereinsabläufen.',
robots: 'index,follow',
},
'/trainingsplanung-tischtennis': {
title: 'Trainingsplanung für Tischtennisvereine | Trainingstagebuch',
description: 'Trainingsplanung für Tischtennisvereine mit Gruppen, Anwesenheiten, Trainingstagebuch und digitaler Organisation von Trainingstagen.',
robots: 'index,follow',
},
'/turniersoftware-tischtennis': {
title: 'Turniersoftware für Tischtennis | Trainingstagebuch',
description: 'Turniersoftware für Tischtennisvereine mit Teilnehmerverwaltung, Gruppen, Paarungen, Ergebnissen und Organisation interner oder offizieller Turniere.',
robots: 'index,follow',
},
'/login': {
title: 'Login | Trainingstagebuch',
description: 'Im Trainingstagebuch einloggen und Vereinsdaten, Trainingsplanung, Mitglieder und Turniere verwalten.',
@@ -175,6 +199,41 @@ function escapeHtmlAttribute(value = '') {
.replace(/>/g, '>');
}
function isPublicFacingHost(host = '') {
const normalizedHost = String(host).toLowerCase().replace(/:\d+$/, '');
return normalizedHost === 'tt-tagebuch.de' || normalizedHost === 'www.tt-tagebuch.de';
}
function getCanonicalRequestContext(req) {
const forwardedProtoHeader = req.get('x-forwarded-proto');
const forwardedHostHeader = req.get('x-forwarded-host');
const forwardedProto = forwardedProtoHeader?.split(',')[0]?.trim();
const forwardedHost = forwardedHostHeader?.split(',')[0]?.trim();
const requestHost = forwardedHost || req.get('host') || publicSiteHost;
const requestProtocol = forwardedProto || req.protocol || publicSiteProtocol;
const canonicalHost = isPublicFacingHost(requestHost) ? requestHost.replace(/^www\./, '') : publicSiteHost;
const canonicalProtocol = isPublicFacingHost(requestHost) ? requestProtocol : publicSiteProtocol;
return {
requestHost,
requestProtocol,
canonicalHost,
canonicalProtocol,
canonicalOrigin: `${canonicalProtocol}://${canonicalHost}`,
};
}
function stripJsonLdByType(html, schemaType) {
const escapedType = schemaType.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const pattern = new RegExp(
`<script type="application/ld\\+json">[\\s\\S]*?"@type"\\s*:\\s*"${escapedType}"[\\s\\S]*?<\\/script>\\s*`,
'g'
);
return html.replace(pattern, '');
}
app.set('trust proxy', true);
// CORS-Konfiguration - Socket.IO hat seine eigene CORS-Konfiguration
app.use(cors({
origin: true,
@@ -244,6 +303,14 @@ app.use('/api/member-orders', memberOrderRoutes);
// Middleware für dynamischen kanonischen Tag (vor express.static)
const setCanonicalTag = (req, res, next) => {
const normalizedPath = normalizeSeoPath(req.path);
const { requestHost, requestProtocol, canonicalHost, canonicalProtocol, canonicalOrigin } = getCanonicalRequestContext(req);
if (requestHost.replace(/^www\./, '') === canonicalHost && requestHost !== canonicalHost) {
const redirectUrl = `${canonicalProtocol}://${canonicalHost}${normalizedPath === '/' ? '/' : normalizedPath}${req.url.includes('?') ? req.url.slice(req.url.indexOf('?')) : ''}`;
return res.redirect(301, redirectUrl);
}
// Socket.IO-Requests komplett ignorieren
if (req.path.startsWith('/socket.io/')) {
return next();
@@ -269,12 +336,7 @@ const setCanonicalTag = (req, res, next) => {
return next();
}
// Bestimme die kanonische URL (bevorzuge non-www)
const protocol = req.protocol || 'https';
const host = req.get('host') || 'tt-tagebuch.de';
const canonicalHost = host.replace(/^www\./, ''); // Entferne www falls vorhanden
const normalizedPath = normalizeSeoPath(req.path);
const canonicalUrl = `${protocol}://${canonicalHost}${normalizedPath === '/' ? '' : normalizedPath}`;
const canonicalUrl = `${canonicalOrigin}${normalizedPath === '/' ? '' : normalizedPath}`;
const seo = getSeoConfigForPath(normalizedPath);
let updatedData = data.replace(
@@ -322,6 +384,11 @@ const setCanonicalTag = (req, res, next) => {
`<meta name="twitter:description" content="${escapeHtmlAttribute(seo.description)}" />`
);
if (normalizedPath !== '/') {
updatedData = stripJsonLdByType(updatedData, 'FAQPage');
updatedData = stripJsonLdByType(updatedData, 'SoftwareApplication');
}
res.setHeader('Content-Type', 'text/html');
res.send(updatedData);
});