From 7c93966878a8df256da3a2b7bdbe5359e94501fe Mon Sep 17 00:00:00 2001 From: "Torsten Schulz (local)" Date: Sun, 31 May 2026 13:36:49 +0200 Subject: [PATCH] feat: add robots.txt and sitemap.xml routes for SEO optimization - Implemented a new route for robots.txt to control crawler access. - Added a sitemap.xml route to provide search engines with a list of site URLs. - Included functions for URL normalization and XML escaping to ensure proper formatting. --- .../ui/components/AppNavigationHeader.kt | 132 +++++++++++++++--- android-app/gradle.properties | 6 +- components/Hero.vue | 21 ++- nuxt.config.js | 14 ++ pages/index.vue | 18 +-- public/images/club_about_us_hero_1600.webp | Bin 0 -> 32838 bytes public/images/club_about_us_hero_960.webp | Bin 0 -> 15464 bytes server/routes/robots.txt.js | 30 ++++ server/routes/sitemap.xml.js | 87 ++++++++++++ 9 files changed, 269 insertions(+), 39 deletions(-) create mode 100644 public/images/club_about_us_hero_1600.webp create mode 100644 public/images/club_about_us_hero_960.webp create mode 100644 server/routes/robots.txt.js create mode 100644 server/routes/sitemap.xml.js diff --git a/android-app/app/src/main/java/de/harheimertc/ui/components/AppNavigationHeader.kt b/android-app/app/src/main/java/de/harheimertc/ui/components/AppNavigationHeader.kt index 9e83723..66383c7 100644 --- a/android-app/app/src/main/java/de/harheimertc/ui/components/AppNavigationHeader.kt +++ b/android-app/app/src/main/java/de/harheimertc/ui/components/AppNavigationHeader.kt @@ -1,10 +1,12 @@ package de.harheimertc.ui.components import androidx.compose.foundation.Image +import androidx.compose.foundation.ScrollState import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.horizontalScroll import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer @@ -72,32 +74,62 @@ private fun CompactNavigation( onNavigate: (String) -> Unit, navigationState: NavigationUiState = NavigationUiState(), ) { - val section = menuSection(selectedRoute) + val routeSection = menuSection(selectedRoute) + val sectionOverride = androidx.compose.runtime.remember { androidx.compose.runtime.mutableStateOf(null) } + val section = routeSection ?: sectionOverride.value val subItems = submenu(section, navigationState) var cmsExpanded = androidx.compose.runtime.remember { androidx.compose.runtime.mutableStateOf(false) } val navigateAndClose: (String) -> Unit = { route -> cmsExpanded.value = false; onNavigate(route) } + val mainScroll = rememberScrollState() + val subScroll = rememberScrollState() + val cmsSubScroll = rememberScrollState() BrandRow(onLogin = { onNavigate(Destinations.Login.route) }) - Row( - modifier = Modifier.horizontalScroll(rememberScrollState()), - horizontalArrangement = Arrangement.spacedBy(4.dp), - ) { - CompactLink("Start", Destinations.Home.route, selectedRoute, onNavigate) - CompactLink("Verein", Destinations.VereinAbout.route, selectedRoute, onNavigate) - CompactLink("Mannschaften", Destinations.Mannschaften.route, selectedRoute, onNavigate) - CompactLink("Training", Destinations.Training.route, selectedRoute, onNavigate) - CompactLink("Termine", Destinations.Termine.route, selectedRoute, onNavigate) - CompactLink("Spielplan", Destinations.Spielplan.route, selectedRoute, onNavigate) - CompactLink("Newsletter", Destinations.NewsletterSubscribe.route, selectedRoute, onNavigate) - if (navigationState.showGallery) { - CompactLink("Galerie", Destinations.Gallery.route, selectedRoute, onNavigate) + Box(modifier = Modifier.fillMaxWidth()) { + Row( + modifier = Modifier.horizontalScroll(mainScroll), + horizontalArrangement = Arrangement.spacedBy(4.dp), + ) { + CompactLink("Start", Destinations.Home.route, selectedRoute, onNavigate) { sectionOverride.value = null } + CompactSectionLink("Verein", MenuSection.VEREIN, section) { sectionOverride.value = MenuSection.VEREIN } + CompactSectionLink("Mannschaften", MenuSection.MANNSCHAFTEN, section) { sectionOverride.value = MenuSection.MANNSCHAFTEN } + CompactSectionLink("Training", MenuSection.TRAINING, section) { sectionOverride.value = MenuSection.TRAINING } + CompactLink("Termine", Destinations.Termine.route, selectedRoute, onNavigate) { sectionOverride.value = null } + CompactLink("Spielplan", Destinations.Spielplan.route, selectedRoute, onNavigate) { sectionOverride.value = MenuSection.MANNSCHAFTEN } + CompactSectionLink("Newsletter", MenuSection.NEWSLETTER, section) { sectionOverride.value = MenuSection.NEWSLETTER } + if (navigationState.showGallery) { + CompactLink("Galerie", Destinations.Gallery.route, selectedRoute, onNavigate) { sectionOverride.value = MenuSection.VEREIN } + } + if (navigationState.loggedIn) { + CompactSectionLink("Intern", MenuSection.INTERN, section) { sectionOverride.value = MenuSection.INTERN } + } + CompactLink("Kontakt", Destinations.Contact.route, selectedRoute, onNavigate) { sectionOverride.value = null } + if (navigationState.isAdmin || navigationState.canAccessNewsletter || navigationState.canAccessContactRequests) { + CompactSectionLink("CMS", MenuSection.INTERN, section) { sectionOverride.value = MenuSection.INTERN } + } } - if (navigationState.loggedIn) { - CompactLink("Intern", Destinations.MemberArea.route, selectedRoute, onNavigate) + + if (mainScroll.canScrollBackward) { + Text( + "◀", + color = Color(0xFFD4D4D8), + style = MaterialTheme.typography.labelSmall, + modifier = Modifier + .align(Alignment.CenterStart) + .background(Color(0x66000000), RoundedCornerShape(8.dp)) + .padding(horizontal = 4.dp, vertical = 2.dp), + ) } - CompactLink("Kontakt", Destinations.Contact.route, selectedRoute, onNavigate) - if (navigationState.isAdmin || navigationState.canAccessNewsletter || navigationState.canAccessContactRequests) { - CompactLink("CMS", Destinations.Cms.route, selectedRoute, onNavigate) + if (mainScroll.canScrollForward) { + Text( + "▶", + color = Color(0xFFD4D4D8), + style = MaterialTheme.typography.labelSmall, + modifier = Modifier + .align(Alignment.CenterEnd) + .background(Color(0x66000000), RoundedCornerShape(8.dp)) + .padding(horizontal = 4.dp, vertical = 2.dp), + ) } } @@ -110,7 +142,7 @@ private fun CompactNavigation( Row( modifier = Modifier .fillMaxWidth() - .horizontalScroll(rememberScrollState()) + .horizontalScroll(subScroll) .padding(top = 3.dp), horizontalArrangement = Arrangement.spacedBy(4.dp), ) { @@ -126,12 +158,15 @@ private fun CompactNavigation( } } } + if (subItems.isNotEmpty()) { + ScrollHintRow(subScroll) + } if (cmsExpanded.value && cmsChildren.isNotEmpty()) { Row( modifier = Modifier .fillMaxWidth() - .horizontalScroll(rememberScrollState()) + .horizontalScroll(cmsSubScroll) .padding(top = 6.dp, bottom = 3.dp), horizontalArrangement = Arrangement.spacedBy(8.dp), ) { @@ -139,6 +174,31 @@ private fun CompactNavigation( SubLink(child.label, child.route == selectedRoute) { onNavigate(child.route) } } } + ScrollHintRow(cmsSubScroll) + } +} + +@Composable +private fun CompactSectionLink( + label: String, + section: MenuSection, + activeSection: MenuSection?, + modifier: Modifier = Modifier, + onSelect: () -> Unit, +) { + Surface( + color = if (activeSection == section) Primary600 else Color.Transparent, + shape = RoundedCornerShape(8.dp), + modifier = modifier.clickable { onSelect() }, + ) { + Text( + label, + color = if (activeSection == section) Color.White else Color(0xFFD4D4D8), + textAlign = TextAlign.Center, + style = MaterialTheme.typography.labelMedium, + modifier = Modifier.padding(vertical = 9.dp, horizontal = 2.dp), + maxLines = 1, + ) } } @@ -285,11 +345,15 @@ private fun CompactLink( selectedRoute: String?, onNavigate: (String) -> Unit, modifier: Modifier = Modifier, + beforeNavigate: () -> Unit = {}, ) { Surface( color = if (route == selectedRoute) Primary600 else Color.Transparent, shape = RoundedCornerShape(8.dp), - modifier = modifier.clickable { onNavigate(route) }, + modifier = modifier.clickable { + beforeNavigate() + onNavigate(route) + }, ) { Text( label, @@ -302,6 +366,30 @@ private fun CompactLink( } } +@Composable +private fun ScrollHintRow(scrollState: ScrollState) { + if (!scrollState.canScrollBackward && !scrollState.canScrollForward) return + + Row( + modifier = Modifier + .fillMaxWidth() + .padding(top = 2.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + if (scrollState.canScrollBackward) "◀" else "", + color = Color(0xFFD4D4D8), + style = MaterialTheme.typography.labelSmall, + ) + Text( + if (scrollState.canScrollForward) "▶" else "", + color = Color(0xFFD4D4D8), + style = MaterialTheme.typography.labelSmall, + ) + } +} + @Composable private fun SubLink(label: String, selected: Boolean, onClick: () -> Unit) { Surface( diff --git a/android-app/gradle.properties b/android-app/gradle.properties index 1601f8e..77403df 100644 --- a/android-app/gradle.properties +++ b/android-app/gradle.properties @@ -5,11 +5,11 @@ org.gradle.workers.max=2 LOCAL_API_BASE_URL=https://harheimertc.tsschulz.de/ # Production backend for Play Store build variant -PRODUCTION_API_BASE_URL=https://harheimertc.tsschulz.de/ +PRODUCTION_API_BASE_URL=https://harheimertc.de/ # Android app versioning for Play Store uploads -ANDROID_VERSION_CODE=17 -ANDROID_VERSION_NAME=0.9.12 +ANDROID_VERSION_CODE=18 +ANDROID_VERSION_NAME=0.9.13 # Temporary hotfix: disable R8 minification for release to avoid Retrofit generic signature stripping. RELEASE_MINIFY_ENABLED=false diff --git a/components/Hero.vue b/components/Hero.vue index 379ccb3..cd54fe2 100644 --- a/components/Hero.vue +++ b/components/Hero.vue @@ -7,11 +7,22 @@
- -
+ + + +
diff --git a/nuxt.config.js b/nuxt.config.js index 80710ff..ac165a4 100644 --- a/nuxt.config.js +++ b/nuxt.config.js @@ -77,6 +77,20 @@ export default defineNuxtConfig({ ], link: [ { rel: 'canonical', href: 'https://www.harheimertc.de/' }, + { + rel: 'preload', + as: 'image', + href: '/images/club_about_us_hero_960.webp', + type: 'image/webp', + media: '(max-width: 1024px)' + }, + { + rel: 'preload', + as: 'image', + href: '/images/club_about_us_hero_1600.webp', + type: 'image/webp', + media: '(min-width: 1025px)' + }, { rel: 'preconnect', href: 'https://fonts.googleapis.com' }, { rel: 'preconnect', href: 'https://fonts.gstatic.com', crossorigin: '' }, { diff --git a/pages/index.vue b/pages/index.vue index 30df1a1..3694a05 100644 --- a/pages/index.vue +++ b/pages/index.vue @@ -226,17 +226,17 @@