feat(seo): enhance multilingual support and SEO handling
All checks were successful
Deploy to production / deploy (push) Successful in 2m46s
All checks were successful
Deploy to production / deploy (push) Successful in 2m46s
- Added support for multiple languages in the frontend, including English, Spanish, and Cebuano, improving accessibility for a broader audience. - Implemented hreflang links for better SEO performance, ensuring search engines can correctly index language-specific content. - Updated SEO metadata handling to utilize internationalization keys, enhancing the clarity and relevance of page titles and descriptions. - Refactored SEO utility functions to streamline the management of OpenGraph and hreflang attributes, improving maintainability and performance.
This commit is contained in:
@@ -26,6 +26,9 @@
|
||||
|
||||
<meta name="theme-color" content="#FF8C5A" />
|
||||
<link rel="alternate" hreflang="de" href="%VITE_PUBLIC_BASE_URL%/" />
|
||||
<link rel="alternate" hreflang="en" href="%VITE_PUBLIC_BASE_URL%/?lang=en" />
|
||||
<link rel="alternate" hreflang="es" href="%VITE_PUBLIC_BASE_URL%/?lang=es" />
|
||||
<link rel="alternate" hreflang="ceb" href="%VITE_PUBLIC_BASE_URL%/?lang=ceb" />
|
||||
<link rel="alternate" hreflang="x-default" href="%VITE_PUBLIC_BASE_URL%/" />
|
||||
|
||||
</head>
|
||||
@@ -33,11 +36,26 @@
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<noscript>
|
||||
<section style="max-width:960px;margin:40px auto;padding:0 20px;font-family:Arial,sans-serif;line-height:1.6;">
|
||||
<section lang="de" style="max-width:960px;margin:40px auto;padding:0 20px;font-family:Arial,sans-serif;line-height:1.6;">
|
||||
<h1>YourPart</h1>
|
||||
<p>YourPart ist eine Plattform fuer Community, Chat, Forum, Blogs, Vokabeltrainer, das Browser-Aufbauspiel Falukant und Minispiele.</p>
|
||||
<p>Wichtige Bereiche: <a href="/blogs">Blogs</a>, <a href="/vokabeltrainer">Vokabeltrainer</a>, <a href="/falukant">Falukant</a> und <a href="/minigames">Minispiele</a>.</p>
|
||||
</section>
|
||||
<section lang="en" style="max-width:960px;margin:24px auto;padding:0 20px;font-family:Arial,sans-serif;line-height:1.6;">
|
||||
<h2>YourPart (English)</h2>
|
||||
<p>YourPart is a platform for community, chat, forums, blogs, a vocabulary trainer, the browser builder game Falukant and minigames.</p>
|
||||
<p>Key areas: <a href="/blogs?lang=en">Blogs</a>, <a href="/vokabeltrainer?lang=en">Vocabulary trainer</a>, <a href="/falukant?lang=en">Falukant</a> and <a href="/minigames?lang=en">Minigames</a>.</p>
|
||||
</section>
|
||||
<section lang="es" style="max-width:960px;margin:24px auto;padding:0 20px;font-family:Arial,sans-serif;line-height:1.6;">
|
||||
<h2>YourPart (Español)</h2>
|
||||
<p>YourPart es una plataforma para comunidad, chat, foros, blogs, entrenador de vocabulario, el juego de construcción Falukant en el navegador y minijuegos.</p>
|
||||
<p>Áreas: <a href="/blogs?lang=es">Blogs</a>, <a href="/vokabeltrainer?lang=es">Vocabulario</a>, <a href="/falukant?lang=es">Falukant</a> y <a href="/minigames?lang=es">Minijuegos</a>.</p>
|
||||
</section>
|
||||
<section lang="ceb" style="max-width:960px;margin:24px auto;padding:0 20px;font-family:Arial,sans-serif;line-height:1.6;">
|
||||
<h2>YourPart (Cebuano)</h2>
|
||||
<p>Ang YourPart usa ka plataporma alang sa komunidad, chat, forum, blog, trainer sa bokabularyo, ang browser game nga Falukant ug minigames.</p>
|
||||
<p>Mga bahin: <a href="/blogs?lang=ceb">Blogs</a>, <a href="/vokabeltrainer?lang=ceb">Bokabularyo</a>, <a href="/falukant?lang=ceb">Falukant</a>, <a href="/minigames?lang=ceb">Minigames</a>.</p>
|
||||
</section>
|
||||
</noscript>
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
</body>
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
51
frontend/src/i18n/locales/ceb/seo.json
Normal file
51
frontend/src/i18n/locales/ceb/seo.json
Normal file
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
51
frontend/src/i18n/locales/de/seo.json
Normal file
51
frontend/src/i18n/locales/de/seo.json
Normal file
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
51
frontend/src/i18n/locales/en/seo.json
Normal file
51
frontend/src/i18n/locales/en/seo.json
Normal file
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
51
frontend/src/i18n/locales/es/seo.json
Normal file
51
frontend/src/i18n/locales/es/seo.json
Normal file
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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(/<style[\s\S]*?<\/style>/gi, ' ')
|
||||
@@ -114,6 +298,13 @@ export function applySeo(overrides = {}) {
|
||||
upsertMeta('property', 'og:image', image);
|
||||
upsertLink('canonical', canonical);
|
||||
|
||||
clearManagedHreflang();
|
||||
clearManagedOgLocaleAlternates();
|
||||
if (overrides.includeHreflangAlternates) {
|
||||
appendHreflangAlternate(overrides.canonicalPath || '/');
|
||||
appendOgLocaleAlternates(locale);
|
||||
}
|
||||
|
||||
clearManagedJsonLd();
|
||||
for (const payload of overrides.jsonLd || []) {
|
||||
const script = document.createElement('script');
|
||||
@@ -127,23 +318,64 @@ export function applySeo(overrides = {}) {
|
||||
export function applyRouteSeo(route) {
|
||||
const seo = route.meta?.seo || {};
|
||||
const isProtected = !!route.meta?.requiresAuth;
|
||||
|
||||
const title = seo.title || DEFAULT_TITLE;
|
||||
const description = seo.description || DEFAULT_DESCRIPTION;
|
||||
const canonicalPath = seo.canonicalPath || route.path || '/';
|
||||
const robots = seo.robots || route.meta?.robots || (isProtected ? 'noindex, nofollow' : 'index, follow');
|
||||
|
||||
const accessor = getSeoI18n ? getSeoI18n() : null;
|
||||
const i18nKey = seo.i18nKey;
|
||||
const prefix = i18nKey ? `seo.${i18nKey}` : '';
|
||||
|
||||
let title = DEFAULT_TITLE;
|
||||
let description = DEFAULT_DESCRIPTION;
|
||||
let keywords;
|
||||
let jsonLd = [];
|
||||
|
||||
if (accessor && prefix && accessor.te(`${prefix}.title`)) {
|
||||
title = accessor.t(`${prefix}.title`);
|
||||
description = accessor.te(`${prefix}.description`)
|
||||
? accessor.t(`${prefix}.description`)
|
||||
: DEFAULT_DESCRIPTION;
|
||||
if (accessor.te(`${prefix}.keywords`)) {
|
||||
keywords = accessor.t(`${prefix}.keywords`);
|
||||
} else if (accessor.te('seo.default.keywords')) {
|
||||
keywords = accessor.t('seo.default.keywords');
|
||||
}
|
||||
if (!isProtected) {
|
||||
jsonLd = buildRouteJsonLd(route, accessor.t, accessor.locale);
|
||||
}
|
||||
} else {
|
||||
title = seo.title || DEFAULT_TITLE;
|
||||
description = seo.description || DEFAULT_DESCRIPTION;
|
||||
keywords = seo.keywords;
|
||||
if (!isProtected) {
|
||||
jsonLd = seo.jsonLd || [];
|
||||
}
|
||||
}
|
||||
|
||||
if (accessor && accessor.te('seo.default.title') && title === DEFAULT_TITLE && !seo.title) {
|
||||
title = accessor.t('seo.default.title');
|
||||
}
|
||||
if (accessor && accessor.te('seo.default.description') && description === DEFAULT_DESCRIPTION && !seo.description) {
|
||||
description = accessor.t('seo.default.description');
|
||||
}
|
||||
if (!keywords && accessor?.te('seo.default.keywords')) {
|
||||
keywords = accessor.t('seo.default.keywords');
|
||||
}
|
||||
|
||||
const uiLocale = accessor?.locale && SEO_UI_LOCALES.includes(accessor.locale) ? accessor.locale : 'de';
|
||||
|
||||
applySeo({
|
||||
title,
|
||||
description,
|
||||
canonicalPath,
|
||||
keywords: seo.keywords,
|
||||
keywords,
|
||||
robots,
|
||||
type: seo.type,
|
||||
image: seo.image,
|
||||
locale: seo.locale,
|
||||
lang: seo.lang,
|
||||
jsonLd: isProtected ? [] : (seo.jsonLd || []),
|
||||
locale: uiLocaleToOgLocale(uiLocale),
|
||||
lang: uiLocaleToHtmlLang(uiLocale),
|
||||
jsonLd,
|
||||
includeHreflangAlternates: !isProtected,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -157,4 +389,6 @@ export function resetSeo() {
|
||||
|
||||
document.head.querySelector('link[rel="canonical"]')?.remove();
|
||||
clearManagedJsonLd();
|
||||
clearManagedHreflang();
|
||||
clearManagedOgLocaleAlternates();
|
||||
}
|
||||
|
||||
@@ -46,7 +46,16 @@
|
||||
import { getBlog, listPosts, createPost } from '@/api/blogApi.js';
|
||||
import DOMPurify from 'dompurify';
|
||||
import RichTextEditor from './components/RichTextEditor.vue';
|
||||
import { applySeo, buildAbsoluteUrl, createBlogSlug, stripHtml, truncateText } from '@/utils/seo.js';
|
||||
import {
|
||||
applySeo,
|
||||
buildAbsoluteUrl,
|
||||
createBlogSlug,
|
||||
stripHtml,
|
||||
truncateText,
|
||||
seoSchemaLang,
|
||||
seoOgLocale,
|
||||
seoHtmlLang,
|
||||
} from '@/utils/seo.js';
|
||||
export default {
|
||||
name: 'BlogView',
|
||||
props: { id: String, slug: String },
|
||||
@@ -92,13 +101,20 @@ export default {
|
||||
const summarySource = this.blog.description || plainTextPosts || this.$t('blog.view.fallbackDescription');
|
||||
const description = truncateText(summarySource, 160);
|
||||
const canonicalPath = this.canonicalBlogPath();
|
||||
const uiLang = this.$store.state.language;
|
||||
const pageTitle = this.$te('seo.blogPost.pageTitle')
|
||||
? this.$t('seo.blogPost.pageTitle', { title: this.blog.title })
|
||||
: `${this.blog.title} | YourPart`;
|
||||
|
||||
applySeo({
|
||||
title: `${this.blog.title} | Blog auf YourPart`,
|
||||
title: pageTitle,
|
||||
description,
|
||||
canonicalPath,
|
||||
keywords: `Blog, ${this.blog.title}, ${this.blog.owner?.username || 'YourPart'}, Community`,
|
||||
type: 'article',
|
||||
locale: seoOgLocale(uiLang),
|
||||
lang: seoHtmlLang(uiLang),
|
||||
includeHreflangAlternates: true,
|
||||
jsonLd: [
|
||||
{
|
||||
'@context': 'https://schema.org',
|
||||
@@ -106,7 +122,7 @@ export default {
|
||||
name: this.blog.title,
|
||||
description,
|
||||
url: buildAbsoluteUrl(canonicalPath),
|
||||
inLanguage: 'de',
|
||||
inLanguage: seoSchemaLang(uiLang),
|
||||
author: this.blog.owner?.username
|
||||
? {
|
||||
'@type': 'Person',
|
||||
@@ -150,6 +166,9 @@ export default {
|
||||
description: this.$t('blog.view.notFoundDescription'),
|
||||
canonicalPath: '/blogs',
|
||||
robots: 'noindex, nofollow',
|
||||
locale: seoOgLocale(this.$store.state.language),
|
||||
lang: seoHtmlLang(this.$store.state.language),
|
||||
includeHreflangAlternates: true,
|
||||
});
|
||||
} finally { this.loading = false; }
|
||||
},
|
||||
|
||||
@@ -44,10 +44,14 @@ echo ""
|
||||
echo "=== Building and Updating Frontend ==="
|
||||
./update-frontend.sh "$TARGET_DIR"
|
||||
|
||||
echo ""
|
||||
echo "Link backend .env"
|
||||
ln -s backend/.env .env
|
||||
|
||||
echo ""
|
||||
echo "=== Update Completed! ==="
|
||||
echo "Your application has been updated."
|
||||
echo ""
|
||||
echo "To check logs:"
|
||||
echo " Backend: sudo journalctl -u yourpart.service -f"
|
||||
echo " Apache: sudo tail -f /var/log/apache2/yourpart.*.log"
|
||||
echo " Apache: sudo tail -f /var/log/apache2/yourpart.*.log"
|
||||
|
||||
Reference in New Issue
Block a user