feat(cms): migrate from Directus to Payload v3 and remove contentlayer
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 3m43s
Build & Deploy / 🏗️ Build (push) Failing after 37s
Build & Deploy / 🧪 QA (push) Failing after 3m40s
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🩺 Health Check (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 2s

This commit is contained in:
2026-02-22 20:50:51 +01:00
parent b2f5e2dd4d
commit 072b6b13f1
30 changed files with 4299 additions and 2062 deletions

View File

@@ -0,0 +1,25 @@
import type { Metadata } from "next";
import configPromise from "@payload-config";
import { RootPage, generatePageMetadata } from "@payloadcms/next/views";
import { importMap } from "../importMap";
type Args = {
params: Promise<{
segments: string[];
}>;
searchParams: Promise<{
[key: string]: string | string[];
}>;
};
export const generateMetadata = async ({
params,
searchParams,
}: Args): Promise<Metadata> =>
generatePageMetadata({ config: configPromise, params, searchParams });
const Page = async ({ params, searchParams }: Args) =>
RootPage({ config: configPromise, importMap, params, searchParams });
export default Page;

View File

@@ -0,0 +1,75 @@
import { RscEntryLexicalCell as RscEntryLexicalCell_44fe37237e0ebf4470c9990d8cb7b07e } from "@payloadcms/richtext-lexical/rsc";
import { RscEntryLexicalField as RscEntryLexicalField_44fe37237e0ebf4470c9990d8cb7b07e } from "@payloadcms/richtext-lexical/rsc";
import { LexicalDiffComponent as LexicalDiffComponent_44fe37237e0ebf4470c9990d8cb7b07e } from "@payloadcms/richtext-lexical/rsc";
import { InlineToolbarFeatureClient as InlineToolbarFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from "@payloadcms/richtext-lexical/client";
import { HorizontalRuleFeatureClient as HorizontalRuleFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from "@payloadcms/richtext-lexical/client";
import { UploadFeatureClient as UploadFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from "@payloadcms/richtext-lexical/client";
import { BlockquoteFeatureClient as BlockquoteFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from "@payloadcms/richtext-lexical/client";
import { RelationshipFeatureClient as RelationshipFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from "@payloadcms/richtext-lexical/client";
import { LinkFeatureClient as LinkFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from "@payloadcms/richtext-lexical/client";
import { ChecklistFeatureClient as ChecklistFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from "@payloadcms/richtext-lexical/client";
import { OrderedListFeatureClient as OrderedListFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from "@payloadcms/richtext-lexical/client";
import { UnorderedListFeatureClient as UnorderedListFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from "@payloadcms/richtext-lexical/client";
import { IndentFeatureClient as IndentFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from "@payloadcms/richtext-lexical/client";
import { AlignFeatureClient as AlignFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from "@payloadcms/richtext-lexical/client";
import { HeadingFeatureClient as HeadingFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from "@payloadcms/richtext-lexical/client";
import { ParagraphFeatureClient as ParagraphFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from "@payloadcms/richtext-lexical/client";
import { InlineCodeFeatureClient as InlineCodeFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from "@payloadcms/richtext-lexical/client";
import { SuperscriptFeatureClient as SuperscriptFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from "@payloadcms/richtext-lexical/client";
import { SubscriptFeatureClient as SubscriptFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from "@payloadcms/richtext-lexical/client";
import { StrikethroughFeatureClient as StrikethroughFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from "@payloadcms/richtext-lexical/client";
import { UnderlineFeatureClient as UnderlineFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from "@payloadcms/richtext-lexical/client";
import { BoldFeatureClient as BoldFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from "@payloadcms/richtext-lexical/client";
import { ItalicFeatureClient as ItalicFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from "@payloadcms/richtext-lexical/client";
import { CollectionCards as CollectionCards_f9c02e79a4aed9a3924487c0cd4cafb1 } from "@payloadcms/next/rsc";
export const importMap = {
"@payloadcms/richtext-lexical/rsc#RscEntryLexicalCell":
RscEntryLexicalCell_44fe37237e0ebf4470c9990d8cb7b07e,
"@payloadcms/richtext-lexical/rsc#RscEntryLexicalField":
RscEntryLexicalField_44fe37237e0ebf4470c9990d8cb7b07e,
"@payloadcms/richtext-lexical/rsc#LexicalDiffComponent":
LexicalDiffComponent_44fe37237e0ebf4470c9990d8cb7b07e,
"@payloadcms/richtext-lexical/client#InlineToolbarFeatureClient":
InlineToolbarFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
"@payloadcms/richtext-lexical/client#HorizontalRuleFeatureClient":
HorizontalRuleFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
"@payloadcms/richtext-lexical/client#UploadFeatureClient":
UploadFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
"@payloadcms/richtext-lexical/client#BlockquoteFeatureClient":
BlockquoteFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
"@payloadcms/richtext-lexical/client#RelationshipFeatureClient":
RelationshipFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
"@payloadcms/richtext-lexical/client#LinkFeatureClient":
LinkFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
"@payloadcms/richtext-lexical/client#ChecklistFeatureClient":
ChecklistFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
"@payloadcms/richtext-lexical/client#OrderedListFeatureClient":
OrderedListFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
"@payloadcms/richtext-lexical/client#UnorderedListFeatureClient":
UnorderedListFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
"@payloadcms/richtext-lexical/client#IndentFeatureClient":
IndentFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
"@payloadcms/richtext-lexical/client#AlignFeatureClient":
AlignFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
"@payloadcms/richtext-lexical/client#HeadingFeatureClient":
HeadingFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
"@payloadcms/richtext-lexical/client#ParagraphFeatureClient":
ParagraphFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
"@payloadcms/richtext-lexical/client#InlineCodeFeatureClient":
InlineCodeFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
"@payloadcms/richtext-lexical/client#SuperscriptFeatureClient":
SuperscriptFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
"@payloadcms/richtext-lexical/client#SubscriptFeatureClient":
SubscriptFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
"@payloadcms/richtext-lexical/client#StrikethroughFeatureClient":
StrikethroughFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
"@payloadcms/richtext-lexical/client#UnderlineFeatureClient":
UnderlineFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
"@payloadcms/richtext-lexical/client#BoldFeatureClient":
BoldFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
"@payloadcms/richtext-lexical/client#ItalicFeatureClient":
ItalicFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
"@payloadcms/next/rsc#CollectionCards":
CollectionCards_f9c02e79a4aed9a3924487c0cd4cafb1,
};

View File

@@ -0,0 +1,16 @@
import config from "@payload-config";
import {
REST_DELETE,
REST_GET,
REST_OPTIONS,
REST_PATCH,
REST_POST,
REST_PUT,
} from "@payloadcms/next/routes";
export const GET = REST_GET(config);
export const POST = REST_POST(config);
export const DELETE = REST_DELETE(config);
export const OPTIONS = REST_OPTIONS(config);
export const PATCH = REST_PATCH(config);
export const PUT = REST_PUT(config);

View File

@@ -0,0 +1,20 @@
import configPromise from "@payload-config";
import "@payloadcms/next/css";
import { RootLayout } from "@payloadcms/next/layouts";
import React from "react";
// @ts-expect-error - Export exists in JS but TS types are missing in this version
import { handleServerFunctions } from "@payloadcms/next/utilities";
import { importMap } from "./admin/importMap";
export default function Layout({ children }: { children: React.ReactNode }) {
return (
<RootLayout
config={configPromise}
importMap={importMap}
serverFunction={handleServerFunctions}
>
{children}
</RootLayout>
);
}

View File

@@ -1,5 +1,5 @@
import { ImageResponse } from "next/og";
import { allPosts } from "contentlayer/generated";
import { getAllPosts } from "../../../src/lib/posts";
import { blogThumbnails } from "../../../src/components/blog/blogThumbnails";
import { BlogOGImageTemplate } from "../../../src/components/BlogOGImageTemplate";
import { getOgFonts, OG_IMAGE_SIZE } from "../../../src/lib/og-helper";
@@ -16,6 +16,7 @@ export default async function Image({
params: Promise<{ slug: string }>;
}) {
const { slug } = await params;
const allPosts = await getAllPosts();
const post = allPosts.find((p) => p.slug === slug);
let backgroundImageSrc: string | undefined = undefined;
@@ -27,11 +28,15 @@ export default async function Image({
const fileBuffer = await fs.readFile(filePath);
const ext = path.extname(post.thumbnail).substring(1).toLowerCase();
const mimeType = ext === "jpg" || ext === "jpeg" ? "image/jpeg" : "image/png";
const mimeType =
ext === "jpg" || ext === "jpeg" ? "image/jpeg" : "image/png";
backgroundImageSrc = `data:${mimeType};base64,${fileBuffer.toString("base64")}`;
} catch (err) {
console.warn(`[OG Image Generator] Could not read thumbnail file for ${slug} to use as background:`, err);
console.warn(
`[OG Image Generator] Could not read thumbnail file for ${slug} to use as background:`,
err,
);
// Fall through to standard plain background
}
}

View File

@@ -1,7 +1,7 @@
import * as React from "react";
import type { Metadata } from "next";
import { notFound } from "next/navigation";
import { allPosts } from "contentlayer/generated";
import { getAllPosts } from "../../../src/lib/posts";
import { BlogPostHeader } from "../../../src/components/blog/BlogPostHeader";
import { Section } from "../../../src/components/Section";
import { Reveal } from "../../../src/components/Reveal";
@@ -11,6 +11,7 @@ import { BlogPostStickyBar } from "../../../src/components/blog/BlogPostStickyBa
import { MDXContent } from "../../../src/components/MDXContent";
export async function generateStaticParams() {
const allPosts = await getAllPosts();
return allPosts.map((post) => ({
slug: post.slug,
}));
@@ -22,6 +23,7 @@ export async function generateMetadata({
params: Promise<{ slug: string }>;
}): Promise<Metadata> {
const { slug } = await params;
const allPosts = await getAllPosts();
const post = allPosts.find((p) => p.slug === slug);
if (!post) return {};
@@ -48,6 +50,7 @@ export default async function BlogPostPage({
params: Promise<{ slug: string }>;
}) {
const { slug } = await params;
const allPosts = await getAllPosts();
const post = allPosts.find((p) => p.slug === slug);
if (!post) {

View File

@@ -1,211 +1,14 @@
"use client";
import { getAllPosts } from "../../src/lib/posts";
import { BlogClient } from "../../src/components/blog/BlogClient";
import type { Metadata } from "next";
import * as React from "react";
import { useState, useEffect } from "react";
import { MediumCard } from "../../src/components/MediumCard";
import { BlogCommandBar } from "../../src/components/blog/BlogCommandBar";
import { allPosts as contentPosts } from "contentlayer/generated";
import { SectionHeader } from "../../src/components/SectionHeader";
import { Reveal } from "../../src/components/Reveal";
import { Section } from "../../src/components/Section";
import { AbstractCircuit, GradientMesh } from "../../src/components/Effects";
import { useAnalytics } from "../../src/components/analytics/useAnalytics";
export const metadata: Metadata = {
title: "Blog | Mintel.me",
description:
"Gedanken über Engineering, Design und die Architektur der Zukunft.",
};
import { motion, AnimatePresence } from "framer-motion";
export default function BlogPage() {
const [searchQuery, setSearchQuery] = useState("");
const [activeTags, setActiveTags] = useState<string[]>([]);
const { trackEvent } = useAnalytics();
// Memoize allPosts
const allPosts = React.useMemo(
() =>
[...contentPosts].sort(
(a, b) => new Date(b.date).getTime() - new Date(a.date).getTime(),
),
[],
);
const [filteredPosts, setFilteredPosts] = useState(allPosts);
// Memoize allTags
const allTags = React.useMemo(
() => Array.from(new Set(allPosts.flatMap((post) => post.tags || []))),
[allPosts],
);
const [visibleCount, setVisibleCount] = useState(8);
const handleTagToggle = (tag: string) => {
setActiveTags((prev) =>
prev.includes(tag) ? prev.filter((t) => t !== tag) : [...prev, tag],
);
setVisibleCount(8); // Reset pagination on filter change
};
useEffect(() => {
const query = searchQuery.toLowerCase().trim();
let filtered = allPosts;
if (query) {
filtered = filtered.filter((post) => {
const title = post.title.toLowerCase();
const description = post.description.toLowerCase();
const pTagString = (post.tags || []).join(" ").toLowerCase();
return (
title.includes(query) ||
description.includes(query) ||
pTagString.includes(query)
);
});
}
if (activeTags.length > 0) {
filtered = filtered.filter((post) =>
post.tags?.some((tag) => activeTags.includes(tag)),
);
}
setFilteredPosts(filtered);
}, [searchQuery, activeTags, allPosts]);
const loadMore = () => {
setVisibleCount((prev) => prev + 6);
};
const hasMore = visibleCount < filteredPosts.length;
const postsToShow = filteredPosts.slice(0, visibleCount);
return (
<div className="flex flex-col bg-slate-50/30 overflow-hidden relative min-h-screen">
<AbstractCircuit />
{/* Header Section */}
<header className="relative pt-32 pb-8 md:pt-44 md:pb-12 z-20 overflow-hidden">
<GradientMesh
variant="metallic"
className="opacity-20 absolute inset-0 -z-10"
/>
<div className="narrow-container">
<div className="space-y-4 text-center">
<Reveal direction="down" delay={0.1}>
<div className="flex items-center justify-center gap-4 text-[10px] font-bold uppercase tracking-[0.3em] text-slate-400">
<div className="w-6 h-px bg-slate-200" />
<span>Knowledge Base</span>
<div className="w-12 h-px bg-slate-100" />
</div>
</Reveal>
<Reveal delay={0.2}>
<div className="flex flex-col items-center gap-6">
<h1 className="text-5xl md:text-7xl font-bold text-slate-900 tracking-tighter leading-none">
Alle Artikel<span className="text-slate-300">.</span>
</h1>
<p className="font-serif italic text-slate-400 text-sm md:text-xl max-w-sm">
Gedanken über Engineering, Design und die Architektur der
Zukunft.
</p>
</div>
</Reveal>
</div>
</div>
</header>
{/* Sticky Filter Bar */}
<div className="sticky top-0 z-40 bg-slate-50/80 backdrop-blur-xl border-y border-slate-200/50 py-4 shadow-sm transition-all duration-300">
<div className="narrow-container">
<Reveal width="100%">
<BlogCommandBar
searchQuery={searchQuery}
onSearchChange={setSearchQuery}
tags={allTags}
activeTags={activeTags}
onTagToggle={handleTagToggle}
/>
</Reveal>
</div>
</div>
<Section
className="pb-32 pt-12"
containerVariant="narrow"
variant="white"
>
<div className="space-y-12 relative z-10 p-4 md:p-0">
{/* Posts List (Vertical & Minimal) */}
<div id="posts-container" className="space-y-12">
<AnimatePresence mode="popLayout" initial={false}>
{postsToShow.length === 0 ? (
<motion.div
key="no-results"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, scale: 0.95 }}
className="py-24 text-center border border-dashed border-slate-200 rounded-3xl bg-white/50"
>
<p className="text-slate-400 font-mono text-sm uppercase tracking-widest">
Keine Beiträge gefunden.
</p>
<button
onClick={() => {
setSearchQuery("");
setActiveTags([]);
}}
className="mt-4 text-xs font-bold text-slate-900 underline underline-offset-4 hover:text-slate-600"
>
Filter zurücksetzen
</button>
</motion.div>
) : (
<motion.div
key="post-grid"
layout
className="grid grid-cols-1 gap-6 w-full"
>
<AnimatePresence mode="popLayout">
{postsToShow.map((post, i) => (
<motion.div
key={post.slug}
layout
initial={{ opacity: 0, scale: 0.9 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.9 }}
transition={{
duration: 0.4,
delay: i < 8 ? i * 0.05 : 0,
ease: [0.16, 1, 0.3, 1]
}}
>
<MediumCard post={post} />
</motion.div>
))}
</AnimatePresence>
</motion.div>
)}
</AnimatePresence>
{/* Pagination */}
{hasMore && (
<div className="flex justify-center pt-8">
<Reveal delay={0.1} width="fit-content">
<button
onClick={loadMore}
className="group relative px-8 py-4 bg-white border border-slate-200 text-slate-600 rounded-full overflow-hidden transition-all hover:border-slate-400 hover:text-slate-900 hover:shadow-lg"
>
<span className="relative z-10 font-mono text-xs uppercase tracking-widest flex items-center gap-3">
Mehr laden
<div className="w-1 h-1 bg-slate-300 rounded-full group-hover:bg-slate-900 transition-colors" />
<div className="w-1 h-1 bg-slate-300 rounded-full group-hover:bg-slate-900 transition-colors delay-75" />
<div className="w-1 h-1 bg-slate-300 rounded-full group-hover:bg-slate-900 transition-colors delay-150" />
</span>
</button>
</Reveal>
</div>
)}
</div>
</div>
</Section>
</div>
);
export default async function BlogPage() {
const posts = await getAllPosts();
return <BlogClient allPosts={posts as any} />;
}

View File

@@ -1,56 +1,57 @@
import { MetadataRoute } from 'next';
import { allPosts } from 'contentlayer/generated';
import { technologies } from './technologies/[slug]/data';
import { MetadataRoute } from "next";
import { getAllPosts } from "../src/lib/posts";
import { technologies } from "./technologies/[slug]/data";
/**
* Sitemap Generator
*
*
* Standard Next.js 15 App Router sitemap generation.
* This file dynamically generates /sitemap.xml
*/
export default function sitemap(): MetadataRoute.Sitemap {
const baseUrl = process.env.NEXT_PUBLIC_BASE_URL || 'https://mintel.me';
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
const allPosts = await getAllPosts();
const baseUrl = process.env.NEXT_PUBLIC_BASE_URL || "https://mintel.me";
// 1. Core Pages
const routes = [
'',
'/about',
'/blog',
'/case-studies',
'/case-studies/klz-cables',
'/contact',
'/websites',
].map((route) => ({
url: `${baseUrl}${route}`,
lastModified: new Date(),
changeFrequency: 'monthly' as const,
priority: route === '' ? 1.0 : 0.8,
}));
// 1. Core Pages
const routes = [
"",
"/about",
"/blog",
"/case-studies",
"/case-studies/klz-cables",
"/contact",
"/websites",
].map((route) => ({
url: `${baseUrl}${route}`,
lastModified: new Date(),
changeFrequency: "monthly" as const,
priority: route === "" ? 1.0 : 0.8,
}));
// 2. Dynamic Blog Posts
const blogRoutes = allPosts.map((post) => ({
url: `${baseUrl}/blog/${post.slug}`,
lastModified: new Date(post.date),
changeFrequency: 'monthly' as const,
priority: 0.7,
}));
// 2. Dynamic Blog Posts
const blogRoutes = allPosts.map((post) => ({
url: `${baseUrl}/blog/${post.slug}`,
lastModified: new Date(post.date),
changeFrequency: "monthly" as const,
priority: 0.7,
}));
// 3. Technology Detail Pages
const techRoutes = Object.keys(technologies).map((slug) => ({
url: `${baseUrl}/technologies/${slug}`,
lastModified: new Date(),
changeFrequency: 'monthly' as const,
priority: 0.6,
}));
// 3. Technology Detail Pages
const techRoutes = Object.keys(technologies).map((slug) => ({
url: `${baseUrl}/technologies/${slug}`,
lastModified: new Date(),
changeFrequency: "monthly" as const,
priority: 0.6,
}));
// 4. Tag Pages
const allTags = [...new Set(allPosts.flatMap((post) => post.tags))];
const tagRoutes = allTags.map((tag) => ({
url: `${baseUrl}/tags/${tag}`,
lastModified: new Date(),
changeFrequency: 'weekly' as const,
priority: 0.3,
}));
// 4. Tag Pages
const allTags = [...new Set(allPosts.flatMap((post) => post.tags))];
const tagRoutes = allTags.map((tag) => ({
url: `${baseUrl}/tags/${tag}`,
lastModified: new Date(),
changeFrequency: "weekly" as const,
priority: 0.3,
}));
return [...routes, ...blogRoutes, ...techRoutes, ...tagRoutes];
return [...routes, ...blogRoutes, ...techRoutes, ...tagRoutes];
}

View File

@@ -1,9 +1,10 @@
import Link from "next/link";
import { allPosts } from "contentlayer/generated";
import { getAllPosts } from "../../../src/lib/posts";
import { MediumCard } from "../../../src/components/MediumCard";
import { Reveal } from "../../../src/components/Reveal";
export async function generateStaticParams() {
const allPosts = await getAllPosts();
const allTags = Array.from(
new Set(allPosts.flatMap((post) => post.tags || [])),
);
@@ -18,6 +19,7 @@ export default async function TagPage({
params: Promise<{ tag: string }>;
}) {
const { tag } = await params;
const allPosts = await getAllPosts();
const posts = allPosts.filter((post) => post.tags?.includes(tag));
return (