diff --git a/frontend/index.html b/frontend/index.html index 38747f1..2ce889f 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -26,6 +26,9 @@ + + + @@ -33,11 +36,26 @@
diff --git a/frontend/src/i18n/index.js b/frontend/src/i18n/index.js index 564f436..23ea99e 100644 --- a/frontend/src/i18n/index.js +++ b/frontend/src/i18n/index.js @@ -19,6 +19,7 @@ import enBlog from './locales/en/blog.json'; import enMinigames from './locales/en/minigames.json'; import enMessage from './locales/en/message.json'; import enPersonal from './locales/en/personal.json'; +import enSeo from './locales/en/seo.json'; import cebGeneral from './locales/ceb/general.json'; import cebHeader from './locales/ceb/header.json'; import cebNavigation from './locales/ceb/navigation.json'; @@ -37,6 +38,7 @@ import cebPersonal from './locales/ceb/personal.json'; import cebFalukant from './locales/ceb/falukant.json'; import cebBlog from './locales/ceb/blog.json'; import cebMinigames from './locales/ceb/minigames.json'; +import cebSeo from './locales/ceb/seo.json'; import deGeneral from './locales/de/general.json'; import deHeader from './locales/de/header.json'; @@ -56,6 +58,7 @@ import deBlog from './locales/de/blog.json'; import deMinigames from './locales/de/minigames.json'; import deMessage from './locales/de/message.json'; import dePersonal from './locales/de/personal.json'; +import deSeo from './locales/de/seo.json'; import esGeneral from './locales/es/general.json'; import esHeader from './locales/es/header.json'; @@ -75,6 +78,7 @@ import esBlog from './locales/es/blog.json'; import esMinigames from './locales/es/minigames.json'; import esMessage from './locales/es/message.json'; import esPersonal from './locales/es/personal.json'; +import esSeo from './locales/es/seo.json'; const messages = { en: { @@ -96,6 +100,7 @@ const messages = { ...enMinigames, ...enMessage, ...enPersonal, + ...enSeo, }, ceb: { ...enGeneral, @@ -134,6 +139,7 @@ const messages = { ...cebFalukant, ...cebBlog, ...cebMinigames, + ...cebSeo, }, de: { 'Ok': 'Ok', @@ -155,6 +161,7 @@ const messages = { ...deMinigames, ...deMessage, ...dePersonal, + ...deSeo, }, es: { ...esGeneral, @@ -175,6 +182,7 @@ const messages = { ...esMinigames, ...esMessage, ...esPersonal, + ...esSeo, } }; diff --git a/frontend/src/i18n/locales/ceb/seo.json b/frontend/src/i18n/locales/ceb/seo.json new file mode 100644 index 0000000..71997db --- /dev/null +++ b/frontend/src/i18n/locales/ceb/seo.json @@ -0,0 +1,51 @@ +{ + "seo": { + "default": { + "title": "YourPart - Komunidad, chat, forum, bokabularyo, Falukant ug minigames", + "description": "YourPart naghiusa sa komunidad, chat, forum, blog, trainer sa bokabularyo, ang browser builder game nga Falukant ug minigames sa usa ka plataporma.", + "keywords": "YourPart, komunidad, chat, forum, blog, bokabularyo, Falukant, minigames, libre, pribado, privacy" + }, + "home": { + "title": "YourPart - Komunidad, chat, forum, blogs, bokabularyo ug mga dula", + "description": "YourPart usa ka plataporma sa komunidad nga adunay chat, forum, blogs, trainer sa bokabularyo, ang browser game nga Falukant ug minigames.", + "keywords": "YourPart, komunidad, chat, forum, blogs, bokabularyo, browser game, Falukant, minigames, libre, pribado", + "jsonLdDescription": "Plataporma sa komunidad nga adunay chat, forum, blogs, bokabularyo, Falukant ug minigames sa browser." + }, + "falukant": { + "title": "Falukant - Medieval nga browser builder game sa YourPart", + "description": "Falukant ang medieval nga browser builder game sa YourPart nga adunay komersyo, politika, pamilya, edukasyon ug pag-uswag sa karakter.", + "keywords": "Falukant, browser game, builder, medieval, ekonomiya, politika, YourPart, libre", + "jsonLdDescription": "Medieval nga browser builder game nga adunay komersyo, politika, pamilya ug pag-uswag sa karakter.", + "jsonLdName": "Falukant" + }, + "minigames": { + "title": "Minigames sa YourPart - Match 3 ug taxi sa browser", + "description": "Diskubreha ang minigames sa browser sa YourPart: Match 3 ug taxi naghatag og paspas nga mga round sa plataporma.", + "keywords": "minigames, browser games, Match 3, taxi, casual, YourPart, libre", + "jsonLdDescription": "Minigames sa browser sa YourPart nga adunay Match 3 ug taxi.", + "jsonLdCollectionName": "Minigames sa YourPart" + }, + "vocab": { + "title": "Trainer sa bokabularyo sa YourPart - pagkat-on og pinulongan online", + "description": "Ang trainer sa bokabularyo sa YourPart motabang kanimo sa pagkat-on og pinulongan nga adunay interaktibong leksyon, kurso ug ehersisyo.", + "keywords": "bokabularyo, pagkat-on pinulongan, online, kurso, ehersisyo, YourPart, libre", + "jsonLdDescription": "Interaktibong trainer sa bokabularyo nga adunay kurso, leksyon ug ehersisyo.", + "jsonLdName": "Bokabularyo YourPart" + }, + "blogList": { + "title": "Blogs sa YourPart - mga post ug hilisgutan sa komunidad", + "description": "Tan-awa ang publiko nga blogs sa YourPart nga adunay mga post, hunahuna, kasinatian ug hilisgutan.", + "keywords": "blogs, blog sa komunidad, artikulo, post, YourPart", + "jsonLdDescription": "Publiko nga blogs ug mga post sa komunidad sa YourPart.", + "jsonLdName": "Blogs sa YourPart" + }, + "blogPage": { + "title": "Blogs sa YourPart", + "description": "Publiko nga blogs, post ug sulod sa komunidad sa YourPart.", + "keywords": "blog, YourPart, komunidad" + }, + "blogPost": { + "pageTitle": "{title} | Blog sa YourPart" + } + } +} diff --git a/frontend/src/i18n/locales/de/seo.json b/frontend/src/i18n/locales/de/seo.json new file mode 100644 index 0000000..43e2340 --- /dev/null +++ b/frontend/src/i18n/locales/de/seo.json @@ -0,0 +1,51 @@ +{ + "seo": { + "default": { + "title": "YourPart - Community, Chat, Forum, Vokabeltrainer, Falukant und Minispiele", + "description": "YourPart verbindet Community, Chat, Forum, Blogs, Vokabeltrainer, das Aufbauspiel Falukant und Browser-Minispiele auf einer Plattform.", + "keywords": "YourPart, Community, Chat, Forum, Blog, Vokabeltrainer, Falukant, Minispiele, kostenlos, privat, Datenschutz" + }, + "home": { + "title": "YourPart - Community, Chat, Forum, Blogs, Vokabeltrainer und Spiele", + "description": "YourPart ist eine Community-Plattform mit Chat, Forum, Blogs, Vokabeltrainer, dem Browser-Aufbauspiel Falukant und Minispielen.", + "keywords": "YourPart, Community, Chat, Forum, Blogs, Vokabeltrainer, Browsergame, Falukant, Minispiele, kostenlos, privat", + "jsonLdDescription": "Community-Plattform mit Chat, Forum, Blogs, Vokabeltrainer, Falukant und Browser-Minispielen." + }, + "falukant": { + "title": "Falukant - Mittelalterliches Browser-Aufbauspiel auf YourPart", + "description": "Falukant ist das mittelalterliche Browser-Aufbauspiel auf YourPart mit Handel, Politik, Familie, Bildung und Charakterentwicklung.", + "keywords": "Falukant, Browsergame, Aufbauspiel, Mittelalterspiel, Wirtschaftsspiel, Politikspiel, YourPart, kostenlos", + "jsonLdDescription": "Mittelalterliches Browser-Aufbauspiel mit Handel, Politik, Familie und Charakterentwicklung.", + "jsonLdName": "Falukant" + }, + "minigames": { + "title": "Minispiele auf YourPart - Match 3 und Taxi im Browser", + "description": "Entdecke die Browser-Minispiele auf YourPart: Match 3 und Taxi bieten schnelle Spielrunden direkt auf der Plattform.", + "keywords": "Minispiele, Browsergames, Match 3, Taxi Spiel, Casual Games, YourPart, kostenlos", + "jsonLdDescription": "Browser-Minispiele auf YourPart mit Match 3 und Taxi.", + "jsonLdCollectionName": "YourPart Minispiele" + }, + "vocab": { + "title": "Vokabeltrainer auf YourPart - Sprachen online lernen", + "description": "Der Vokabeltrainer auf YourPart unterstützt dich beim Sprachenlernen mit interaktiven Lektionen, Kursen und Übungen.", + "keywords": "Vokabeltrainer, Sprachen lernen, Online lernen, Sprachkurse, Übungen, YourPart, kostenlos", + "jsonLdDescription": "Interaktiver Vokabeltrainer mit Kursen, Lektionen und Übungen zum Sprachenlernen.", + "jsonLdName": "YourPart Vokabeltrainer" + }, + "blogList": { + "title": "Blogs auf YourPart - Community-Beiträge und Themen", + "description": "Entdecke öffentliche Blogs auf YourPart mit Community-Beiträgen, Gedanken, Erfahrungen und Themen aus verschiedenen Bereichen.", + "keywords": "Blogs, Community Blog, Artikel, Beiträge, YourPart", + "jsonLdDescription": "Öffentliche Blogs und Community-Beiträge auf YourPart.", + "jsonLdName": "Blogs auf YourPart" + }, + "blogPage": { + "title": "Blogs auf YourPart", + "description": "Öffentliche Blogs, Beiträge und Community-Inhalte auf YourPart.", + "keywords": "Blog, YourPart, Community" + }, + "blogPost": { + "pageTitle": "{title} | Blog auf YourPart" + } + } +} diff --git a/frontend/src/i18n/locales/en/seo.json b/frontend/src/i18n/locales/en/seo.json new file mode 100644 index 0000000..9ebb18e --- /dev/null +++ b/frontend/src/i18n/locales/en/seo.json @@ -0,0 +1,51 @@ +{ + "seo": { + "default": { + "title": "YourPart - Community, chat, forum, vocabulary trainer, Falukant and minigames", + "description": "YourPart brings together community, chat, forums, blogs, a vocabulary trainer, the browser builder game Falukant and minigames in one platform.", + "keywords": "YourPart, community, chat, forum, blog, vocabulary trainer, Falukant, minigames, free, private, privacy" + }, + "home": { + "title": "YourPart - Community, chat, forum, blogs, vocabulary trainer and games", + "description": "YourPart is a community platform with chat, forums, blogs, a vocabulary trainer, the browser builder game Falukant and minigames.", + "keywords": "YourPart, community, chat, forum, blogs, vocabulary trainer, browser game, Falukant, minigames, free, private", + "jsonLdDescription": "Community platform with chat, forums, blogs, vocabulary trainer, Falukant and browser minigames." + }, + "falukant": { + "title": "Falukant - Medieval browser builder game on YourPart", + "description": "Falukant is the medieval browser builder game on YourPart with trade, politics, family, education and character progression.", + "keywords": "Falukant, browser game, builder game, medieval game, economy game, politics game, YourPart, free", + "jsonLdDescription": "Medieval browser builder game with trade, politics, family and character progression.", + "jsonLdName": "Falukant" + }, + "minigames": { + "title": "Minigames on YourPart - Match 3 and taxi in the browser", + "description": "Discover browser minigames on YourPart: Match 3 and taxi offer quick rounds directly on the platform.", + "keywords": "minigames, browser games, Match 3, taxi game, casual games, YourPart, free", + "jsonLdDescription": "Browser minigames on YourPart with Match 3 and taxi.", + "jsonLdCollectionName": "YourPart minigames" + }, + "vocab": { + "title": "Vocabulary trainer on YourPart - learn languages online", + "description": "The YourPart vocabulary trainer helps you learn languages with interactive lessons, courses and exercises.", + "keywords": "vocabulary trainer, learn languages, online learning, language courses, exercises, YourPart, free", + "jsonLdDescription": "Interactive vocabulary trainer with courses, lessons and exercises for language learning.", + "jsonLdName": "YourPart vocabulary trainer" + }, + "blogList": { + "title": "Blogs on YourPart - community posts and topics", + "description": "Explore public blogs on YourPart with community posts, ideas, experiences and topics from many areas.", + "keywords": "blogs, community blog, articles, posts, YourPart", + "jsonLdDescription": "Public blogs and community posts on YourPart.", + "jsonLdName": "Blogs on YourPart" + }, + "blogPage": { + "title": "Blogs on YourPart", + "description": "Public blogs, posts and community content on YourPart.", + "keywords": "blog, YourPart, community" + }, + "blogPost": { + "pageTitle": "{title} | Blog on YourPart" + } + } +} diff --git a/frontend/src/i18n/locales/es/seo.json b/frontend/src/i18n/locales/es/seo.json new file mode 100644 index 0000000..25b3686 --- /dev/null +++ b/frontend/src/i18n/locales/es/seo.json @@ -0,0 +1,51 @@ +{ + "seo": { + "default": { + "title": "YourPart - Comunidad, chat, foro, vocabulario, Falukant y minijuegos", + "description": "YourPart reúne comunidad, chat, foros, blogs, un entrenador de vocabulario, el juego de construcción Falukant y minijuegos en un navegador.", + "keywords": "YourPart, comunidad, chat, foro, blog, vocabulario, Falukant, minijuegos, gratis, privado, privacidad" + }, + "home": { + "title": "YourPart - Comunidad, chat, foro, blogs, vocabulario y juegos", + "description": "YourPart es una plataforma comunitaria con chat, foro, blogs, entrenador de vocabulario, el juego de construcción Falukant y minijuegos.", + "keywords": "YourPart, comunidad, chat, foro, blogs, vocabulario, juego navegador, Falukant, minijuegos, gratis, privado", + "jsonLdDescription": "Plataforma comunitaria con chat, foro, blogs, vocabulario, Falukant y minijuegos en el navegador." + }, + "falukant": { + "title": "Falukant - Juego de construcción medieval en el navegador en YourPart", + "description": "Falukant es el juego de construcción medieval en YourPart con comercio, política, familia, educación y progresión del personaje.", + "keywords": "Falukant, juego navegador, construcción, medieval, economía, política, YourPart, gratis", + "jsonLdDescription": "Juego de construcción medieval en el navegador con comercio, política, familia y progresión.", + "jsonLdName": "Falukant" + }, + "minigames": { + "title": "Minijuegos en YourPart - Match 3 y taxi en el navegador", + "description": "Descubre minijuegos en el navegador en YourPart: Match 3 y taxi ofrecen partidas rápidas en la plataforma.", + "keywords": "minijuegos, juegos navegador, Match 3, taxi, casual, YourPart, gratis", + "jsonLdDescription": "Minijuegos en el navegador en YourPart con Match 3 y taxi.", + "jsonLdCollectionName": "Minijuegos YourPart" + }, + "vocab": { + "title": "Entrenador de vocabulario en YourPart - aprende idiomas online", + "description": "El entrenador de vocabulario de YourPart te ayuda a aprender idiomas con lecciones interactivas, cursos y ejercicios.", + "keywords": "vocabulario, aprender idiomas, online, cursos, ejercicios, YourPart, gratis", + "jsonLdDescription": "Entrenador de vocabulario interactivo con cursos, lecciones y ejercicios.", + "jsonLdName": "Vocabulario YourPart" + }, + "blogList": { + "title": "Blogs en YourPart - aportes y temas de la comunidad", + "description": "Descubre blogs públicos en YourPart con aportes, ideas, experiencias y temas de distintas áreas.", + "keywords": "blogs, blog comunitario, artículos, entradas, YourPart", + "jsonLdDescription": "Blogs públicos y aportes de la comunidad en YourPart.", + "jsonLdName": "Blogs en YourPart" + }, + "blogPage": { + "title": "Blogs en YourPart", + "description": "Blogs públicos, entradas y contenido comunitario en YourPart.", + "keywords": "blog, YourPart, comunidad" + }, + "blogPost": { + "pageTitle": "{title} | Blog en YourPart" + } + } +} diff --git a/frontend/src/main.js b/frontend/src/main.js index 57cb43a..ad47c77 100644 --- a/frontend/src/main.js +++ b/frontend/src/main.js @@ -4,6 +4,7 @@ import store from './store'; import router from './router'; import './assets/styles.scss'; import i18n from './i18n'; +import { setSeoI18nAccessor, applyRouteSeo } from './utils/seo'; import { createVuetify } from 'vuetify'; import * as components from 'vuetify/components'; import * as directives from 'vuetify/directives'; @@ -53,7 +54,28 @@ function getBrowserLanguage() { const SUPPORTED_UI_LOCALES = ['de', 'en', 'ceb', 'es']; +function readLangFromUrl() { + try { + const q = new URLSearchParams(window.location.search).get('lang'); + if (q && SUPPORTED_UI_LOCALES.includes(q)) { + return q; + } + } catch (_) { + /* ignore */ + } + return null; +} + function getInitialAppLanguage() { + const fromUrl = readLangFromUrl(); + if (fromUrl) { + try { + localStorage.setItem('uiLanguage', fromUrl); + } catch (_) { + /* ignore */ + } + return fromUrl; + } try { const saved = localStorage.getItem('uiLanguage'); if (saved && SUPPORTED_UI_LOCALES.includes(saved)) { @@ -72,6 +94,19 @@ const vuetify = createVuetify({ store.dispatch('setLanguage', getInitialAppLanguage()); +setSeoI18nAccessor(() => ({ + t: (...args) => i18n.global.t(...args), + te: (...args) => i18n.global.te(...args), + locale: store.state.language, +})); + +store.watch( + (state) => state.language, + () => { + applyRouteSeo(router.currentRoute.value); + } +); + const app = createApp(App); app.use(store); diff --git a/frontend/src/router/blogRoutes.js b/frontend/src/router/blogRoutes.js index c9d3051..bb1ecb3 100644 --- a/frontend/src/router/blogRoutes.js +++ b/frontend/src/router/blogRoutes.js @@ -1,7 +1,6 @@ const BlogListView = () => import('@/views/blog/BlogListView.vue'); const BlogView = () => import('@/views/blog/BlogView.vue'); const BlogEditorView = () => import('@/views/blog/BlogEditorView.vue'); -import { buildAbsoluteUrl } from '@/utils/seo.js'; export default [ { path: '/blogs/create', name: 'BlogCreate', component: BlogEditorView, meta: { requiresAuth: true } }, @@ -14,8 +13,7 @@ export default [ props: route => ({ slug: route.params.slug }), meta: { seo: { - title: 'Blogs auf YourPart', - description: 'Öffentliche Blogs, Beiträge und Community-Inhalte auf YourPart.', + i18nKey: 'blogPage', }, }, }, @@ -27,8 +25,7 @@ export default [ props: true, meta: { seo: { - title: 'Blogs auf YourPart', - description: 'Öffentliche Blogs, Beiträge und Community-Inhalte auf YourPart.', + i18nKey: 'blogPage', }, }, }, @@ -38,20 +35,8 @@ export default [ component: BlogListView, meta: { seo: { - title: 'Blogs auf YourPart - Community-Beiträge und Themen', - description: 'Entdecke öffentliche Blogs auf YourPart mit Community-Beiträgen, Gedanken, Erfahrungen und Themen aus verschiedenen Bereichen.', - keywords: 'Blogs, Community Blog, Artikel, Beiträge, YourPart', + i18nKey: 'blogList', canonicalPath: '/blogs', - jsonLd: [ - { - '@context': 'https://schema.org', - '@type': 'CollectionPage', - name: 'Blogs auf YourPart', - url: buildAbsoluteUrl('/blogs'), - description: 'Öffentliche Blogs und Community-Beiträge auf YourPart.', - inLanguage: 'de', - }, - ], }, }, }, diff --git a/frontend/src/router/index.js b/frontend/src/router/index.js index b80df89..4c18b8d 100644 --- a/frontend/src/router/index.js +++ b/frontend/src/router/index.js @@ -9,7 +9,7 @@ import blogRoutes from './blogRoutes'; import minigamesRoutes from './minigamesRoutes'; import personalRoutes from './personalRoutes'; import marketingRoutes from './marketingRoutes'; -import { applyRouteSeo, buildAbsoluteUrl } from '../utils/seo'; +import { applyRouteSeo } from '../utils/seo'; import apiClient from '../utils/axios'; const HomeView = () => import('../views/HomeView.vue'); @@ -21,25 +21,8 @@ const routes = [ component: HomeView, meta: { seo: { - title: 'YourPart - Community, Chat, Forum, Blogs, Vokabeltrainer und Spiele', - description: 'YourPart ist eine Community-Plattform mit Chat, Forum, Blogs, Vokabeltrainer, dem Browser-Aufbauspiel Falukant und Minispielen.', - keywords: 'YourPart, Community, Chat, Forum, Blogs, Vokabeltrainer, Browsergame, Falukant, Minispiele', + i18nKey: 'home', canonicalPath: '/', - jsonLd: [ - { - '@context': 'https://schema.org', - '@type': 'WebSite', - name: 'YourPart', - url: buildAbsoluteUrl('/'), - inLanguage: 'de', - description: 'Community-Plattform mit Chat, Forum, Blogs, Vokabeltrainer, Falukant und Browser-Minispielen.', - potentialAction: { - '@type': 'SearchAction', - target: `${buildAbsoluteUrl('/blogs')}?q={search_term_string}`, - 'query-input': 'required name=search_term_string', - }, - }, - ], }, }, }, diff --git a/frontend/src/router/marketingRoutes.js b/frontend/src/router/marketingRoutes.js index f100816..03c5505 100644 --- a/frontend/src/router/marketingRoutes.js +++ b/frontend/src/router/marketingRoutes.js @@ -1,5 +1,3 @@ -import { buildAbsoluteUrl } from '../utils/seo'; - const FalukantLandingView = () => import('../views/public/FalukantLandingView.vue'); const MinigamesLandingView = () => import('../views/public/MinigamesLandingView.vue'); const VocabLandingView = () => import('../views/public/VocabLandingView.vue'); @@ -11,26 +9,8 @@ const marketingRoutes = [ component: FalukantLandingView, meta: { seo: { - title: 'Falukant - Mittelalterliches Browser-Aufbauspiel auf YourPart', - description: 'Falukant ist das mittelalterliche Browser-Aufbauspiel auf YourPart mit Handel, Politik, Familie, Bildung und Charakterentwicklung.', - keywords: 'Falukant, Browsergame, Aufbauspiel, Mittelalterspiel, Wirtschaftsspiel, Politikspiel, YourPart', + i18nKey: 'falukant', canonicalPath: '/falukant', - jsonLd: [ - { - '@context': 'https://schema.org', - '@type': 'VideoGame', - name: 'Falukant', - url: buildAbsoluteUrl('/falukant'), - description: 'Mittelalterliches Browser-Aufbauspiel mit Handel, Politik, Familie und Charakterentwicklung.', - gamePlatform: 'Web Browser', - applicationCategory: 'Game', - inLanguage: 'de', - publisher: { - '@type': 'Organization', - name: 'YourPart', - }, - }, - ], }, }, }, @@ -40,20 +20,8 @@ const marketingRoutes = [ component: MinigamesLandingView, meta: { seo: { - title: 'Minispiele auf YourPart - Match 3 und Taxi im Browser', - description: 'Entdecke die Browser-Minispiele auf YourPart: Match 3 und Taxi bieten schnelle Spielrunden direkt auf der Plattform.', - keywords: 'Minispiele, Browsergames, Match 3, Taxi Spiel, Casual Games, YourPart', + i18nKey: 'minigames', canonicalPath: '/minigames', - jsonLd: [ - { - '@context': 'https://schema.org', - '@type': 'CollectionPage', - name: 'YourPart Minispiele', - url: buildAbsoluteUrl('/minigames'), - description: 'Browser-Minispiele auf YourPart mit Match 3 und Taxi.', - inLanguage: 'de', - }, - ], }, }, }, @@ -63,22 +31,8 @@ const marketingRoutes = [ component: VocabLandingView, meta: { seo: { - title: 'Vokabeltrainer auf YourPart - Sprachen online lernen', - description: 'Der Vokabeltrainer auf YourPart unterstützt dich beim Sprachenlernen mit interaktiven Lektionen, Kursen und Übungen.', - keywords: 'Vokabeltrainer, Sprachen lernen, Online lernen, Sprachkurse, Übungen, YourPart', + i18nKey: 'vocab', canonicalPath: '/vokabeltrainer', - jsonLd: [ - { - '@context': 'https://schema.org', - '@type': 'SoftwareApplication', - name: 'YourPart Vokabeltrainer', - url: buildAbsoluteUrl('/vokabeltrainer'), - description: 'Interaktiver Vokabeltrainer mit Kursen, Lektionen und Übungen zum Sprachenlernen.', - applicationCategory: 'EducationalApplication', - operatingSystem: 'Web', - inLanguage: 'de', - }, - ], }, }, }, diff --git a/frontend/src/utils/seo.js b/frontend/src/utils/seo.js index 0a9d9cb..a86e83a 100644 --- a/frontend/src/utils/seo.js +++ b/frontend/src/utils/seo.js @@ -5,6 +5,22 @@ const DEFAULT_SITE_NAME = 'YourPart'; const DEFAULT_TITLE = 'YourPart - Community, Chat, Forum, Vokabeltrainer, Falukant und Minispiele'; const DEFAULT_DESCRIPTION = 'YourPart verbindet Community, Chat, Forum, Blogs, Vokabeltrainer, das Aufbauspiel Falukant und Browser-Minispiele auf einer Plattform.'; const DEFAULT_IMAGE = `${DEFAULT_BASE_URL}/images/logos/logo.png`; +const SEO_UI_LOCALES = ['de', 'en', 'es', 'ceb']; + +const HREFLANG_FOR_UI = { + de: 'de', + en: 'en', + es: 'es', + ceb: 'ceb', +}; + +const OG_LOCALE_FOR_UI = { + de: 'de_DE', + en: 'en_GB', + es: 'es_ES', + ceb: 'ceb_PH', +}; + const MANAGED_META_KEYS = [ ['name', 'description'], ['name', 'keywords'], @@ -22,6 +38,13 @@ const MANAGED_META_KEYS = [ ['property', 'og:image'], ]; +/** @type {null | (() => { t: Function, te: Function, locale: string })} */ +let getSeoI18n = null; + +export function setSeoI18nAccessor(fn) { + getSeoI18n = typeof fn === 'function' ? fn : null; +} + function getBaseUrl() { return getPublicBaseUrl().replace(/\/$/, '') || DEFAULT_BASE_URL; } @@ -50,6 +73,14 @@ function clearManagedJsonLd() { document.head.querySelectorAll('script[data-seo-managed="true"]').forEach((node) => node.remove()); } +function clearManagedHreflang() { + document.head.querySelectorAll('link[data-seo-managed="hreflang"]').forEach((node) => node.remove()); +} + +function clearManagedOgLocaleAlternates() { + document.head.querySelectorAll('meta[data-seo-managed="og-locale-alt"]').forEach((node) => node.remove()); +} + export function buildAbsoluteUrl(path = '/') { if (/^https?:\/\//i.test(path)) { return path; @@ -59,6 +90,159 @@ export function buildAbsoluteUrl(path = '/') { return `${getBaseUrl()}${normalizedPath}`; } +/** + * URL für hreflang: Deutsch ohne ?lang=, andere Sprachen mit ?lang=. + */ +export function buildHreflangUrl(canonicalPath = '/', uiLocale = 'de') { + const base = buildAbsoluteUrl(canonicalPath).split('?')[0]; + if (uiLocale === 'de') { + return base; + } + const sep = base.includes('?') ? '&' : '?'; + return `${base}${sep}lang=${encodeURIComponent(uiLocale)}`; +} + +function appendHreflangAlternate(canonicalPath) { + SEO_UI_LOCALES.forEach((ui) => { + const link = document.createElement('link'); + link.setAttribute('rel', 'alternate'); + link.setAttribute('hreflang', HREFLANG_FOR_UI[ui] || ui); + link.setAttribute('href', buildHreflangUrl(canonicalPath, ui)); + link.dataset.seoManaged = 'hreflang'; + document.head.appendChild(link); + }); + const xDefault = document.createElement('link'); + xDefault.setAttribute('rel', 'alternate'); + xDefault.setAttribute('hreflang', 'x-default'); + xDefault.setAttribute('href', buildHreflangUrl(canonicalPath, 'de')); + xDefault.dataset.seoManaged = 'hreflang'; + document.head.appendChild(xDefault); +} + +function appendOgLocaleAlternates(currentOgLocale) { + const used = new Set([currentOgLocale]); + Object.values(OG_LOCALE_FOR_UI).forEach((og) => { + if (used.has(og)) { + return; + } + used.add(og); + const meta = document.createElement('meta'); + meta.setAttribute('property', 'og:locale:alternate'); + meta.setAttribute('content', og); + meta.dataset.seoManaged = 'og-locale-alt'; + document.head.appendChild(meta); + }); +} + +function uiLocaleToOgLocale(ui) { + return OG_LOCALE_FOR_UI[ui] || OG_LOCALE_FOR_UI.de; +} + +/** OpenGraph locale (z. B. de_DE) aus UI-Sprache (de|en|es|ceb). */ +export function seoOgLocale(ui) { + return uiLocaleToOgLocale(ui); +} + +export function seoHtmlLang(ui) { + return uiLocaleToHtmlLang(ui); +} + +function uiLocaleToHtmlLang(ui) { + return HREFLANG_FOR_UI[ui] || 'de'; +} + +export function seoSchemaLang(ui) { + return HREFLANG_FOR_UI[ui] || 'de'; +} + +/** + * JSON-LD für öffentliche Marketing-/Start-Routen (sprachabhängig). + */ +export function buildRouteJsonLd(route, t, uiLocale) { + const lang = seoSchemaLang(uiLocale); + const name = route.name; + + if (name === 'Home') { + return [ + { + '@context': 'https://schema.org', + '@type': 'WebSite', + name: DEFAULT_SITE_NAME, + url: buildAbsoluteUrl('/'), + inLanguage: lang, + description: t('seo.home.jsonLdDescription'), + potentialAction: { + '@type': 'SearchAction', + target: `${buildAbsoluteUrl('/blogs')}?q={search_term_string}`, + 'query-input': 'required name=search_term_string', + }, + }, + ]; + } + + if (name === 'FalukantLanding') { + return [ + { + '@context': 'https://schema.org', + '@type': 'VideoGame', + name: t('seo.falukant.jsonLdName'), + url: buildAbsoluteUrl('/falukant'), + description: t('seo.falukant.jsonLdDescription'), + gamePlatform: 'Web Browser', + applicationCategory: 'Game', + inLanguage: lang, + publisher: { + '@type': 'Organization', + name: DEFAULT_SITE_NAME, + }, + }, + ]; + } + + if (name === 'MinigamesLanding') { + return [ + { + '@context': 'https://schema.org', + '@type': 'CollectionPage', + name: t('seo.minigames.jsonLdCollectionName'), + url: buildAbsoluteUrl('/minigames'), + description: t('seo.minigames.jsonLdDescription'), + inLanguage: lang, + }, + ]; + } + + if (name === 'VocabLanding') { + return [ + { + '@context': 'https://schema.org', + '@type': 'SoftwareApplication', + name: t('seo.vocab.jsonLdName'), + url: buildAbsoluteUrl('/vokabeltrainer'), + description: t('seo.vocab.jsonLdDescription'), + applicationCategory: 'EducationalApplication', + operatingSystem: 'Web', + inLanguage: lang, + }, + ]; + } + + if (name === 'BlogList') { + return [ + { + '@context': 'https://schema.org', + '@type': 'CollectionPage', + name: t('seo.blogList.jsonLdName'), + url: buildAbsoluteUrl('/blogs'), + description: t('seo.blogList.jsonLdDescription'), + inLanguage: lang, + }, + ]; + } + + return []; +} + export function stripHtml(html = '') { return html .replace(/