- 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
100 lines
3.0 KiB
TypeScript
100 lines
3.0 KiB
TypeScript
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} />;
|
||
}
|
||
|