feat(seo): Zweisprachige Meta-Tags und hreflang für DE/EN
SEO-Texte in i18n, dynamische Meta-Updates beim Sprachwechsel, hreflang-Links und ?lng-Parameter; PWA-Manifest zweisprachig. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+4
-1
@@ -5,11 +5,14 @@
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="description" content="Kostenloses, werbefreies digitales Yacht-Logbuch mit End-to-End-Verschlüsselung und Passkey-Anmeldung. Reisetage, GPS-Tracks, Crew und Schiffsdaten sicher dokumentieren – auch offline als PWA." />
|
||||
<meta name="keywords" content="Yacht-Logbuch, Schiffstagebuch, Bordlogbuch, Segeln, Passkey, E2E-Verschlüsselung, GPS-Track, maritimes Logbuch, kostenlos, werbefrei, gratis, ohne Werbung" />
|
||||
<meta name="keywords" content="Yacht-Logbuch, Schiffstagebuch, Bordlogbuch, Segeln, Passkey, E2E-Verschlüsselung, GPS-Track, maritimes Logbuch, kostenlos, werbefrei, gratis, ohne Werbung, yacht logbook, sailing log, ad-free" />
|
||||
<meta name="author" content="Markus F.J. Busche" />
|
||||
<meta name="robots" content="index, follow" />
|
||||
<meta name="application-name" content="Kapteins Daagbok" />
|
||||
<link rel="canonical" href="https://kapteins-daagbok.eu/" />
|
||||
<link rel="alternate" hreflang="de" href="https://kapteins-daagbok.eu/?lng=de" />
|
||||
<link rel="alternate" hreflang="en" href="https://kapteins-daagbok.eu/?lng=en" />
|
||||
<link rel="alternate" hreflang="x-default" href="https://kapteins-daagbok.eu/" />
|
||||
<meta name="mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
||||
|
||||
@@ -3,6 +3,7 @@ import { initReactI18next } from 'react-i18next'
|
||||
import LanguageDetector from 'i18next-browser-languagedetector'
|
||||
import enTranslation from './locales/en.json'
|
||||
import deTranslation from './locales/de.json'
|
||||
import { initSeo } from '../utils/seo.js'
|
||||
|
||||
i18n
|
||||
.use(LanguageDetector)
|
||||
@@ -17,9 +18,12 @@ i18n
|
||||
escapeValue: false // React already escapes values (prevents XSS)
|
||||
},
|
||||
detection: {
|
||||
order: ['localStorage', 'navigator'],
|
||||
order: ['querystring', 'localStorage', 'navigator'],
|
||||
lookupQuerystring: 'lng',
|
||||
caches: ['localStorage']
|
||||
}
|
||||
})
|
||||
|
||||
initSeo(i18n)
|
||||
|
||||
export default i18n
|
||||
|
||||
@@ -332,6 +332,7 @@
|
||||
"color_scheme_dark": "Dunkel",
|
||||
"share_title": "Logbuch teilen (Schreibgeschützt)",
|
||||
"share_desc": "Aktiviere diese Option, um einen öffentlichen, schreibgeschützten Link zu erstellen. Jeder mit dem Link kann deine Reisen, Yacht-Profile und Besatzung ansehen. Die Verschlüsselungsschlüssel werden niemals an den Server übertragen (sie bleiben im Hash-Teil der URL).",
|
||||
"share_privacy_warning": "Empfehlung: Teile diesen Link nur privat (z. B. per E-Mail oder Messenger), nicht in sozialen Medien.",
|
||||
"share_enable": "Öffentlichen Link aktivieren",
|
||||
"share_copied": "Link kopiert!",
|
||||
"share_copy_btn": "Link kopieren",
|
||||
@@ -560,6 +561,12 @@
|
||||
"body": "Du landest gleich im Statistik-Dashboard. Die Tour kannst du jederzeit unter Einstellungen erneut starten. Gute Fahrt!"
|
||||
}
|
||||
}
|
||||
},
|
||||
"seo": {
|
||||
"title": "Kapteins Daagbok – Kostenloses digitales Yacht-Logbuch (werbefrei)",
|
||||
"description": "Kostenloses, werbefreies digitales Yacht-Logbuch mit End-to-End-Verschlüsselung und Passkey-Anmeldung. Reisetage, GPS-Tracks, Crew und Schiffsdaten sicher dokumentieren – auch offline als PWA.",
|
||||
"keywords": "Yacht-Logbuch, Schiffstagebuch, Bordlogbuch, Segeln, Passkey, E2E-Verschlüsselung, GPS-Track, maritimes Logbuch, kostenlos, werbefrei, gratis, ohne Werbung",
|
||||
"ogImageAlt": "Kapteins Daagbok Logo"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -332,6 +332,7 @@
|
||||
"color_scheme_dark": "Dark",
|
||||
"share_title": "Share Logbook (Read-Only)",
|
||||
"share_desc": "Enable this to generate a public, read-only link. Anyone with the link can view your travels, yacht profile, and crew members. Decryption keys are never transmitted to the server (they stay in the hash part of the URL).",
|
||||
"share_privacy_warning": "Recommendation: Share this link only privately (e.g. via email or messenger), not on social media.",
|
||||
"share_enable": "Enable Public Link",
|
||||
"share_copied": "Link copied!",
|
||||
"share_copy_btn": "Copy Link",
|
||||
@@ -560,6 +561,12 @@
|
||||
"body": "You'll land on the statistics dashboard next. You can restart the tour anytime in Settings. Fair winds!"
|
||||
}
|
||||
}
|
||||
},
|
||||
"seo": {
|
||||
"title": "Kapteins Daagbok – Free Digital Yacht Logbook (Ad-Free)",
|
||||
"description": "Free, ad-free digital yacht logbook with end-to-end encryption and Passkey sign-in. Document travel days, GPS tracks, crew and vessel data securely — offline-capable PWA.",
|
||||
"keywords": "yacht logbook, ship logbook, sailing log, maritime logbook, passkey, E2E encryption, GPS track, free, ad-free, offline PWA",
|
||||
"ogImageAlt": "Kapteins Daagbok logo"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
import type { i18n as I18nInstance } from 'i18next'
|
||||
|
||||
const SITE_ORIGIN = 'https://kapteins-daagbok.eu'
|
||||
|
||||
export type SeoLang = 'de' | 'en'
|
||||
|
||||
let i18nRef: I18nInstance | null = null
|
||||
|
||||
export function normalizeSeoLang(lng: string): SeoLang {
|
||||
return lng.startsWith('de') ? 'de' : 'en'
|
||||
}
|
||||
|
||||
function setMeta(attr: 'name' | 'property', key: string, content: string) {
|
||||
let el = document.querySelector(`meta[${attr}="${key}"]`)
|
||||
if (!el) {
|
||||
el = document.createElement('meta')
|
||||
el.setAttribute(attr, key)
|
||||
document.head.appendChild(el)
|
||||
}
|
||||
el.setAttribute('content', content)
|
||||
}
|
||||
|
||||
function syncLanguageUrl(lang: SeoLang) {
|
||||
const url = new URL(window.location.href)
|
||||
url.searchParams.set('lng', lang)
|
||||
const next = `${url.pathname}${url.search}${url.hash}`
|
||||
window.history.replaceState({}, '', next)
|
||||
}
|
||||
|
||||
export function updatePageSeo(lng?: string) {
|
||||
if (!i18nRef?.isInitialized) return
|
||||
|
||||
const lang = normalizeSeoLang(lng ?? i18nRef.language)
|
||||
document.documentElement.lang = lang
|
||||
|
||||
const title = i18nRef.t('seo.title')
|
||||
document.title = title
|
||||
|
||||
const description = i18nRef.t('seo.description')
|
||||
const keywords = i18nRef.t('seo.keywords')
|
||||
const imageAlt = i18nRef.t('seo.ogImageAlt')
|
||||
|
||||
setMeta('name', 'description', description)
|
||||
setMeta('name', 'keywords', keywords)
|
||||
setMeta('property', 'og:title', title)
|
||||
setMeta('property', 'og:description', description)
|
||||
setMeta('property', 'og:locale', lang === 'de' ? 'de_DE' : 'en_US')
|
||||
setMeta('property', 'og:locale:alternate', lang === 'de' ? 'en_US' : 'de_DE')
|
||||
setMeta('name', 'twitter:title', title)
|
||||
setMeta('name', 'twitter:description', description)
|
||||
setMeta('property', 'og:image:alt', imageAlt)
|
||||
setMeta('name', 'twitter:image:alt', imageAlt)
|
||||
|
||||
syncLanguageUrl(lang)
|
||||
}
|
||||
|
||||
export function initSeo(i18n: I18nInstance) {
|
||||
i18nRef = i18n
|
||||
i18n.on('initialized', () => updatePageSeo())
|
||||
i18n.on('languageChanged', (lng) => updatePageSeo(lng))
|
||||
if (i18n.isInitialized) {
|
||||
updatePageSeo()
|
||||
}
|
||||
}
|
||||
|
||||
export function hreflangUrl(lang: SeoLang): string {
|
||||
return `${SITE_ORIGIN}/?lng=${lang}`
|
||||
}
|
||||
|
||||
export const seoSiteOrigin = SITE_ORIGIN
|
||||
@@ -49,7 +49,9 @@ export default defineConfig({
|
||||
manifest: {
|
||||
name: 'Kapteins Daagbok',
|
||||
short_name: 'Daagbok',
|
||||
description: 'Free, ad-free maritime logbook with E2E encryption and Passkeys',
|
||||
lang: 'de',
|
||||
description:
|
||||
'Digitales Yacht-Logbuch — E2E-verschlüsselt, offline-fähig. Digital yacht logbook — E2E encrypted, offline-capable PWA.',
|
||||
theme_color: '#1e293b',
|
||||
background_color: '#0f172a',
|
||||
display: 'standalone',
|
||||
|
||||
Reference in New Issue
Block a user