import { getPublicBaseUrl } from './appConfig.js'; const DEFAULT_BASE_URL = 'https://www.your-part.de'; 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'], ['name', 'robots'], ['name', 'twitter:card'], ['name', 'twitter:title'], ['name', 'twitter:description'], ['name', 'twitter:image'], ['property', 'og:type'], ['property', 'og:site_name'], ['property', 'og:title'], ['property', 'og:description'], ['property', 'og:url'], ['property', 'og:locale'], ['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; } function upsertMeta(attr, key, content) { let element = document.head.querySelector(`meta[${attr}="${key}"]`); if (!element) { element = document.createElement('meta'); element.setAttribute(attr, key); document.head.appendChild(element); } element.setAttribute('content', content); } function upsertLink(rel, href) { let element = document.head.querySelector(`link[rel="${rel}"]`); if (!element) { element = document.createElement('link'); element.setAttribute('rel', rel); document.head.appendChild(element); } element.setAttribute('href', href); } 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; } const normalizedPath = path.startsWith('/') ? path : `/${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(//gi, ' ') .replace(//gi, ' ') .replace(/<[^>]+>/g, ' ') .replace(/\s+/g, ' ') .trim(); } export function truncateText(text = '', maxLength = 160) { if (text.length <= maxLength) { return text; } return `${text.slice(0, Math.max(0, maxLength - 1)).trim()}…`; } export function createBlogSlug(ownerUsername = '', blogTitle = '') { const usernamePart = String(ownerUsername || '').trim(); const titlePart = String(blogTitle || '') .replace(/\s+/g, '') .trim(); return `${usernamePart}${titlePart}`.replace(/[^a-zA-Z0-9_-]/g, ''); } export function applySeo(overrides = {}) { const title = overrides.title || DEFAULT_TITLE; const description = overrides.description || DEFAULT_DESCRIPTION; const keywords = overrides.keywords || 'YourPart, Community, Chat, Forum, Blog, Vokabeltrainer, Falukant, Minispiele'; const robots = overrides.robots || 'index, follow'; const canonical = buildAbsoluteUrl(overrides.canonicalPath || '/'); const image = buildAbsoluteUrl(overrides.image || DEFAULT_IMAGE); const type = overrides.type || 'website'; const locale = overrides.locale || 'de_DE'; document.title = title; document.documentElement.setAttribute('lang', overrides.lang || 'de'); upsertMeta('name', 'description', description); upsertMeta('name', 'keywords', keywords); upsertMeta('name', 'robots', robots); upsertMeta('name', 'twitter:card', overrides.twitterCard || 'summary_large_image'); upsertMeta('name', 'twitter:title', overrides.twitterTitle || title); upsertMeta('name', 'twitter:description', overrides.twitterDescription || description); upsertMeta('name', 'twitter:image', image); upsertMeta('property', 'og:type', type); upsertMeta('property', 'og:site_name', DEFAULT_SITE_NAME); upsertMeta('property', 'og:title', overrides.ogTitle || title); upsertMeta('property', 'og:description', overrides.ogDescription || description); upsertMeta('property', 'og:url', canonical); upsertMeta('property', 'og:locale', locale); 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'); script.type = 'application/ld+json'; script.dataset.seoManaged = 'true'; script.textContent = JSON.stringify(payload); document.head.appendChild(script); } } export function applyRouteSeo(route) { const seo = route.meta?.seo || {}; const isProtected = !!route.meta?.requiresAuth; 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, robots, type: seo.type, image: seo.image, locale: uiLocaleToOgLocale(uiLocale), lang: uiLocaleToHtmlLang(uiLocale), jsonLd, includeHreflangAlternates: !isProtected, }); } export function resetSeo() { for (const [attr, key] of MANAGED_META_KEYS) { const element = document.head.querySelector(`meta[${attr}="${key}"]`); if (element) { element.remove(); } } document.head.querySelector('link[rel="canonical"]')?.remove(); clearManagedJsonLd(); clearManagedHreflang(); clearManagedOgLocaleAlternates(); }