diff --git a/backend/server.js b/backend/server.js index 27985cb1..357a4a11 100644 --- a/backend/server.js +++ b/backend/server.js @@ -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( + ` + + diff --git a/frontend/src/router.js b/frontend/src/router.js index 83596da8..9d81abf9 100644 --- a/frontend/src/router.js +++ b/frontend/src/router.js @@ -8,6 +8,10 @@ const Activate = () => import('./views/Activate.vue'); const ForgotPassword = () => import('./views/ForgotPassword.vue'); const ResetPassword = () => import('./views/ResetPassword.vue'); const Home = () => import('./views/Home.vue'); +const TableTennisClubSoftware = () => import('./views/TableTennisClubSoftware.vue'); +const ClubMemberManagementPage = () => import('./views/ClubMemberManagementPage.vue'); +const TrainingPlanningPage = () => import('./views/TrainingPlanningPage.vue'); +const TableTennisTournamentSoftwarePage = () => import('./views/TableTennisTournamentSoftwarePage.vue'); const CreateClub = () => import('./views/CreateClub.vue'); const ClubView = () => import('./views/ClubView.vue'); const MembersView = () => import('./views/MembersView.vue'); @@ -38,6 +42,10 @@ const routes = [ { path: '/forgot-password', name: 'forgot-password', component: ForgotPassword, meta: { public: true } }, { path: '/reset-password/:token', name: 'reset-password', component: ResetPassword, meta: { public: true } }, { path: '/', name: 'home', component: Home, meta: { public: true } }, + { path: '/vereinssoftware-tischtennis', name: 'club-software-seo', component: TableTennisClubSoftware, meta: { public: true } }, + { path: '/mitgliederverwaltung-verein', name: 'member-management-seo', component: ClubMemberManagementPage, meta: { public: true } }, + { path: '/trainingsplanung-tischtennis', name: 'training-planning-seo', component: TrainingPlanningPage, meta: { public: true } }, + { path: '/turniersoftware-tischtennis', name: 'tournament-software-seo', component: TableTennisTournamentSoftwarePage, meta: { public: true } }, { path: '/createclub', name: 'create-club', component: CreateClub }, { path: '/showclub/:clubId', name: 'show-club', component: ClubView }, { path: '/members', name: 'members', component: MembersView }, diff --git a/frontend/src/utils/seo.js b/frontend/src/utils/seo.js index be790ba1..8e0105b7 100644 --- a/frontend/src/utils/seo.js +++ b/frontend/src/utils/seo.js @@ -14,6 +14,26 @@ const ROUTE_SEO = { description: DEFAULT_SEO.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.', diff --git a/frontend/src/views/ClubMemberManagementPage.vue b/frontend/src/views/ClubMemberManagementPage.vue new file mode 100644 index 00000000..ddf48922 --- /dev/null +++ b/frontend/src/views/ClubMemberManagementPage.vue @@ -0,0 +1,66 @@ + + + diff --git a/frontend/src/views/Home.vue b/frontend/src/views/Home.vue index 9458213c..7877a26a 100644 --- a/frontend/src/views/Home.vue +++ b/frontend/src/views/Home.vue @@ -85,6 +85,9 @@ Trainingsgruppen, Teilnahmen und Team-Zuordnungen direkt mit den Mitgliedsdaten verknüpfen.

+ + Mehr zur Mitgliederverwaltung +

Trainingsplanung und Trainingstagebuch

@@ -93,6 +96,9 @@ halten Aktivitäten im Trainingstagebuch fest. Das eignet sich besonders für Tischtennistraining mit wechselnden Gruppen, Anwesenheiten und Stationsformen.

+ + Mehr zur Trainingsplanung +

Turniersoftware für Vereinswettbewerbe

@@ -101,6 +107,9 @@ Wettbewerbe. Teilnehmer, Gruppen, Auslosungen, Ergebnisse und Übersichten lassen sich in einer Anwendung verwalten.

+ + Mehr zur Turniersoftware +

Team-Management und Statistiken

@@ -109,9 +118,30 @@ Damit eignet sich Trainingstagebuch nicht nur für die Trainingsorganisation, sondern auch für die laufende Vereinsarbeit über die ganze Saison.

+ + Mehr zur Vereinssoftware +
+
+

Öffentliche Themenseiten

+ +
+

{{ $t('home.whatCanYouDo') }}

@@ -505,12 +535,34 @@ export default { margin-bottom: 0; } +.topic-link, +.seo-hub-link { + color: var(--primary-color); + font-weight: 600; + text-decoration: none; +} + +.topic-link:hover, +.seo-hub-link:hover { + text-decoration: underline; +} + .seo-intro-points { display: grid; gap: 0.85rem; align-content: start; } +.seo-hub { + padding: 1.5rem; +} + +.seo-hub-links { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); + gap: 0.75rem 1rem; +} + .seo-point { display: flex; flex-direction: column; diff --git a/frontend/src/views/TableTennisClubSoftware.vue b/frontend/src/views/TableTennisClubSoftware.vue new file mode 100644 index 00000000..a5ab7f32 --- /dev/null +++ b/frontend/src/views/TableTennisClubSoftware.vue @@ -0,0 +1,67 @@ + + + diff --git a/frontend/src/views/TableTennisTournamentSoftwarePage.vue b/frontend/src/views/TableTennisTournamentSoftwarePage.vue new file mode 100644 index 00000000..6415380b --- /dev/null +++ b/frontend/src/views/TableTennisTournamentSoftwarePage.vue @@ -0,0 +1,66 @@ + + + diff --git a/frontend/src/views/TrainingPlanningPage.vue b/frontend/src/views/TrainingPlanningPage.vue new file mode 100644 index 00000000..600f2722 --- /dev/null +++ b/frontend/src/views/TrainingPlanningPage.vue @@ -0,0 +1,66 @@ + + + diff --git a/update-sitemap.sh b/update-sitemap.sh index 4d1b5d71..43ac76ef 100755 --- a/update-sitemap.sh +++ b/update-sitemap.sh @@ -18,6 +18,10 @@ echo "Aktualisiere lastmod-Datum auf: $TODAY" URLS=( "https://tt-tagebuch.de/" "https://tt-tagebuch.de/impressum" + "https://tt-tagebuch.de/vereinssoftware-tischtennis" + "https://tt-tagebuch.de/mitgliederverwaltung-verein" + "https://tt-tagebuch.de/trainingsplanung-tischtennis" + "https://tt-tagebuch.de/turniersoftware-tischtennis" "https://tt-tagebuch.de/datenschutz" ) @@ -42,6 +46,30 @@ cat > "$SITEMAP_FILE" < ${URLS[2]} ${TODAY} + monthly + 0.8 + + + ${URLS[3]} + ${TODAY} + monthly + 0.8 + + + ${URLS[4]} + ${TODAY} + monthly + 0.8 + + + ${URLS[5]} + ${TODAY} + monthly + 0.8 + + + ${URLS[6]} + ${TODAY} yearly 0.3