feat(blog): enhance blog functionality and SEO
- Added a new '/blog' route to middleware for improved navigation. - Updated sitemap to dynamically include blog posts and series URLs. - Enhanced blog page metadata with Open Graph and Twitter card information. - Implemented structured data for blog posts and series to improve SEO. - Removed outdated blog post "Week 1: De Kickoff" from content. - Introduced optional OG image field in blog frontmatter for better sharing visuals.
This commit is contained in:
@@ -10,6 +10,21 @@ import { getAllPosts, getSeriesWithCounts, type BlogSeries } from '@/lib/mdx/blo
|
||||
export const metadata = {
|
||||
title: 'Blog - AI Speedrun',
|
||||
description: 'Artikelen over AI-gestuurde software ontwikkeling, healthcare IT en het bouwen van een EPD.',
|
||||
openGraph: {
|
||||
title: 'Blog - AI Speedrun',
|
||||
description: 'Artikelen over AI-gestuurde software ontwikkeling, healthcare IT en het bouwen van een EPD.',
|
||||
type: 'website',
|
||||
siteName: 'AI Speedrun',
|
||||
locale: 'nl_NL',
|
||||
},
|
||||
twitter: {
|
||||
card: 'summary',
|
||||
title: 'Blog - AI Speedrun',
|
||||
description: 'Artikelen over AI-gestuurde software ontwikkeling, healthcare IT en het bouwen van een EPD.',
|
||||
},
|
||||
alternates: {
|
||||
canonical: `${process.env.NEXT_PUBLIC_APP_URL || 'https://aispeedrun.vercel.app'}/blog`,
|
||||
},
|
||||
}
|
||||
|
||||
export default async function BlogPage() {
|
||||
|
||||
@@ -31,14 +31,43 @@ export async function generateMetadata({ params }: PostPageProps) {
|
||||
return { title: 'Post niet gevonden' }
|
||||
}
|
||||
|
||||
const siteUrl = process.env.NEXT_PUBLIC_APP_URL || 'https://aispeedrun.vercel.app'
|
||||
const postUrl = `${siteUrl}/blog/serie/${serieId}/${slug}`
|
||||
// Use post-specific image if provided, otherwise fall back to default
|
||||
const ogImageUrl = post.frontmatter.image
|
||||
? `${siteUrl}${post.frontmatter.image.startsWith('/') ? '' : '/'}${post.frontmatter.image}`
|
||||
: `${siteUrl}/og-blog-default.png`
|
||||
|
||||
return {
|
||||
title: `${post.frontmatter.title} - ${series.title}`,
|
||||
description: post.frontmatter.description,
|
||||
authors: [{ name: 'Colin van der Heijden', url: 'https://ikbenlit.nl' }],
|
||||
keywords: post.frontmatter.tags || [],
|
||||
openGraph: {
|
||||
title: post.frontmatter.title,
|
||||
description: post.frontmatter.description,
|
||||
type: 'article',
|
||||
publishedTime: post.frontmatter.date,
|
||||
authors: ['Colin van der Heijden'],
|
||||
siteName: 'AI Speedrun',
|
||||
locale: 'nl_NL',
|
||||
images: [
|
||||
{
|
||||
url: ogImageUrl,
|
||||
width: 1200,
|
||||
height: 630,
|
||||
alt: post.frontmatter.title,
|
||||
},
|
||||
],
|
||||
},
|
||||
twitter: {
|
||||
card: 'summary_large_image',
|
||||
title: post.frontmatter.title,
|
||||
description: post.frontmatter.description,
|
||||
images: [ogImageUrl],
|
||||
},
|
||||
alternates: {
|
||||
canonical: postUrl,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -55,6 +84,77 @@ export default async function PostPage({ params }: PostPageProps) {
|
||||
const navigation = await getSeriesNavigation(serieId, slug)
|
||||
const { frontmatter, content, readingTime } = post
|
||||
|
||||
// Generate Article structured data for SEO
|
||||
const siteUrl = process.env.NEXT_PUBLIC_APP_URL || 'https://aispeedrun.vercel.app'
|
||||
const postUrl = `${siteUrl}/blog/serie/${serieId}/${slug}`
|
||||
// Use post-specific image if provided, otherwise fall back to default
|
||||
const articleImage = frontmatter.image
|
||||
? `${siteUrl}${frontmatter.image.startsWith('/') ? '' : '/'}${frontmatter.image}`
|
||||
: `${siteUrl}/og-blog-default.png`
|
||||
const articleSchema = {
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'Article',
|
||||
headline: frontmatter.title,
|
||||
description: frontmatter.description,
|
||||
image: articleImage,
|
||||
datePublished: frontmatter.date,
|
||||
dateModified: frontmatter.date,
|
||||
author: {
|
||||
'@type': 'Person',
|
||||
name: 'Colin van der Heijden',
|
||||
url: 'https://ikbenlit.nl',
|
||||
},
|
||||
publisher: {
|
||||
'@type': 'Organization',
|
||||
name: 'AI Speedrun',
|
||||
url: siteUrl,
|
||||
logo: {
|
||||
'@type': 'ImageObject',
|
||||
url: `${siteUrl}/images/aispeedrun-logo.webp`,
|
||||
},
|
||||
},
|
||||
mainEntityOfPage: {
|
||||
'@type': 'WebPage',
|
||||
'@id': postUrl,
|
||||
},
|
||||
articleSection: series.title,
|
||||
keywords: frontmatter.tags?.join(', ') || '',
|
||||
wordCount: content.split(/\s+/).length,
|
||||
timeRequired: `PT${readingTime}M`,
|
||||
}
|
||||
|
||||
// Generate BreadcrumbList structured data
|
||||
const breadcrumbSchema = {
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'BreadcrumbList',
|
||||
itemListElement: [
|
||||
{
|
||||
'@type': 'ListItem',
|
||||
position: 1,
|
||||
name: 'Home',
|
||||
item: siteUrl,
|
||||
},
|
||||
{
|
||||
'@type': 'ListItem',
|
||||
position: 2,
|
||||
name: 'Blog',
|
||||
item: `${siteUrl}/blog`,
|
||||
},
|
||||
{
|
||||
'@type': 'ListItem',
|
||||
position: 3,
|
||||
name: series.title,
|
||||
item: `${siteUrl}/blog/serie/${serieId}`,
|
||||
},
|
||||
{
|
||||
'@type': 'ListItem',
|
||||
position: 4,
|
||||
name: frontmatter.title,
|
||||
item: postUrl,
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
const colorStyles = {
|
||||
teal: {
|
||||
badge: 'bg-teal-100 text-teal-700 border-teal-200',
|
||||
@@ -74,6 +174,16 @@ export default async function PostPage({ params }: PostPageProps) {
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-white pb-16">
|
||||
{/* Structured Data for SEO */}
|
||||
<script
|
||||
type="application/ld+json"
|
||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(articleSchema) }}
|
||||
/>
|
||||
<script
|
||||
type="application/ld+json"
|
||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(breadcrumbSchema) }}
|
||||
/>
|
||||
|
||||
<article className="max-w-3xl mx-auto px-4 md:px-8">
|
||||
{/* Back link */}
|
||||
<div className="pt-20 md:pt-24 mb-6">
|
||||
|
||||
@@ -25,9 +25,27 @@ export async function generateMetadata({ params }: SeriesPageProps) {
|
||||
return { title: 'Serie niet gevonden' }
|
||||
}
|
||||
|
||||
const siteUrl = process.env.NEXT_PUBLIC_APP_URL || 'https://aispeedrun.vercel.app'
|
||||
const seriesUrl = `${siteUrl}/blog/serie/${serieId}`
|
||||
|
||||
return {
|
||||
title: `${series.title} - Blog`,
|
||||
title: `${series.title} - Blog | AI Speedrun`,
|
||||
description: series.description,
|
||||
openGraph: {
|
||||
title: series.title,
|
||||
description: series.description,
|
||||
type: 'website',
|
||||
siteName: 'AI Speedrun',
|
||||
locale: 'nl_NL',
|
||||
},
|
||||
twitter: {
|
||||
card: 'summary',
|
||||
title: series.title,
|
||||
description: series.description,
|
||||
},
|
||||
alternates: {
|
||||
canonical: seriesUrl,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
|
||||
import type { MetadataRoute } from 'next'
|
||||
import { getAllReleases } from '@/lib/mdx/documentatie'
|
||||
import { getAllPosts, getAllSeries } from '@/lib/mdx/blog'
|
||||
|
||||
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
|
||||
const baseUrl = process.env.NEXT_PUBLIC_APP_URL || 'https://aispeedrun.vercel.app'
|
||||
@@ -27,6 +28,29 @@ export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
|
||||
}
|
||||
})
|
||||
|
||||
// Fetch all blog posts dynamically
|
||||
const posts = await getAllPosts()
|
||||
const postUrls: MetadataRoute.Sitemap = posts.map((post) => {
|
||||
const postDate = new Date(post.frontmatter.date)
|
||||
const isValidDate = !isNaN(postDate.getTime())
|
||||
|
||||
return {
|
||||
url: `${baseUrl}/blog/serie/${post.seriesId}/${post.slug}`,
|
||||
lastModified: isValidDate ? postDate : currentDate,
|
||||
changeFrequency: 'monthly',
|
||||
priority: 0.7,
|
||||
}
|
||||
})
|
||||
|
||||
// Fetch all blog series
|
||||
const series = await getAllSeries()
|
||||
const seriesUrls: MetadataRoute.Sitemap = series.map((serie) => ({
|
||||
url: `${baseUrl}/blog/serie/${serie.id}`,
|
||||
lastModified: currentDate,
|
||||
changeFrequency: 'weekly',
|
||||
priority: 0.8,
|
||||
}))
|
||||
|
||||
return [
|
||||
{
|
||||
url: baseUrl,
|
||||
@@ -34,12 +58,20 @@ export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
|
||||
changeFrequency: 'weekly',
|
||||
priority: 1.0,
|
||||
},
|
||||
{
|
||||
url: `${baseUrl}/blog`,
|
||||
lastModified: currentDate,
|
||||
changeFrequency: 'weekly',
|
||||
priority: 0.9,
|
||||
},
|
||||
{
|
||||
url: `${baseUrl}/documentatie`,
|
||||
lastModified: currentDate,
|
||||
changeFrequency: 'weekly',
|
||||
priority: 0.9,
|
||||
},
|
||||
...seriesUrls,
|
||||
...postUrls,
|
||||
...releaseUrls,
|
||||
{
|
||||
url: `${baseUrl}/contact`,
|
||||
|
||||
Reference in New Issue
Block a user