fix: resolve project 404s with Directus fallback and upgrade 404 page
Some checks failed
Dev Deployment (Zero Downtime) / deploy-dev (push) Has been cancelled

Merged Directus and PostgreSQL project data, implemented single project fetch from CMS, and modernized the NotFound component with liquid design.
This commit is contained in:
2026-02-15 22:47:25 +01:00
parent 6998a0e7a1
commit cc8fff14d2
7 changed files with 370 additions and 237 deletions

View File

@@ -3,6 +3,7 @@ import ProjectDetailClient from "@/app/_ui/ProjectDetailClient";
import { notFound } from "next/navigation";
import type { Metadata } from "next";
import { getLanguageAlternates, toAbsoluteUrl } from "@/lib/seo";
import { getProjectBySlug } from "@/lib/directus";
export const revalidate = 300;
@@ -12,6 +13,20 @@ export async function generateMetadata({
params: Promise<{ locale: string; slug: string }>;
}): Promise<Metadata> {
const { locale, slug } = await params;
// Try Directus first for metadata
const directusProject = await getProjectBySlug(slug, locale);
if (directusProject) {
return {
title: directusProject.title,
description: directusProject.description,
alternates: {
canonical: toAbsoluteUrl(`/${locale}/projects/${slug}`),
languages: getLanguageAlternates({ pathWithoutLocale: `projects/${slug}` }),
},
};
}
const languages = getLanguageAlternates({ pathWithoutLocale: `projects/${slug}` });
return {
alternates: {
@@ -28,7 +43,8 @@ export default async function ProjectPage({
}) {
const { locale, slug } = await params;
const project = await prisma.project.findFirst({
// Try PostgreSQL first
const dbProject = await prisma.project.findFirst({
where: { slug, published: true },
include: {
translations: {
@@ -37,43 +53,56 @@ export default async function ProjectPage({
},
});
if (!project) return notFound();
let projectData: any = null;
const trPreferred = project.translations?.find((t) => t.locale === locale && (t?.title || t?.description));
const trDefault = project.translations?.find(
(t) => t.locale === project.defaultLocale && (t?.title || t?.description),
);
const tr = trPreferred ?? trDefault;
const { translations: _translations, ...rest } = project;
const localizedContent = (() => {
if (typeof tr?.content === "string") return tr.content;
if (tr?.content && typeof tr.content === "object" && "markdown" in tr.content) {
const markdown = (tr.content as Record<string, unknown>).markdown;
if (typeof markdown === "string") return markdown;
if (dbProject) {
const trPreferred = dbProject.translations?.find((t) => t.locale === locale && (t?.title || t?.description));
const trDefault = dbProject.translations?.find(
(t) => t.locale === dbProject.defaultLocale && (t?.title || t?.description),
);
const tr = trPreferred ?? trDefault;
const { translations: _translations, ...rest } = dbProject;
const localizedContent = (() => {
if (typeof tr?.content === "string") return tr.content;
if (tr?.content && typeof tr.content === "object" && "markdown" in tr.content) {
const markdown = (tr.content as Record<string, unknown>).markdown;
if (typeof markdown === "string") return markdown;
}
return dbProject.content;
})();
projectData = {
...rest,
title: tr?.title ?? dbProject.title,
description: tr?.description ?? dbProject.description,
content: localizedContent,
};
} else {
// Try Directus fallback
const directusProject = await getProjectBySlug(slug, locale);
if (directusProject) {
projectData = {
...directusProject,
id: parseInt(directusProject.id) || 0,
};
}
return project.content;
})();
const localized = {
...rest,
title: tr?.title ?? project.title,
description: tr?.description ?? project.description,
content: localizedContent,
};
}
if (!projectData) return notFound();
const jsonLd = {
"@context": "https://schema.org",
"@type": "SoftwareSourceCode",
"name": localized.title,
"description": localized.description,
"codeRepository": localized.github,
"programmingLanguage": localized.technologies,
"name": projectData.title,
"description": projectData.description,
"codeRepository": projectData.github_url || projectData.github,
"programmingLanguage": projectData.technologies,
"author": {
"@type": "Person",
"name": "Dennis Konkol"
},
"dateCreated": project.date,
"dateCreated": projectData.date || projectData.created_at,
"url": toAbsoluteUrl(`/${locale}/projects/${slug}`),
"image": localized.imageUrl ? toAbsoluteUrl(localized.imageUrl) : undefined,
"image": projectData.imageUrl || projectData.image_url ? toAbsoluteUrl(projectData.imageUrl || projectData.image_url) : undefined,
};
return (
@@ -82,7 +111,7 @@ export default async function ProjectPage({
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
/>
<ProjectDetailClient project={localized} locale={locale} />
<ProjectDetailClient project={projectData} locale={locale} />
</>
);
}

View File

@@ -2,6 +2,7 @@ import { prisma } from "@/lib/prisma";
import ProjectsPageClient 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;
@@ -27,7 +28,8 @@ export default async function ProjectsPage({
}) {
const { locale } = await params;
const projects = await prisma.project.findMany({
// Fetch from PostgreSQL
const dbProjects = await prisma.project.findMany({
where: { published: true },
orderBy: { createdAt: "desc" },
include: {
@@ -37,7 +39,21 @@ export default async function ProjectsPage({
},
});
const localized = projects.map((p) => {
// Fetch from Directus
let directusProjects: any[] = [];
try {
const fetched = await getDirectusProjects(locale, { published: true });
if (fetched) {
directusProjects = fetched.map(p => ({
...p,
id: parseInt(p.id) || 0,
}));
}
} catch (err) {
console.error("Directus projects fetch failed:", err);
}
const localizedDb = 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),
@@ -51,6 +67,23 @@ export default async function ProjectsPage({
};
});
return <ProjectsPageClient projects={localized} locale={locale} />;
// Merge projects, prioritizing DB ones if slugs match
const allProjects = [...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} />;
}