Update SEO and meta tags in index.html, enhance robots.txt for better crawling control, and improve sitemap.xml priorities. Refactor blog routes to include SEO metadata and adjust blog view for canonical URLs. Implement blog URL generation in BlogListView and apply SEO dynamically in BlogView.

This commit is contained in:
Torsten Schulz (local)
2026-03-18 22:02:44 +01:00
parent 971e09a72a
commit 59869e077e
14 changed files with 759 additions and 81 deletions

158
frontend/src/utils/seo.js Normal file
View File

@@ -0,0 +1,158 @@
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 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'],
];
function getBaseUrl() {
return (import.meta.env.VITE_PUBLIC_BASE_URL || DEFAULT_BASE_URL).replace(/\/$/, '');
}
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());
}
export function buildAbsoluteUrl(path = '/') {
if (/^https?:\/\//i.test(path)) {
return path;
}
const normalizedPath = path.startsWith('/') ? path : `/${path}`;
return `${getBaseUrl()}${normalizedPath}`;
}
export function stripHtml(html = '') {
return html
.replace(/<style[\s\S]*?<\/style>/gi, ' ')
.replace(/<script[\s\S]*?<\/script>/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);
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 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');
applySeo({
title,
description,
canonicalPath,
keywords: seo.keywords,
robots,
type: seo.type,
image: seo.image,
locale: seo.locale,
lang: seo.lang,
jsonLd: isProtected ? [] : (seo.jsonLd || []),
});
}
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();
}