Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2eb6551200 | |||
| 9baaccf239 | |||
| df53420f3b | |||
| 5271ed90c1 | |||
| a8ba998444 | |||
| 67d169080e |
+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" />
|
||||
|
||||
@@ -401,6 +401,10 @@ export default function SettingsForm({ logbookId, onLogbookRestored }: SettingsF
|
||||
{t('settings.share_desc')}
|
||||
</p>
|
||||
|
||||
<p className="signature-lock-notice" style={{ marginBottom: '16px' }}>
|
||||
{t('settings.share_privacy_warning')}
|
||||
</p>
|
||||
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '10px', marginBottom: '20px' }}>
|
||||
<label className="switch-label" style={{ display: 'flex', alignItems: 'center', gap: '10px', cursor: 'pointer', fontSize: '14px', color: '#f1f5f9' }}>
|
||||
<input
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -169,6 +169,33 @@
|
||||
width: 4mm;
|
||||
}
|
||||
|
||||
.lang-list {
|
||||
display: inline-flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 1.5mm;
|
||||
}
|
||||
|
||||
.lang-item {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 1.2mm;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.feature-flag {
|
||||
display: inline-block;
|
||||
width: 5mm;
|
||||
height: 3.5mm;
|
||||
border-radius: 0.3mm;
|
||||
flex-shrink: 0;
|
||||
box-shadow: 0 0 0 0.15mm rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
|
||||
.lang-sep {
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.beta-box {
|
||||
background: rgba(30, 41, 59, 0.85);
|
||||
border: 1px solid rgba(251, 191, 36, 0.35);
|
||||
@@ -301,7 +328,7 @@
|
||||
<div class="feature"><span class="feature-icon">✦</span><span>Verschlüsseltes Backup & Wiederherstellung</span></div>
|
||||
<div class="feature"><span class="feature-icon">✦</span><span>Logbuch mit Freunden teilen</span></div>
|
||||
<div class="feature"><span class="feature-icon">✦</span><span>Beliebig viele Schiffe und Logbücher</span></div>
|
||||
<div class="feature"><span class="feature-icon">✦</span><span>Deutsch & Englisch</span></div>
|
||||
<div class="feature"><span class="feature-icon">✦</span><span class="lang-list"><span class="lang-item"><svg class="feature-flag" viewBox="0 0 5 3" xmlns="http://www.w3.org/2000/svg" aria-hidden="true"><rect width="5" height="1" fill="#000"/><rect y="1" width="5" height="1" fill="#D00"/><rect y="2" width="5" height="1" fill="#FFCE00"/></svg>Deutsch</span><span class="lang-sep">&</span><span class="lang-item"><svg class="feature-flag" viewBox="0 0 60 30" xmlns="http://www.w3.org/2000/svg" aria-hidden="true"><clipPath id="gb-a"><path d="M0 0v30h60V0z"/></clipPath><clipPath id="gb-b"><path d="M30 15h30v15zv15H0z"/></clipPath><g clip-path="url(#gb-a)"><path d="M0 0v30h60V0z" fill="#012169"/><path d="M0 0l60 30m0-30L0 30" stroke="#fff" stroke-width="6"/><path d="M0 0l60 30m0-30L0 30" clip-path="url(#gb-b)" stroke="#C8102E" stroke-width="4"/><path d="M30 0v30M0 15h60" stroke="#fff" stroke-width="10"/><path d="M30 0v30M0 15h60" stroke="#C8102E" stroke-width="6"/></g></svg>Englisch</span></span></div>
|
||||
<div class="feature"><span class="feature-icon">✦</span><span>Crafted in Kiel.Sailing.City.</span></div>
|
||||
</section>
|
||||
|
||||
|
||||
Binary file not shown.
Reference in New Issue
Block a user