v1.3.2: SEO fixes + map bugfixes

SEO:
- Landing page converted to Server Component (SSR)
- Extracted NavAuthButtons + ContactForm as client islands
- Removed fake aggregateRating from JSON-LD
- Added FAQPage JSON-LD schema (7 questions)
- Extended sitemap: /datenschutz, /spenden, /demo

Map fixes:
- WebGL context lost recovery (black tiles after inactivity)
- Page visibility handler for tile reload on tab switch
- Arrow direction: geographic bearing instead of screen angle
- All markers rotationAlignment viewport->map (geographic orientation)
- DEL key now deletes selected lines/polygons/arrows (not just symbols)
- Default drawing color: black
This commit is contained in:
Pepe Ziberi
2026-03-03 23:33:04 +01:00
parent 708bdf6be0
commit 1f508bca74
8 changed files with 298 additions and 156 deletions

View File

@@ -728,6 +728,46 @@ export function MapView({
// Expose map instance to parent for export
if (externalMapRef) externalMapRef.current = map.current
// --- WebGL context loss recovery ---
// When the browser reclaims GPU memory (background tab, memory pressure),
// the WebGL context is lost and tiles go black. This recovers automatically.
const canvas = map.current.getCanvas()
canvas.addEventListener('webglcontextlost', (e) => {
console.warn('[Map] WebGL context lost — will restore when possible')
e.preventDefault() // allows context to be restored
})
canvas.addEventListener('webglcontextrestored', () => {
console.info('[Map] WebGL context restored — reloading map style')
const m = map.current
if (m) {
// Force full tile reload by re-setting the style
const style = m.getStyle()
if (style) {
m.setStyle(style)
}
}
})
// --- Page visibility recovery ---
// When user switches back to this tab after a while, tiles may be stale/black.
// Force a resize + tile re-request on visibility change.
const handleVisibilityChange = () => {
if (document.visibilityState === 'visible' && map.current) {
// Small delay to let browser finish tab switch
setTimeout(() => {
if (!map.current) return
map.current.resize()
// Nudge the map to force tile re-requests
const center = map.current.getCenter()
map.current.setCenter(center)
}, 100)
}
}
document.addEventListener('visibilitychange', handleVisibilityChange)
// Store cleanup reference
const cleanupVisibility = () => document.removeEventListener('visibilitychange', handleVisibilityChange)
map.current.addControl(new maplibregl.NavigationControl(), 'bottom-right')
map.current.addControl(new maplibregl.ScaleControl(), 'bottom-left')
@@ -1286,6 +1326,7 @@ export function MapView({
})
return () => {
cleanupVisibility()
map.current?.remove()
map.current = null
}
@@ -1421,26 +1462,27 @@ export function MapView({
const lineCoords = f.geometry.coordinates as number[][]
if (lineCoords.length < 2) return
// Get last two points to calculate arrow direction using screen-projected coords
// Geographic bearing from p1 to p2 (works for short distances)
const p1 = lineCoords[lineCoords.length - 2]
const p2 = lineCoords[lineCoords.length - 1]
const px1 = map.current.project(p1 as [number, number])
const px2 = map.current.project(p2 as [number, number])
const screenAngle = Math.atan2(px2.y - px1.y, px2.x - px1.x) * (180 / Math.PI) + 90
const dLng = p2[0] - p1[0]
const dLat = p2[1] - p1[1]
// atan2(dLng, dLat) gives angle from north (up), clockwise — matches CSS triangle ▲ default
const geoBearing = Math.atan2(dLng, dLat) * (180 / Math.PI)
const color = (f.properties.color as string) || '#000000'
const arrowEl = document.createElement('div')
arrowEl.style.cssText = `
width: 0; height: 0;
border-left: 10px solid transparent;
border-right: 10px solid transparent;
border-bottom: 20px solid ${color};
transform: rotate(${screenAngle}deg);
border-left: 12px solid transparent;
border-right: 12px solid transparent;
border-bottom: 24px solid ${color};
transform: rotate(${geoBearing}deg);
transform-origin: center center;
pointer-events: none;
`
const marker = new maplibregl.Marker({ element: arrowEl, anchor: 'center', rotationAlignment: 'viewport' })
const marker = new maplibregl.Marker({ element: arrowEl, anchor: 'center', rotationAlignment: 'map' })
.setLngLat(p2 as [number, number])
.addTo(map.current)
markersRef.current.push(marker)
@@ -1545,7 +1587,7 @@ export function MapView({
})
}
const marker = new maplibregl.Marker({ element: el, anchor: 'center', draggable: canEdit, rotationAlignment: 'viewport' })
const marker = new maplibregl.Marker({ element: el, anchor: 'center', draggable: canEdit, rotationAlignment: 'map' })
.setLngLat(midpoint)
.addTo(map.current)
@@ -1664,7 +1706,7 @@ export function MapView({
}
try {
const marker = new maplibregl.Marker({ element: wrapper, draggable: canEdit, anchor: 'center', rotationAlignment: 'viewport' })
const marker = new maplibregl.Marker({ element: wrapper, draggable: canEdit, anchor: 'center', rotationAlignment: 'map' })
.setLngLat(coords)
.addTo(map.current)
@@ -1730,7 +1772,7 @@ export function MapView({
el.textContent = (f.properties.text as string) || ''
wrapper.appendChild(el)
const marker = new maplibregl.Marker({ element: wrapper, draggable: canEdit, anchor: 'center', rotationAlignment: 'viewport' })
const marker = new maplibregl.Marker({ element: wrapper, draggable: canEdit, anchor: 'center', rotationAlignment: 'map' })
.setLngLat(coords)
.addTo(map.current)
@@ -1892,18 +1934,27 @@ export function MapView({
}
}, [drawMode, deselectSymbol])
// ESC to cancel drawing, DEL to delete selected symbol
// ESC to cancel drawing, DEL to delete selected symbol/line/polygon
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
// DEL / Backspace → delete selected symbol
// DEL / Backspace → delete selected symbol or line/polygon
if (e.key === 'Delete' || e.key === 'Backspace') {
const tag = (e.target as HTMLElement)?.tagName
if (tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT' || (e.target as HTMLElement)?.isContentEditable) return
// Delete selected symbol/text
if (selectedSymbolRef.current) {
e.preventDefault()
deleteSelectedSymbol()
return
}
// Delete selected line/polygon/arrow (vertex-editing selection)
if (selectedLineIdRef.current) {
e.preventDefault()
const updated = featuresRef.current.filter(f => f.id !== selectedLineIdRef.current)
onFeaturesChangeRef.current(updated)
showVertexMarkersRef.current(null)
return
}
}
if (e.key === 'Escape') {
// In measure mode: finalize (keep line + labels), just stop adding