Files
portfolio/app/[locale]/projects/page.tsx
denshooter 049dda8dc5
All checks were successful
CI / CD / test-build (push) Successful in 10m14s
CI / CD / deploy-dev (push) Successful in 1m20s
CI / CD / deploy-production (push) Has been skipped
feat: improve SEO with locale-specific metadata, structured data, and keywords
- Add locale-specific title/description for DE and EN homepage
- Expand keywords with local SEO terms (Webentwicklung Osnabrück, Informatik, etc.)
- Add WebSite schema and enhance Person schema with knowsAbout, alternateName
- Add hreflang alternates for DE/EN
- Update projects page with locale-specific metadata
- Keep visible titles short, move SEO terms to description/structured data
2026-04-19 15:47:22 +02:00

100 lines
3.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { prisma } from "@/lib/prisma";
import ProjectsPageClient, { ProjectListItem } from "@/app/_ui/ProjectsPageClient";
import type { Metadata } from "next";
import { getLanguageAlternates, toAbsoluteUrl } from "@/lib/seo";
import { getProjects as getDirectusProjects } from "@/lib/directus";
export const revalidate = 300;
export async function generateMetadata({
params,
}: {
params: Promise<{ locale: string }>;
}): Promise<Metadata> {
const { locale } = await params;
const languages = getLanguageAlternates({ pathWithoutLocale: "projects" });
const isDe = locale === "de";
return {
title: isDe ? "Projekte Dennis Konkol" : "Projects Dennis Konkol",
description: isDe
? "Webentwicklung, Fullstack-Apps und Mobile-Projekte von Dennis Konkol. Next.js, Flutter, Docker und mehr Osnabrück."
: "Web development, fullstack apps and mobile projects by Dennis Konkol. Next.js, Flutter, Docker and more Osnabrück.",
alternates: {
canonical: toAbsoluteUrl(`/${locale}/projects`),
languages,
},
};
}
export default async function ProjectsPage({
params,
}: {
params: Promise<{ locale: string }>;
}) {
const { locale } = await params;
// Fetch from PostgreSQL
const dbProjects = await prisma.project.findMany({
where: { published: true },
orderBy: { createdAt: "desc" },
include: {
translations: {
select: { title: true, description: true, locale: true },
},
},
});
// Fetch from Directus
let directusProjects: ProjectListItem[] = [];
try {
const fetched = await getDirectusProjects(locale, { published: true });
if (fetched) {
directusProjects = fetched.map(p => ({
...p,
id: typeof p.id === 'string' ? (parseInt(p.id) || 0) : p.id,
})) as ProjectListItem[];
}
} catch (err) {
console.error("Directus projects fetch failed:", err);
}
const localizedDb: ProjectListItem[] = dbProjects.map((p) => {
const trPreferred = p.translations?.find((t) => t.locale === locale && (t?.title || t?.description));
const trDefault = p.translations?.find(
(t) => t.locale === p.defaultLocale && (t?.title || t?.description),
);
const tr = trPreferred ?? trDefault;
return {
id: p.id,
slug: p.slug,
title: tr?.title ?? p.title,
description: tr?.description ?? p.description,
tags: p.tags,
category: p.category,
date: p.date,
createdAt: p.createdAt.toISOString(),
imageUrl: p.imageUrl,
};
});
// Merge projects, prioritizing DB ones if slugs match
const allProjects: ProjectListItem[] = [...localizedDb];
const dbSlugs = new Set(localizedDb.map(p => p.slug));
for (const dp of directusProjects) {
if (!dbSlugs.has(dp.slug)) {
allProjects.push(dp);
}
}
// Final sort by date
allProjects.sort((a, b) => {
const dateA = new Date(a.date || a.createdAt || 0).getTime();
const dateB = new Date(b.date || b.createdAt || 0).getTime();
return dateB - dateA;
});
return <ProjectsPageClient projects={allProjects} locale={locale} />;
}