feat(website): bilingual i18n (English default, Danish at /da) + SEO

Add @nuxtjs/i18n: English is the default locale (no prefix), Danish lives
under /da (prefix_except_default). Both server-rendered and indexed with
hreflang alternates + per-locale canonical (useLocaleHead in app.vue).
First-visit browser language is auto-detected and remembered in the
i18n_redirected cookie (redirectOn root).

- Keep the hand-authored COPY object; useLang/useCopy now read the i18n
  locale; useLocalizeHref/useLangToggle added. Every internal link is
  localized so navigation stays in-locale.
- Clear segmented EN|DA language switcher (active segment filled) replacing
  the ambiguous "en · da" pill.
- SEO: useSeoMeta defaults are locale-aware; og:image switches per locale
  (English / Danish share card); favicon links; robots.txt + sitemap.xml;
  env-aware siteUrl/baseUrl (localhost in dev, dezky.eu in prod).
- Update the cookie policy (dezky-lang -> i18n_redirected).
- Gate the Traefik wss:443 HMR behind DEZKY_TRAEFIK so localhost dev/DevTools
  connect over plain ws (fixes the DevTools disconnect loop).
This commit is contained in:
Ronni Baslund
2026-06-06 20:46:26 +02:00
parent 3f0298e011
commit 7bee161ac1
24 changed files with 1812 additions and 107 deletions
+51 -7
View File
@@ -6,10 +6,15 @@
// the Docker app stack. Locally it runs behind Traefik at dezky.local /
// www.dezky.local with the same mkcert TLS as the rest of the platform.
const siteUrl = process.env.NUXT_PUBLIC_SITE_URL
|| (process.env.NODE_ENV === 'production' ? 'https://dezky.eu' : 'http://localhost:3000')
export default defineNuxtConfig({
compatibilityDate: '2026-01-01',
devtools: { enabled: true },
modules: ['@nuxtjs/i18n'],
css: ['~/assets/styles/tokens.css', '~/assets/styles/base.css'],
// Auto-import from the shared packages/ui workspace in addition to the
@@ -25,10 +30,12 @@ export default defineNuxtConfig({
app: {
head: {
// Marketing site is light by default (the design's primary theme). The
// page sets <html lang> reactively based on the da/en toggle.
htmlAttrs: { lang: 'da' },
// <html lang>/dir + hreflang alternates are managed by @nuxtjs/i18n
// (useLocaleHead in app.vue). Static favicons + theme-color live here.
link: [
{ rel: 'icon', type: 'image/svg+xml', href: '/favicon.svg' },
{ rel: 'icon', type: 'image/png', sizes: '32x32', href: '/favicon-32.png' },
{ rel: 'apple-touch-icon', href: '/apple-touch-icon.png' },
{ rel: 'preconnect', href: 'https://fonts.googleapis.com' },
{ rel: 'preconnect', href: 'https://fonts.gstatic.com', crossorigin: '' },
{
@@ -36,6 +43,41 @@ export default defineNuxtConfig({
href: 'https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Inter+Tight:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600;700&display=swap',
},
],
// theme-color + favicons are static; the OG/Twitter/canonical SEO is set
// in app.vue via useSeoMeta so og:url/og:image are absolute against the
// env-aware siteUrl (runtimeConfig below) and og:url is per-route.
meta: [
{ name: 'theme-color', content: '#FAFAF7' },
],
},
},
// Public site URL used to build absolute canonical / OG / sitemap URLs.
// Defaults to localhost in dev (so local share-image previews work) and
// the production domain otherwise; override with NUXT_PUBLIC_SITE_URL.
runtimeConfig: {
public: {
siteUrl,
},
},
// Bilingual: English is the default (no prefix); Danish lives under /da.
// Both are server-rendered and indexed with hreflang alternates. First-time
// visitors are auto-detected (cookie-remembered) on the root path only.
i18n: {
defaultLocale: 'en',
strategy: 'prefix_except_default',
baseUrl: siteUrl,
locales: [
{ code: 'en', language: 'en', name: 'English' },
{ code: 'da', language: 'da-DK', name: 'Dansk' },
],
detectBrowserLanguage: {
useCookie: true,
cookieKey: 'i18n_redirected',
redirectOn: 'root',
fallbackLocale: 'en',
alwaysRedirect: false,
},
},
@@ -44,10 +86,12 @@ export default defineNuxtConfig({
// Vite 7 added a strict host check; allow the Traefik-fronted hostnames
// this site is served on in dev.
allowedHosts: ['dezky.local', 'www.dezky.local'],
hmr: {
protocol: 'wss',
clientPort: 443,
},
// HMR/DevTools websocket. Default (localhost:3000 dev) lets Vite infer
// ws on the page host/port. When served behind Traefik TLS at
// dezky.local, set DEZKY_TRAEFIK=1 so the client connects via wss:443.
hmr: process.env.DEZKY_TRAEFIK === '1'
? { protocol: 'wss', clientPort: 443 }
: undefined,
},
},
})