- Next.js 14 blog application with theme support - Docker containerization with volume bindings - Traefik integration with Let's Encrypt SSL - MDX support for blog posts - Theme toggle with localStorage persistence - Scheduled posts directory structure - Brand guidelines compliance with CHORUS colors 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
174 lines
5.7 KiB
TypeScript
174 lines
5.7 KiB
TypeScript
import { notFound } from 'next/navigation'
|
|
import { MDXRemote } from 'next-mdx-remote/rsc'
|
|
import { getPostData, getAllPostSlugs } from '@/lib/blog'
|
|
import BlogHeader from '@/components/BlogHeader'
|
|
import BlogFooter from '@/components/BlogFooter'
|
|
import Link from 'next/link'
|
|
import remarkGfm from 'remark-gfm'
|
|
import rehypeHighlight from 'rehype-highlight'
|
|
import rehypeSlug from 'rehype-slug'
|
|
import rehypeAutolinkHeadings from 'rehype-autolink-headings'
|
|
|
|
interface PostPageProps {
|
|
params: Promise<{
|
|
slug: string
|
|
}>
|
|
}
|
|
|
|
export async function generateStaticParams() {
|
|
const slugs = getAllPostSlugs()
|
|
return slugs.map(({ params }) => ({ slug: params.slug }))
|
|
}
|
|
|
|
export async function generateMetadata({ params }: PostPageProps) {
|
|
const { slug } = await params
|
|
const post = getPostData(slug)
|
|
|
|
if (!post) {
|
|
return {
|
|
title: 'Post Not Found - CHORUS PING!'
|
|
}
|
|
}
|
|
|
|
return {
|
|
title: `${post.title} - CHORUS PING!`,
|
|
description: post.description,
|
|
openGraph: {
|
|
title: post.title,
|
|
description: post.description,
|
|
type: 'article',
|
|
publishedTime: post.date,
|
|
authors: [post.author?.name || 'CHORUS Team'],
|
|
tags: post.tags,
|
|
},
|
|
}
|
|
}
|
|
|
|
export default async function PostPage({ params }: PostPageProps) {
|
|
const { slug } = await params
|
|
const post = getPostData(slug)
|
|
|
|
if (!post) {
|
|
notFound()
|
|
}
|
|
|
|
const formattedDate = new Date(post.date).toLocaleDateString('en-US', {
|
|
year: 'numeric',
|
|
month: 'long',
|
|
day: 'numeric'
|
|
})
|
|
|
|
const mdxOptions = {
|
|
mdxOptions: {
|
|
remarkPlugins: [remarkGfm],
|
|
rehypePlugins: [
|
|
rehypeHighlight,
|
|
rehypeSlug,
|
|
[rehypeAutolinkHeadings, { behavior: 'wrap' }]
|
|
],
|
|
}
|
|
}
|
|
|
|
return (
|
|
<main className="min-h-screen">
|
|
<BlogHeader />
|
|
|
|
<article className="blog-container py-12">
|
|
{/* Back link */}
|
|
<div className="mb-8">
|
|
<Link
|
|
href="/"
|
|
className="inline-flex items-center text-carbon-600 dark:text-carbon-400 hover:text-carbon-950 dark:hover:text-carbon-200 transition-colors text-sm group"
|
|
>
|
|
<svg
|
|
className="w-4 h-4 mr-2 group-hover:-translate-x-1 transition-transform"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
viewBox="0 0 24 24"
|
|
>
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
|
|
</svg>
|
|
Back to all posts
|
|
</Link>
|
|
</div>
|
|
|
|
{/* Article header */}
|
|
<header className="mb-12 max-w-4xl">
|
|
<div className="flex flex-wrap gap-2 mb-4">
|
|
{post.tags.map((tag) => (
|
|
<span key={tag} className="blog-tag">
|
|
{tag}
|
|
</span>
|
|
))}
|
|
</div>
|
|
|
|
<h1 className="text-h1 font-logo text-carbon-950 dark:text-mulberry-100 mb-6">
|
|
{post.title}
|
|
</h1>
|
|
|
|
<p className="text-xl text-carbon-700 dark:text-carbon-300 leading-relaxed mb-8">
|
|
{post.description}
|
|
</p>
|
|
|
|
<div className="flex items-center justify-between border-b border-carbon-300 dark:border-carbon-800 pb-8">
|
|
<div className="flex items-center space-x-4">
|
|
<div className="w-12 h-12 bg-gradient-to-br from-mulberry-400 to-ocean-500 rounded-full flex items-center justify-center">
|
|
<span className="text-carbon-950 font-semibold">
|
|
{post.author.name.charAt(0)}
|
|
</span>
|
|
</div>
|
|
<div>
|
|
<p className="text-carbon-950 dark:text-carbon-200 font-medium">
|
|
{post.author.name}
|
|
</p>
|
|
{post.author.role && (
|
|
<p className="text-carbon-600 dark:text-carbon-500 text-sm">
|
|
{post.author.role}
|
|
</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="blog-meta text-right">
|
|
<time dateTime={post.date} className="block">
|
|
{formattedDate}
|
|
</time>
|
|
<span className="text-carbon-600 dark:text-carbon-600">
|
|
{post.readingTime} min read
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</header>
|
|
|
|
{/* Article content */}
|
|
<div className="prose prose-lg max-w-none">
|
|
<MDXRemote source={post.content} options={mdxOptions} />
|
|
</div>
|
|
|
|
{/* Article footer */}
|
|
<footer className="mt-16 pt-8 border-t border-carbon-300 dark:border-carbon-800">
|
|
<div className="text-center">
|
|
<h3 className="text-h5 font-logo text-carbon-950 dark:text-carbon-100 mb-4">
|
|
Join the CHORUS Community
|
|
</h3>
|
|
<p className="text-carbon-600 dark:text-carbon-400 mb-6 max-w-2xl mx-auto">
|
|
Stay updated with the latest insights on contextual AI and agent orchestration.
|
|
Join our waitlist to get early access to the CHORUS platform.
|
|
</p>
|
|
<Link
|
|
href="https://chorus.services"
|
|
className="inline-flex items-center px-6 py-3 bg-carbon-900 dark:bg-mulberry-700 hover:bg-carbon-800 dark:hover:bg-mulberry-600 text-white dark:text-mulberry-100 rounded-lg font-medium transition-colors"
|
|
>
|
|
Join Waitlist
|
|
<svg className="w-4 h-4 ml-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17 8l4 4m0 0l-4 4m4-4H3" />
|
|
</svg>
|
|
</Link>
|
|
</div>
|
|
</footer>
|
|
</article>
|
|
|
|
<BlogFooter />
|
|
</main>
|
|
)
|
|
} |