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

View File

@@ -9,6 +9,7 @@
<meta name="keywords" content="YourPart, Community, Chat, Forum, soziales Netzwerk, Vokabeltrainer, Sprachen lernen, Falukant, Aufbauspiel, Minispiele, Match3, Taxi, Bildergalerie, Tagebuch, Freundschaften" /> <meta name="keywords" content="YourPart, Community, Chat, Forum, soziales Netzwerk, Vokabeltrainer, Sprachen lernen, Falukant, Aufbauspiel, Minispiele, Match3, Taxi, Bildergalerie, Tagebuch, Freundschaften" />
<meta name="robots" content="index, follow" /> <meta name="robots" content="index, follow" />
<link rel="canonical" href="https://www.your-part.de/" /> <link rel="canonical" href="https://www.your-part.de/" />
<meta name="author" content="YourPart" />
<meta property="og:type" content="website" /> <meta property="og:type" content="website" />
<meta property="og:site_name" content="YourPart" /> <meta property="og:site_name" content="YourPart" />
@@ -16,62 +17,28 @@
<meta property="og:description" content="Community, Chat, Forum, soziales Netzwerk, Vokabeltrainer zum Sprachen lernen, das Aufbauspiel Falukant sowie Minispiele jetzt in der Beta testen." /> <meta property="og:description" content="Community, Chat, Forum, soziales Netzwerk, Vokabeltrainer zum Sprachen lernen, das Aufbauspiel Falukant sowie Minispiele jetzt in der Beta testen." />
<meta property="og:url" content="https://www.your-part.de/" /> <meta property="og:url" content="https://www.your-part.de/" />
<meta property="og:locale" content="de_DE" /> <meta property="og:locale" content="de_DE" />
<meta property="og:image" content="https://www.your-part.de/images/logos/logo.png" />
<meta name="twitter:card" content="summary" /> <meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content="YourPart Community, Chat, Forum, Vokabeltrainer, Falukant & Minispiele" /> <meta name="twitter:title" content="YourPart Community, Chat, Forum, Vokabeltrainer, Falukant & Minispiele" />
<meta name="twitter:description" content="Community, Chat, Forum, soziales Netzwerk, Vokabeltrainer zum Sprachen lernen, das Aufbauspiel Falukant sowie Minispiele jetzt in der Beta testen." /> <meta name="twitter:description" content="Community, Chat, Forum, soziales Netzwerk, Vokabeltrainer zum Sprachen lernen, das Aufbauspiel Falukant sowie Minispiele jetzt in der Beta testen." />
<meta name="twitter:image" content="https://www.your-part.de/images/logos/logo.png" />
<meta name="theme-color" content="#FF8C5A" /> <meta name="theme-color" content="#FF8C5A" />
<link rel="alternate" hreflang="de" href="https://www.your-part.de/" />
<link rel="alternate" hreflang="x-default" href="https://www.your-part.de/" />
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "WebSite",
"name": "YourPart",
"url": "https://www.your-part.de/",
"inLanguage": "de",
"description": "Community-Plattform mit Chat, Forum, Vokabeltrainer, Aufbauspiel Falukant und Minispielen",
"potentialAction": {
"@type": "SearchAction",
"target": "https://www.your-part.de/?q={search_term_string}",
"query-input": "required name=search_term_string"
},
"about": [
{
"@type": "SoftwareApplication",
"name": "Vokabeltrainer",
"description": "Lerne Sprachen mit dem interaktiven Vokabeltrainer",
"applicationCategory": "EducationalApplication",
"url": "https://www.your-part.de/socialnetwork/vocab"
},
{
"@type": "VideoGame",
"name": "Falukant",
"description": "Mittelalterliches Aufbauspiel mit Handel, Politik und Charakterentwicklung",
"gamePlatform": "Web",
"url": "https://www.your-part.de/falukant"
},
{
"@type": "VideoGame",
"name": "Match3",
"description": "Klassisches Match-3 Puzzle-Spiel",
"gamePlatform": "Web",
"url": "https://www.your-part.de/minigames/match3"
},
{
"@type": "VideoGame",
"name": "Taxi",
"description": "Taxi-Fahrspiel mit Passagieren und Strecken",
"gamePlatform": "Web",
"url": "https://www.your-part.de/minigames/taxi"
}
]
}
</script>
</head> </head>
<body> <body>
<div id="app"></div> <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;">
<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>
</noscript>
<script type="module" src="/src/main.js"></script> <script type="module" src="/src/main.js"></script>
</body> </body>

View File

@@ -1,6 +1,34 @@
User-agent: * User-agent: *
Allow: / Allow: /
Disallow: /activate
Disallow: /admin/
Disallow: /friends
Disallow: /personal/
Disallow: /settings/
Disallow: /socialnetwork/diary
Disallow: /socialnetwork/forum/
Disallow: /socialnetwork/forumtopic/
Disallow: /socialnetwork/gallery
Disallow: /socialnetwork/guestbook
Disallow: /socialnetwork/search
Disallow: /socialnetwork/vocab/
Disallow: /falukant/home
Disallow: /falukant/create
Disallow: /falukant/branch/
Disallow: /falukant/moneyhistory
Disallow: /falukant/family
Disallow: /falukant/house
Disallow: /falukant/nobility
Disallow: /falukant/reputation
Disallow: /falukant/church
Disallow: /falukant/education
Disallow: /falukant/bank
Disallow: /falukant/directors
Disallow: /falukant/health
Disallow: /falukant/politics
Disallow: /falukant/darknet
Disallow: /minigames/match3
Disallow: /minigames/taxi
Sitemap: https://www.your-part.de/sitemap.xml Sitemap: https://www.your-part.de/sitemap.xml

View File

@@ -8,6 +8,11 @@
<url> <url>
<loc>https://www.your-part.de/blogs</loc> <loc>https://www.your-part.de/blogs</loc>
<changefreq>daily</changefreq> <changefreq>daily</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://www.your-part.de/vokabeltrainer</loc>
<changefreq>weekly</changefreq>
<priority>0.8</priority> <priority>0.8</priority>
</url> </url>
<url> <url>
@@ -16,25 +21,8 @@
<priority>0.8</priority> <priority>0.8</priority>
</url> </url>
<url> <url>
<loc>https://www.your-part.de/socialnetwork/vocab</loc> <loc>https://www.your-part.de/minigames</loc>
<changefreq>weekly</changefreq> <changefreq>weekly</changefreq>
<priority>0.7</priority> <priority>0.7</priority>
</url> </url>
<url>
<loc>https://www.your-part.de/minigames</loc>
<changefreq>monthly</changefreq>
<priority>0.6</priority>
</url>
<url>
<loc>https://www.your-part.de/minigames/match3</loc>
<changefreq>monthly</changefreq>
<priority>0.5</priority>
</url>
<url>
<loc>https://www.your-part.de/minigames/taxi</loc>
<changefreq>monthly</changefreq>
<priority>0.5</priority>
</url>
</urlset> </urlset>

View File

@@ -42,9 +42,6 @@ import MultiChatDialog from './dialogues/chat/MultiChatDialog.vue';
export default { export default {
name: 'App', name: 'App',
mounted() {
document.title = 'yourPart';
},
computed: { computed: {
...mapGetters(['isLoggedIn', 'user']) ...mapGetters(['isLoggedIn', 'user'])
}, },

View File

@@ -4,7 +4,10 @@ const authRoutes = [
{ {
path: '/activate', path: '/activate',
name: 'Activate page', name: 'Activate page',
component: ActivateView component: ActivateView,
meta: {
robots: 'noindex, nofollow'
}
}, },
]; ];

View File

@@ -6,8 +6,52 @@ export default [
{ path: '/blogs/create', name: 'BlogCreate', component: BlogEditorView, meta: { requiresAuth: true } }, { path: '/blogs/create', name: 'BlogCreate', component: BlogEditorView, meta: { requiresAuth: true } },
{ path: '/blogs/:id/edit', name: 'BlogEdit', component: BlogEditorView, props: true, meta: { requiresAuth: true } }, { path: '/blogs/:id/edit', name: 'BlogEdit', component: BlogEditorView, props: true, meta: { requiresAuth: true } },
// Slug-only route first so it doesn't get captured by the :id route // Slug-only route first so it doesn't get captured by the :id route
{ path: '/blogs/:slug', name: 'BlogSlug', component: BlogView, props: route => ({ slug: route.params.slug }) }, {
path: '/blogs/:slug',
name: 'BlogSlug',
component: BlogView,
props: route => ({ slug: route.params.slug }),
meta: {
seo: {
title: 'Blogs auf YourPart',
description: 'Oeffentliche Blogs, Beitraege und Community-Inhalte auf YourPart.',
},
},
},
// Id-constrained route (numeric id only) with optional slug for canonical links // Id-constrained route (numeric id only) with optional slug for canonical links
{ path: '/blogs/:id(\\d+)/:slug?', name: 'Blog', component: BlogView, props: true }, {
{ path: '/blogs', name: 'BlogList', component: BlogListView }, path: '/blogs/:id(\\d+)/:slug?',
name: 'Blog',
component: BlogView,
props: true,
meta: {
seo: {
title: 'Blogs auf YourPart',
description: 'Oeffentliche Blogs, Beitraege und Community-Inhalte auf YourPart.',
},
},
},
{
path: '/blogs',
name: 'BlogList',
component: BlogListView,
meta: {
seo: {
title: 'Blogs auf YourPart - Community-Beitraege und Themen',
description: 'Entdecke oeffentliche Blogs auf YourPart mit Community-Beitraegen, Gedanken, Erfahrungen und Themen aus verschiedenen Bereichen.',
keywords: 'Blogs, Community Blog, Artikel, Beitraege, YourPart',
canonicalPath: '/blogs',
jsonLd: [
{
'@context': 'https://schema.org',
'@type': 'CollectionPage',
name: 'Blogs auf YourPart',
url: 'https://www.your-part.de/blogs',
description: 'Oeffentliche Blogs und Community-Beitraege auf YourPart.',
inLanguage: 'de',
},
],
},
},
},
]; ];

View File

@@ -9,13 +9,39 @@ import falukantRoutes from './falukantRoutes';
import blogRoutes from './blogRoutes'; import blogRoutes from './blogRoutes';
import minigamesRoutes from './minigamesRoutes'; import minigamesRoutes from './minigamesRoutes';
import personalRoutes from './personalRoutes'; import personalRoutes from './personalRoutes';
import marketingRoutes from './marketingRoutes';
import { applyRouteSeo } from '../utils/seo';
const routes = [ const routes = [
{ {
path: '/', path: '/',
name: 'Home', name: 'Home',
component: HomeView 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',
canonicalPath: '/',
jsonLd: [
{
'@context': 'https://schema.org',
'@type': 'WebSite',
name: 'YourPart',
url: 'https://www.your-part.de/',
inLanguage: 'de',
description: 'Community-Plattform mit Chat, Forum, Blogs, Vokabeltrainer, Falukant und Browser-Minispielen.',
potentialAction: {
'@type': 'SearchAction',
target: 'https://www.your-part.de/blogs?q={search_term_string}',
'query-input': 'required name=search_term_string',
},
},
],
},
},
}, },
...marketingRoutes,
...authRoutes, ...authRoutes,
...socialRoutes, ...socialRoutes,
...settingsRoutes, ...settingsRoutes,
@@ -45,4 +71,8 @@ router.beforeEach((to, from, next) => {
} }
}); });
router.afterEach((to) => {
applyRouteSeo(to);
});
export default router; export default router;

View File

@@ -0,0 +1,85 @@
const FalukantLandingView = () => import('../views/public/FalukantLandingView.vue');
const MinigamesLandingView = () => import('../views/public/MinigamesLandingView.vue');
const VocabLandingView = () => import('../views/public/VocabLandingView.vue');
const marketingRoutes = [
{
path: '/falukant',
name: 'FalukantLanding',
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',
canonicalPath: '/falukant',
jsonLd: [
{
'@context': 'https://schema.org',
'@type': 'VideoGame',
name: 'Falukant',
url: 'https://www.your-part.de/falukant',
description: 'Mittelalterliches Browser-Aufbauspiel mit Handel, Politik, Familie und Charakterentwicklung.',
gamePlatform: 'Web Browser',
applicationCategory: 'Game',
inLanguage: 'de',
publisher: {
'@type': 'Organization',
name: 'YourPart',
},
},
],
},
},
},
{
path: '/minigames',
name: 'MinigamesLanding',
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',
canonicalPath: '/minigames',
jsonLd: [
{
'@context': 'https://schema.org',
'@type': 'CollectionPage',
name: 'YourPart Minispiele',
url: 'https://www.your-part.de/minigames',
description: 'Browser-Minispiele auf YourPart mit Match 3 und Taxi.',
inLanguage: 'de',
},
],
},
},
},
{
path: '/vokabeltrainer',
name: 'VocabLanding',
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',
canonicalPath: '/vokabeltrainer',
jsonLd: [
{
'@context': 'https://schema.org',
'@type': 'SoftwareApplication',
name: 'YourPart Vokabeltrainer',
url: 'https://www.your-part.de/vokabeltrainer',
description: 'Interaktiver Vokabeltrainer mit Kursen, Lektionen und Übungen zum Sprachenlernen.',
applicationCategory: 'EducationalApplication',
operatingSystem: 'Web',
inLanguage: 'de',
},
],
},
},
},
];
export default marketingRoutes;

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();
}

View File

@@ -9,7 +9,7 @@
<div v-if="!blogs.length">Keine Blogs gefunden.</div> <div v-if="!blogs.length">Keine Blogs gefunden.</div>
<ul> <ul>
<li v-for="b in blogs" :key="b.id"> <li v-for="b in blogs" :key="b.id">
<router-link :to="`/blogs/${b.id}`">{{ b.title }}</router-link> <router-link :to="blogUrl(b)">{{ b.title }}</router-link>
<small> {{ b.owner?.username }}</small> <small> {{ b.owner?.username }}</small>
</li> </li>
</ul> </ul>
@@ -19,11 +19,18 @@
<script> <script>
import { listBlogs } from '@/api/blogApi.js'; import { listBlogs } from '@/api/blogApi.js';
import { createBlogSlug } from '@/utils/seo.js';
export default { export default {
name: 'BlogListView', name: 'BlogListView',
data: () => ({ blogs: [], loading: true }), data: () => ({ blogs: [], loading: true }),
async mounted() { async mounted() {
try { this.blogs = await listBlogs(); } finally { this.loading = false; } try { this.blogs = await listBlogs(); } finally { this.loading = false; }
} },
methods: {
blogUrl(blog) {
const slug = createBlogSlug(blog?.owner?.username, blog?.title);
return slug ? `/blogs/${encodeURIComponent(slug)}` : `/blogs/${blog.id}`;
},
},
} }
</script> </script>

View File

@@ -37,6 +37,7 @@
import { getBlog, listPosts, createPost } from '@/api/blogApi.js'; import { getBlog, listPosts, createPost } from '@/api/blogApi.js';
import DOMPurify from 'dompurify'; import DOMPurify from 'dompurify';
import RichTextEditor from './components/RichTextEditor.vue'; import RichTextEditor from './components/RichTextEditor.vue';
import { applySeo, buildAbsoluteUrl, createBlogSlug, stripHtml, truncateText } from '@/utils/seo.js';
export default { export default {
name: 'BlogView', name: 'BlogView',
props: { id: String, slug: String }, props: { id: String, slug: String },
@@ -46,9 +47,73 @@ export default {
isOwner() { isOwner() {
const u = this.$store.getters.user; const u = this.$store.getters.user;
return !!(u && this.blog && this.blog.owner && this.blog.owner.hashedId === u.id); return !!(u && this.blog && this.blog.owner && this.blog.owner.hashedId === u.id);
} },
pages() {
return Math.max(1, Math.ceil(this.total / this.pageSize));
},
}, },
async mounted() { async mounted() {
await this.loadBlog();
},
watch: {
'$route.fullPath': {
async handler() {
await this.loadBlog();
},
},
},
methods: {
canonicalBlogPath() {
const slug = createBlogSlug(this.blog?.owner?.username, this.blog?.title);
if (slug) {
return `/blogs/${encodeURIComponent(slug)}`;
}
return this.blog?.id ? `/blogs/${this.blog.id}` : '/blogs';
},
applyBlogSeo() {
if (!this.blog) {
return;
}
const plainTextPosts = this.items
.map((item) => `${item.title || ''} ${stripHtml(item.content || '')}`.trim())
.filter(Boolean)
.join(' ');
const summarySource = this.blog.description || plainTextPosts || 'Oeffentlicher Community-Blog auf YourPart.';
const description = truncateText(summarySource, 160);
const canonicalPath = this.canonicalBlogPath();
applySeo({
title: `${this.blog.title} | Blog auf YourPart`,
description,
canonicalPath,
keywords: `Blog, ${this.blog.title}, ${this.blog.owner?.username || 'YourPart'}, Community`,
type: 'article',
jsonLd: [
{
'@context': 'https://schema.org',
'@type': 'Blog',
name: this.blog.title,
description,
url: buildAbsoluteUrl(canonicalPath),
inLanguage: 'de',
author: this.blog.owner?.username
? {
'@type': 'Person',
name: this.blog.owner.username,
}
: undefined,
},
],
});
},
async loadBlog() {
this.loading = true;
this.blog = null;
this.items = [];
this.resolvedId = null;
try { try {
let id = this.$route.params.id; let id = this.$route.params.id;
// If we have a slug route param or the id is non-numeric, resolve to id // If we have a slug route param or the id is non-numeric, resolve to id
@@ -67,12 +132,18 @@ export default {
const useId = id || this.resolvedId; const useId = id || this.resolvedId;
this.blog = await getBlog(useId); this.blog = await getBlog(useId);
await this.fetchPage(1); await this.fetchPage(1);
this.applyBlogSeo();
} catch (e) { } catch (e) {
console.log(e); console.log(e);
// this.$router.replace('/blogs'); // this.$router.replace('/blogs');
applySeo({
title: 'Blog nicht gefunden | YourPart',
description: 'Der angeforderte Blog konnte nicht geladen werden.',
canonicalPath: '/blogs',
robots: 'noindex, nofollow',
});
} finally { this.loading = false; } } finally { this.loading = false; }
}, },
methods: {
sanitize(html) { sanitize(html) {
return DOMPurify.sanitize(html || ''); return DOMPurify.sanitize(html || '');
}, },
@@ -83,8 +154,8 @@ export default {
this.page = res.page; this.page = res.page;
this.pageSize = res.pageSize; this.pageSize = res.pageSize;
this.total = res.total; this.total = res.total;
this.applyBlogSeo();
}, },
get pages() { return Math.max(1, Math.ceil(this.total / this.pageSize)); },
async go(p) { if (p>=1 && p<=this.pages) await this.fetchPage(p); }, async go(p) { if (p>=1 && p<=this.pages) await this.fetchPage(p); },
async addPost() { async addPost() {
if (!this.newPost.title || !this.newPost.content) return; if (!this.newPost.title || !this.newPost.content) return;

View File

@@ -0,0 +1,100 @@
<template>
<section class="marketing-page">
<div class="hero">
<p class="eyebrow">Browser-Aufbauspiel</p>
<h1>Falukant verbindet Wirtschaft, Politik und Charakterentwicklung in einer mittelalterlichen Spielwelt.</h1>
<p class="lead">
Baue Besitz auf, verwalte Zweigstellen, pflege Beziehungen und triff politische Entscheidungen in einem
persistenten Browsergame innerhalb von YourPart.
</p>
<router-link class="cta" to="/">Jetzt entdecken</router-link>
</div>
<div class="grid">
<article>
<h2>Wirtschaft mit Tiefe</h2>
<p>Produktion, Lager, Handel und Finanzen greifen ineinander und erzeugen eine langfristige Aufbauspiel-Dynamik.</p>
</article>
<article>
<h2>Persoenliche Entwicklung</h2>
<p>Familie, Bildung, Gesundheit und gesellschaftlicher Status beeinflussen deinen Weg in Falukant.</p>
</article>
<article>
<h2>Politik und Unterwelt</h2>
<p>Zwischen Kirche, Reputation, Adel und dunklen Netzwerken entstehen Entscheidungen mit spuerbaren Folgen.</p>
</article>
</div>
</section>
</template>
<style scoped>
.marketing-page {
max-width: 1100px;
margin: 0 auto;
padding: 48px 20px 72px;
color: #40261a;
}
.hero {
padding: 32px;
border-radius: 20px;
background: linear-gradient(135deg, #f7e0bb 0%, #f6c27d 45%, #e8924d 100%);
box-shadow: 0 20px 60px rgba(106, 56, 20, 0.18);
}
.eyebrow {
margin: 0 0 12px;
font-size: 0.82rem;
font-weight: 700;
letter-spacing: 0.12em;
text-transform: uppercase;
}
.hero h1 {
margin: 0;
font-size: clamp(2rem, 4vw, 3.6rem);
line-height: 1.08;
}
.lead {
max-width: 760px;
margin: 20px 0 0;
font-size: 1.1rem;
line-height: 1.65;
}
.cta {
display: inline-block;
margin-top: 24px;
padding: 12px 20px;
border-radius: 999px;
background: #40261a;
color: #fff;
text-decoration: none;
font-weight: 700;
}
.grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
gap: 18px;
margin-top: 28px;
}
.grid article {
padding: 24px;
border-radius: 18px;
background: #fff7ef;
border: 1px solid rgba(64, 38, 26, 0.08);
}
.grid h2 {
margin-top: 0;
margin-bottom: 10px;
}
.grid p {
margin: 0;
line-height: 1.65;
}
</style>

View File

@@ -0,0 +1,98 @@
<template>
<section class="marketing-page">
<div class="hero">
<p class="eyebrow">Browser-Minispiele</p>
<h1>Kurze Spielrunden, klare Ziele und direkte Action mit Match 3 und Taxi.</h1>
<p class="lead">
Die Minispiele auf YourPart liefern schnelle Abwechslung direkt im Browser und erweitern die Plattform um
spielbare Casual-Formate.
</p>
<router-link class="cta" to="/">Zur Startseite</router-link>
</div>
<div class="cards">
<article>
<h2>Match 3</h2>
<p>Das klassische Puzzle-Prinzip mit Kampagnenstruktur fuer Spielerinnen und Spieler, die kurze Sessions lieben.</p>
</article>
<article>
<h2>Taxi</h2>
<p>Fahre Passagiere effizient ans Ziel und verbessere deine Kontrolle, Streckenwahl und Reaktionsfaehigkeit.</p>
</article>
</div>
</section>
</template>
<style scoped>
.marketing-page {
max-width: 1100px;
margin: 0 auto;
padding: 48px 20px 72px;
color: #17323a;
}
.hero {
padding: 32px;
border-radius: 20px;
background:
radial-gradient(circle at top left, rgba(255, 255, 255, 0.7), transparent 35%),
linear-gradient(135deg, #d4f0e6 0%, #7dd0be 40%, #2e8b83 100%);
box-shadow: 0 20px 60px rgba(13, 84, 93, 0.18);
}
.eyebrow {
margin: 0 0 12px;
font-size: 0.82rem;
font-weight: 700;
letter-spacing: 0.12em;
text-transform: uppercase;
}
.hero h1 {
margin: 0;
font-size: clamp(2rem, 4vw, 3.5rem);
line-height: 1.08;
}
.lead {
max-width: 760px;
margin: 20px 0 0;
font-size: 1.1rem;
line-height: 1.65;
}
.cta {
display: inline-block;
margin-top: 24px;
padding: 12px 20px;
border-radius: 999px;
background: #17323a;
color: #fff;
text-decoration: none;
font-weight: 700;
}
.cards {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
gap: 18px;
margin-top: 28px;
}
.cards article {
padding: 24px;
border-radius: 18px;
background: #effaf6;
border: 1px solid rgba(23, 50, 58, 0.08);
}
.cards h2 {
margin-top: 0;
margin-bottom: 10px;
}
.cards p {
margin: 0;
line-height: 1.65;
}
</style>

View File

@@ -0,0 +1,102 @@
<template>
<section class="marketing-page">
<div class="hero">
<p class="eyebrow">Sprachen online lernen</p>
<h1>Der Vokabeltrainer auf YourPart kombiniert Lernen, Kurse und Uebungen in einer Plattform.</h1>
<p class="lead">
Arbeite mit interaktiven Lektionen, erweitere deinen Wortschatz und nutze strukturierte Inhalte fuer einen
motivierenden Lernfluss direkt im Browser.
</p>
<router-link class="cta" to="/">Kostenlos starten</router-link>
</div>
<div class="features">
<article>
<h2>Interaktive Kurse</h2>
<p>Kurse, Lektionen und Uebungen helfen beim systematischen Aufbau neuer Sprachkenntnisse.</p>
</article>
<article>
<h2>Praxisorientiert</h2>
<p>Wortschatz, Grammatik und Wiederholung werden auf eine alltagstaugliche Lernroutine ausgerichtet.</p>
</article>
<article>
<h2>Teil einer Community</h2>
<p>Der Sprachbereich ist in eine groessere Community-Plattform mit Blogs, Forum und Chat eingebettet.</p>
</article>
</div>
</section>
</template>
<style scoped>
.marketing-page {
max-width: 1100px;
margin: 0 auto;
padding: 48px 20px 72px;
color: #1f2f1d;
}
.hero {
padding: 32px;
border-radius: 20px;
background:
radial-gradient(circle at right top, rgba(255, 255, 255, 0.78), transparent 30%),
linear-gradient(135deg, #eef6c8 0%, #bddd74 45%, #6b9d34 100%);
box-shadow: 0 20px 60px rgba(60, 87, 28, 0.18);
}
.eyebrow {
margin: 0 0 12px;
font-size: 0.82rem;
font-weight: 700;
letter-spacing: 0.12em;
text-transform: uppercase;
}
.hero h1 {
margin: 0;
font-size: clamp(2rem, 4vw, 3.5rem);
line-height: 1.08;
}
.lead {
max-width: 760px;
margin: 20px 0 0;
font-size: 1.1rem;
line-height: 1.65;
}
.cta {
display: inline-block;
margin-top: 24px;
padding: 12px 20px;
border-radius: 999px;
background: #1f2f1d;
color: #fff;
text-decoration: none;
font-weight: 700;
}
.features {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
gap: 18px;
margin-top: 28px;
}
.features article {
padding: 24px;
border-radius: 18px;
background: #f7fbe9;
border: 1px solid rgba(31, 47, 29, 0.08);
}
.features h2 {
margin-top: 0;
margin-bottom: 10px;
}
.features p {
margin: 0;
line-height: 1.65;
}
</style>