feat(navigation): add Blog link to Dutch navigation
- Introduced a new link for "Blog" in the navigation JSON file, enhancing user access to blog content.
This commit is contained in:
154
app/(marketing)/blog/page.tsx
Normal file
154
app/(marketing)/blog/page.tsx
Normal file
@@ -0,0 +1,154 @@
|
||||
/**
|
||||
* Blog Overview Page
|
||||
*
|
||||
* Landing page with series cards and recent posts
|
||||
*/
|
||||
|
||||
import Link from 'next/link'
|
||||
import { getAllPosts, getSeriesWithCounts, type BlogSeries } from '@/lib/mdx/blog'
|
||||
|
||||
export const metadata = {
|
||||
title: 'Blog - AI Speedrun',
|
||||
description: 'Artikelen over AI-gestuurde software ontwikkeling, healthcare IT en het bouwen van een EPD.',
|
||||
}
|
||||
|
||||
export default async function BlogPage() {
|
||||
const series = await getSeriesWithCounts()
|
||||
const posts = await getAllPosts()
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-slate-50 pb-16">
|
||||
<div className="max-w-4xl mx-auto px-4 sm:px-6 md:px-8">
|
||||
{/* Header */}
|
||||
<div className="mb-8 md:mb-12 pt-20 md:pt-24">
|
||||
<h1 className="text-3xl md:text-4xl lg:text-5xl font-bold text-slate-900 mb-3 md:mb-4">
|
||||
Blog
|
||||
</h1>
|
||||
<p className="text-lg md:text-xl text-slate-600 max-w-3xl leading-relaxed">
|
||||
Artikelen over AI-gestuurde ontwikkeling en de reis van het bouwen van een EPD.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Series Section */}
|
||||
{series.length > 0 && (
|
||||
<section className="mb-12">
|
||||
<h2 className="text-xs font-semibold text-slate-500 uppercase tracking-wide mb-4">
|
||||
Series
|
||||
</h2>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
{series.map((serie) => (
|
||||
<SeriesCard key={serie.id} series={serie} />
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* Recent Posts Section */}
|
||||
<section>
|
||||
<h2 className="text-xs font-semibold text-slate-500 uppercase tracking-wide mb-4">
|
||||
Recente Posts
|
||||
</h2>
|
||||
{posts.length === 0 ? (
|
||||
<p className="text-slate-500 text-lg text-center py-12">
|
||||
Nog geen blogposts gepubliceerd.
|
||||
</p>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
{posts.map((post) => (
|
||||
<Link
|
||||
key={`${post.seriesId}-${post.slug}`}
|
||||
href={`/blog/serie/${post.seriesId}/${post.slug}`}
|
||||
className="block bg-white rounded-lg border border-slate-200 p-5 md:p-6 hover:border-teal-300 hover:shadow-md transition-all active:scale-[0.99]"
|
||||
>
|
||||
<div className="flex flex-wrap items-center gap-2 mb-2 text-sm text-slate-500">
|
||||
<span className="text-xs font-medium uppercase tracking-wide">
|
||||
{post.seriesId.replace('-', ' ')}
|
||||
</span>
|
||||
<span>•</span>
|
||||
<time dateTime={post.frontmatter.date}>
|
||||
{new Date(post.frontmatter.date).toLocaleDateString('nl-NL', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
})}
|
||||
</time>
|
||||
<span>•</span>
|
||||
<span>{post.readingTime} min leestijd</span>
|
||||
</div>
|
||||
<h3 className="text-xl md:text-2xl font-bold text-slate-900 mb-2 leading-tight">
|
||||
{post.frontmatter.title}
|
||||
</h3>
|
||||
<p className="text-slate-600 leading-relaxed line-clamp-2">
|
||||
{post.frontmatter.description}
|
||||
</p>
|
||||
{post.frontmatter.tags && post.frontmatter.tags.length > 0 && (
|
||||
<div className="flex flex-wrap gap-2 mt-4">
|
||||
{post.frontmatter.tags.map((tag) => (
|
||||
<span
|
||||
key={tag}
|
||||
className="px-2 py-1 bg-slate-100 text-slate-600 text-xs rounded-full"
|
||||
>
|
||||
{tag}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function SeriesCard({ series }: { series: BlogSeries & { postCount: number } }) {
|
||||
const colorStyles = {
|
||||
teal: {
|
||||
bg: 'bg-teal-50',
|
||||
border: 'border-teal-200 hover:border-teal-400',
|
||||
badge: 'bg-teal-100 text-teal-700',
|
||||
dot: 'bg-teal-500',
|
||||
},
|
||||
amber: {
|
||||
bg: 'bg-amber-50',
|
||||
border: 'border-amber-200 hover:border-amber-400',
|
||||
badge: 'bg-amber-100 text-amber-700',
|
||||
dot: 'bg-amber-500',
|
||||
},
|
||||
slate: {
|
||||
bg: 'bg-slate-50',
|
||||
border: 'border-slate-200 hover:border-slate-400',
|
||||
badge: 'bg-slate-100 text-slate-700',
|
||||
dot: 'bg-slate-500',
|
||||
},
|
||||
}
|
||||
|
||||
const styles = colorStyles[series.color] || colorStyles.slate
|
||||
const statusLabels = {
|
||||
completed: 'Voltooid',
|
||||
active: 'Actief',
|
||||
planned: 'Gepland',
|
||||
}
|
||||
|
||||
return (
|
||||
<Link
|
||||
href={`/blog/serie/${series.id}`}
|
||||
className={`block rounded-lg border-2 p-5 transition-all hover:shadow-md active:scale-[0.98] ${styles.bg} ${styles.border}`}
|
||||
>
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<div className={`w-2 h-2 rounded-full ${styles.dot}`} />
|
||||
<span className={`text-xs font-medium px-2 py-0.5 rounded-full ${styles.badge}`}>
|
||||
{statusLabels[series.status]}
|
||||
</span>
|
||||
</div>
|
||||
<h3 className="text-lg font-bold text-slate-900 mb-1">{series.title}</h3>
|
||||
<p className="text-sm text-slate-600 mb-3 line-clamp-2">{series.description}</p>
|
||||
<div className="text-xs text-slate-500">
|
||||
{series.postCount} {series.postCount === 1 ? 'deel' : 'delen'}
|
||||
</div>
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
||||
214
app/(marketing)/blog/serie/[serieId]/[slug]/page.tsx
Normal file
214
app/(marketing)/blog/serie/[serieId]/[slug]/page.tsx
Normal file
@@ -0,0 +1,214 @@
|
||||
/**
|
||||
* Individual Blog Post Page
|
||||
*
|
||||
* Renders MDX content with series navigation
|
||||
*/
|
||||
|
||||
import { notFound } from 'next/navigation'
|
||||
import Link from 'next/link'
|
||||
import { MDXRemote } from 'next-mdx-remote/rsc'
|
||||
import { getPost, getSeries, getPostsBySeries, getSeriesNavigation, getAllPosts } from '@/lib/mdx/blog'
|
||||
import { mdxComponents } from '../../../../documentatie/components/mdx-components'
|
||||
|
||||
interface PostPageProps {
|
||||
params: Promise<{ serieId: string; slug: string }>
|
||||
}
|
||||
|
||||
export async function generateStaticParams() {
|
||||
const posts = await getAllPosts()
|
||||
return posts.map((post) => ({
|
||||
serieId: post.seriesId,
|
||||
slug: post.slug,
|
||||
}))
|
||||
}
|
||||
|
||||
export async function generateMetadata({ params }: PostPageProps) {
|
||||
const { serieId, slug } = await params
|
||||
const post = await getPost(serieId, slug)
|
||||
const series = await getSeries(serieId)
|
||||
|
||||
if (!post || !series) {
|
||||
return { title: 'Post niet gevonden' }
|
||||
}
|
||||
|
||||
return {
|
||||
title: `${post.frontmatter.title} - ${series.title}`,
|
||||
description: post.frontmatter.description,
|
||||
openGraph: {
|
||||
title: post.frontmatter.title,
|
||||
description: post.frontmatter.description,
|
||||
type: 'article',
|
||||
publishedTime: post.frontmatter.date,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export default async function PostPage({ params }: PostPageProps) {
|
||||
const { serieId, slug } = await params
|
||||
const post = await getPost(serieId, slug)
|
||||
const series = await getSeries(serieId)
|
||||
|
||||
if (!post || !series) {
|
||||
notFound()
|
||||
}
|
||||
|
||||
const navigation = await getSeriesNavigation(serieId, slug)
|
||||
const { frontmatter, content, readingTime } = post
|
||||
|
||||
const colorStyles = {
|
||||
teal: {
|
||||
badge: 'bg-teal-100 text-teal-700 border-teal-200',
|
||||
link: 'text-teal-600 hover:text-teal-700',
|
||||
},
|
||||
amber: {
|
||||
badge: 'bg-amber-100 text-amber-700 border-amber-200',
|
||||
link: 'text-amber-600 hover:text-amber-700',
|
||||
},
|
||||
slate: {
|
||||
badge: 'bg-slate-100 text-slate-700 border-slate-200',
|
||||
link: 'text-slate-600 hover:text-slate-700',
|
||||
},
|
||||
}
|
||||
|
||||
const styles = colorStyles[series.color] || colorStyles.slate
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-white pb-16">
|
||||
<article className="max-w-3xl mx-auto px-4 md:px-8">
|
||||
{/* Back link */}
|
||||
<div className="pt-20 md:pt-24 mb-6">
|
||||
<Link
|
||||
href={`/blog/serie/${serieId}`}
|
||||
className="inline-flex items-center gap-2 text-slate-600 hover:text-teal-600 transition-colors"
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
|
||||
</svg>
|
||||
<span>Terug naar {series.title}</span>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{/* Header */}
|
||||
<header className="mb-8 pb-8 border-b border-slate-200">
|
||||
{/* Series badge */}
|
||||
<div className="flex flex-wrap items-center gap-3 mb-4">
|
||||
<Link
|
||||
href={`/blog/serie/${serieId}`}
|
||||
className={`inline-flex items-center px-3 py-1 rounded-full text-xs font-medium border transition-colors hover:opacity-80 ${styles.badge}`}
|
||||
>
|
||||
{series.title}
|
||||
</Link>
|
||||
<span className="text-sm text-slate-500">
|
||||
Deel {navigation.current} van {navigation.total}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Title */}
|
||||
<h1 className="text-3xl md:text-4xl lg:text-5xl font-bold text-slate-900 mb-4 leading-tight">
|
||||
{frontmatter.title}
|
||||
</h1>
|
||||
|
||||
{/* Description */}
|
||||
<p className="text-xl text-slate-600 leading-relaxed mb-4">
|
||||
{frontmatter.description}
|
||||
</p>
|
||||
|
||||
{/* Meta */}
|
||||
<div className="flex flex-wrap items-center gap-3 text-sm text-slate-500">
|
||||
<time dateTime={frontmatter.date}>
|
||||
{new Date(frontmatter.date).toLocaleDateString('nl-NL', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
})}
|
||||
</time>
|
||||
<span>•</span>
|
||||
<span>{readingTime} min leestijd</span>
|
||||
</div>
|
||||
|
||||
{/* Tags */}
|
||||
{frontmatter.tags && frontmatter.tags.length > 0 && (
|
||||
<div className="flex flex-wrap gap-2 mt-4">
|
||||
{frontmatter.tags.map((tag) => (
|
||||
<span
|
||||
key={tag}
|
||||
className="px-2.5 py-1 bg-slate-100 text-slate-600 text-xs rounded-full"
|
||||
>
|
||||
{tag}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</header>
|
||||
|
||||
{/* MDX Content */}
|
||||
<div className="prose prose-slate max-w-none">
|
||||
<MDXRemote source={content} components={mdxComponents} />
|
||||
</div>
|
||||
|
||||
{/* Series Navigation */}
|
||||
<nav className="mt-12 pt-8 border-t border-slate-200">
|
||||
<div className="text-xs font-semibold text-slate-500 uppercase tracking-wide mb-4">
|
||||
{series.title} Serie
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
{/* Previous */}
|
||||
<div>
|
||||
{navigation.previous ? (
|
||||
<Link
|
||||
href={`/blog/serie/${serieId}/${navigation.previous.slug}`}
|
||||
className="block p-4 rounded-lg border border-slate-200 hover:border-teal-300 hover:shadow-sm transition-all group"
|
||||
>
|
||||
<div className="flex items-center gap-2 text-sm text-slate-500 mb-1">
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
|
||||
</svg>
|
||||
<span>Vorige</span>
|
||||
</div>
|
||||
<div className="font-medium text-slate-900 group-hover:text-teal-600 transition-colors line-clamp-1">
|
||||
{navigation.previous.title}
|
||||
</div>
|
||||
</Link>
|
||||
) : (
|
||||
<div />
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Next */}
|
||||
<div>
|
||||
{navigation.next ? (
|
||||
<Link
|
||||
href={`/blog/serie/${serieId}/${navigation.next.slug}`}
|
||||
className="block p-4 rounded-lg border border-slate-200 hover:border-teal-300 hover:shadow-sm transition-all group text-right"
|
||||
>
|
||||
<div className="flex items-center justify-end gap-2 text-sm text-slate-500 mb-1">
|
||||
<span>Volgende</span>
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
</div>
|
||||
<div className="font-medium text-slate-900 group-hover:text-teal-600 transition-colors line-clamp-1">
|
||||
{navigation.next.title}
|
||||
</div>
|
||||
</Link>
|
||||
) : (
|
||||
<div />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
{/* Back to overview */}
|
||||
<footer className="mt-8 text-center">
|
||||
<Link
|
||||
href="/blog"
|
||||
className="text-sm text-slate-600 hover:text-teal-600 transition-colors"
|
||||
>
|
||||
← Terug naar blog overzicht
|
||||
</Link>
|
||||
</footer>
|
||||
</article>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
160
app/(marketing)/blog/serie/[serieId]/page.tsx
Normal file
160
app/(marketing)/blog/serie/[serieId]/page.tsx
Normal file
@@ -0,0 +1,160 @@
|
||||
/**
|
||||
* Series Overview Page
|
||||
*
|
||||
* Shows all posts in a specific series
|
||||
*/
|
||||
|
||||
import { notFound } from 'next/navigation'
|
||||
import Link from 'next/link'
|
||||
import { getSeries, getPostsBySeries, getAllSeries } from '@/lib/mdx/blog'
|
||||
|
||||
interface SeriesPageProps {
|
||||
params: Promise<{ serieId: string }>
|
||||
}
|
||||
|
||||
export async function generateStaticParams() {
|
||||
const series = await getAllSeries()
|
||||
return series.map((serie) => ({ serieId: serie.id }))
|
||||
}
|
||||
|
||||
export async function generateMetadata({ params }: SeriesPageProps) {
|
||||
const { serieId } = await params
|
||||
const series = await getSeries(serieId)
|
||||
|
||||
if (!series) {
|
||||
return { title: 'Serie niet gevonden' }
|
||||
}
|
||||
|
||||
return {
|
||||
title: `${series.title} - Blog`,
|
||||
description: series.description,
|
||||
}
|
||||
}
|
||||
|
||||
export default async function SeriesPage({ params }: SeriesPageProps) {
|
||||
const { serieId } = await params
|
||||
const series = await getSeries(serieId)
|
||||
|
||||
if (!series) {
|
||||
notFound()
|
||||
}
|
||||
|
||||
const posts = await getPostsBySeries(serieId)
|
||||
|
||||
const colorStyles = {
|
||||
teal: {
|
||||
bg: 'bg-teal-50',
|
||||
badge: 'bg-teal-100 text-teal-700 border-teal-200',
|
||||
dot: 'bg-teal-500',
|
||||
number: 'bg-teal-600 text-white',
|
||||
},
|
||||
amber: {
|
||||
bg: 'bg-amber-50',
|
||||
badge: 'bg-amber-100 text-amber-700 border-amber-200',
|
||||
dot: 'bg-amber-500',
|
||||
number: 'bg-amber-600 text-white',
|
||||
},
|
||||
slate: {
|
||||
bg: 'bg-slate-50',
|
||||
badge: 'bg-slate-100 text-slate-700 border-slate-200',
|
||||
dot: 'bg-slate-500',
|
||||
number: 'bg-slate-600 text-white',
|
||||
},
|
||||
}
|
||||
|
||||
const styles = colorStyles[series.color] || colorStyles.slate
|
||||
const statusLabels = {
|
||||
completed: 'Voltooid',
|
||||
active: 'Actief',
|
||||
planned: 'Gepland',
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-slate-50 pb-16">
|
||||
<div className="max-w-3xl mx-auto px-4 sm:px-6 md:px-8">
|
||||
{/* Back link */}
|
||||
<div className="pt-20 md:pt-24 mb-6">
|
||||
<Link
|
||||
href="/blog"
|
||||
className="inline-flex items-center gap-2 text-slate-600 hover:text-teal-600 transition-colors"
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
|
||||
</svg>
|
||||
<span>Terug naar blog</span>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{/* Series Header */}
|
||||
<header className={`rounded-xl p-6 md:p-8 mb-8 ${styles.bg}`}>
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<div className={`w-2.5 h-2.5 rounded-full ${styles.dot}`} />
|
||||
<span className={`text-xs font-medium px-2.5 py-1 rounded-full border ${styles.badge}`}>
|
||||
{statusLabels[series.status]}
|
||||
</span>
|
||||
</div>
|
||||
<h1 className="text-3xl md:text-4xl font-bold text-slate-900 mb-3">
|
||||
{series.title}
|
||||
</h1>
|
||||
<p className="text-lg text-slate-600 leading-relaxed">
|
||||
{series.description}
|
||||
</p>
|
||||
<div className="mt-4 text-sm text-slate-500">
|
||||
{posts.length} {posts.length === 1 ? 'deel' : 'delen'}
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* Posts List */}
|
||||
{posts.length === 0 ? (
|
||||
<p className="text-slate-500 text-center py-12">
|
||||
Nog geen posts in deze serie.
|
||||
</p>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{posts.map((post, index) => (
|
||||
<Link
|
||||
key={post.slug}
|
||||
href={`/blog/serie/${serieId}/${post.slug}`}
|
||||
className="flex items-start gap-4 bg-white rounded-lg border border-slate-200 p-4 md:p-5 hover:border-teal-300 hover:shadow-md transition-all active:scale-[0.99]"
|
||||
>
|
||||
<div
|
||||
className={`flex-shrink-0 w-8 h-8 rounded-full flex items-center justify-center text-sm font-bold ${styles.number}`}
|
||||
>
|
||||
{index + 1}
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<h2 className="text-lg font-bold text-slate-900 mb-1 leading-tight">
|
||||
{post.frontmatter.title}
|
||||
</h2>
|
||||
<p className="text-sm text-slate-600 mb-2 line-clamp-2">
|
||||
{post.frontmatter.description}
|
||||
</p>
|
||||
<div className="flex flex-wrap items-center gap-2 text-xs text-slate-500">
|
||||
<time dateTime={post.frontmatter.date}>
|
||||
{new Date(post.frontmatter.date).toLocaleDateString('nl-NL', {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
})}
|
||||
</time>
|
||||
<span>•</span>
|
||||
<span>{post.readingTime} min</span>
|
||||
</div>
|
||||
</div>
|
||||
<svg
|
||||
className="w-5 h-5 text-slate-400 flex-shrink-0 mt-1"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user