Skip to main content
Glama
blog-layout.md•16.4 kB
# Blog and Article Layout Patterns ## Overview Common blog and article layout patterns for Next.js applications with MDX content. ## Pattern A: Blog Index with Card Grid ### Characteristics - Grid of article cards - Each card shows: title, date, excerpt, featured image - Pagination or infinite scroll - RSS feed integration - **Common in**: Company blogs, personal blogs, content sites ### Structure ```tsx // app/blog/page.tsx import { getBlogPosts } from '@/lib/blog' import Link from 'next/link' import Image from 'next/image' export default async function BlogPage() { const posts = await getBlogPosts() return ( <div className="mx-auto max-w-7xl px-6 py-24"> <div className="mx-auto max-w-2xl text-center"> <h1 className="text-4xl font-bold tracking-tight text-gray-900 sm:text-6xl"> Blog </h1> <p className="mt-6 text-lg leading-8 text-gray-600"> Thoughts, ideas, and insights from our team. </p> </div> <div className="mx-auto mt-16 grid max-w-2xl auto-rows-fr grid-cols-1 gap-8 sm:mt-20 lg:mx-0 lg:max-w-none lg:grid-cols-3"> {posts.map((post) => ( <article key={post.slug} className="relative isolate flex flex-col justify-end overflow-hidden rounded-2xl bg-gray-900 px-8 pb-8 pt-80 sm:pt-48 lg:pt-80" > <Image src={post.image} alt={post.title} fill className="absolute inset-0 -z-10 h-full w-full object-cover" /> <div className="absolute inset-0 -z-10 bg-gradient-to-t from-gray-900 via-gray-900/40" /> <div className="flex flex-wrap items-center gap-y-1 overflow-hidden text-sm leading-6 text-gray-300"> <time dateTime={post.date} className="mr-8"> {formatDate(post.date)} </time> <div className="-ml-4 flex items-center gap-x-4"> <div className="h-px w-4 bg-gray-600" /> <div className="flex gap-x-2.5"> {post.author.name} </div> </div> </div> <h3 className="mt-3 text-lg font-semibold leading-6 text-white"> <Link href={`/blog/${post.slug}`}> <span className="absolute inset-0" /> {post.title} </Link> </h3> </article> ))} </div> </div> ) } ``` ### Alternative: Simple Card Style ```tsx <div className="mx-auto grid max-w-2xl grid-cols-1 gap-x-8 gap-y-20 lg:mx-0 lg:max-w-none lg:grid-cols-3"> {posts.map((post) => ( <article key={post.slug} className="flex flex-col items-start"> <div className="relative w-full"> <Image src={post.image} alt={post.title} width={800} height={400} className="aspect-[16/9] w-full rounded-2xl bg-gray-100 object-cover sm:aspect-[2/1] lg:aspect-[3/2]" /> <div className="absolute inset-0 rounded-2xl ring-1 ring-inset ring-gray-900/10" /> </div> <div className="max-w-xl"> <div className="mt-8 flex items-center gap-x-4 text-xs"> <time dateTime={post.date} className="text-gray-500"> {formatDate(post.date)} </time> <span className="relative z-10 rounded-full bg-gray-100 px-3 py-1.5 font-medium text-gray-600"> {post.category} </span> </div> <div className="group relative"> <h3 className="mt-3 text-lg font-semibold leading-6 text-gray-900 group-hover:text-gray-600"> <Link href={`/blog/${post.slug}`}> <span className="absolute inset-0" /> {post.title} </Link> </h3> <p className="mt-5 line-clamp-3 text-sm leading-6 text-gray-600"> {post.excerpt} </p> </div> <div className="relative mt-8 flex items-center gap-x-4"> <Image src={post.author.avatar} alt={post.author.name} width={40} height={40} className="h-10 w-10 rounded-full bg-gray-100" /> <div className="text-sm leading-6"> <p className="font-semibold text-gray-900">{post.author.name}</p> <p className="text-gray-600">{post.author.role}</p> </div> </div> </div> </article> ))} </div> ``` ## Pattern B: Article Layout with Sidebar ### Characteristics - Full article content in typography-optimized container - Sidebar with author info, related posts, or table of contents - Previous/next article navigation - Social sharing buttons - **Common in**: Technical blogs, long-form content ### Structure ```tsx // app/blog/[slug]/page.tsx import { getPostBySlug, getAllPosts } from '@/lib/blog' import { MDXRemote } from 'next-mdx-remote/rsc' import Link from 'next/link' export default async function ArticlePage({ params }: { params: { slug: string } }) { const post = await getPostBySlug(params.slug) const allPosts = await getAllPosts() const postIndex = allPosts.findIndex((p) => p.slug === params.slug) const previousPost = allPosts[postIndex + 1] const nextPost = allPosts[postIndex - 1] return ( <div className="mx-auto max-w-7xl px-6 py-24"> <div className="mx-auto grid max-w-2xl grid-cols-1 gap-x-12 gap-y-16 lg:mx-0 lg:max-w-none lg:grid-cols-12"> {/* Main Content */} <article className="lg:col-span-8"> <header> <div className="flex items-center gap-x-4 text-xs"> <time dateTime={post.date} className="text-gray-500"> {formatDate(post.date)} </time> <span className="rounded-full bg-gray-100 px-3 py-1.5 font-medium text-gray-600"> {post.category} </span> </div> <h1 className="mt-4 text-4xl font-bold tracking-tight text-gray-900 sm:text-5xl"> {post.title} </h1> <p className="mt-6 text-xl leading-8 text-gray-600"> {post.excerpt} </p> <div className="mt-10 flex items-center gap-x-6 border-t border-gray-200 pt-6"> <Image src={post.author.avatar} alt={post.author.name} width={48} height={48} className="h-12 w-12 rounded-full bg-gray-100" /> <div> <p className="text-sm font-semibold text-gray-900">{post.author.name}</p> <p className="text-sm text-gray-600">{post.author.role}</p> </div> </div> </header> <div className="prose prose-lg prose-indigo mt-10 max-w-none"> <MDXRemote source={post.content} /> </div> {/* Previous/Next Navigation */} <nav className="mt-16 flex justify-between border-t border-gray-200 pt-8"> {previousPost && ( <Link href={`/blog/${previousPost.slug}`} className="group"> <p className="text-sm text-gray-500">Previous article</p> <p className="mt-1 font-semibold text-gray-900 group-hover:text-indigo-600"> {previousPost.title} </p> </Link> )} {nextPost && ( <Link href={`/blog/${nextPost.slug}`} className="group text-right"> <p className="text-sm text-gray-500">Next article</p> <p className="mt-1 font-semibold text-gray-900 group-hover:text-indigo-600"> {nextPost.title} </p> </Link> )} </nav> </article> {/* Sidebar */} <aside className="lg:col-span-4"> <div className="sticky top-8 space-y-8"> {/* About Author */} <div className="rounded-2xl border border-gray-200 p-6"> <h3 className="text-sm font-semibold text-gray-900">About the author</h3> <div className="mt-4 flex items-center gap-x-4"> <Image src={post.author.avatar} alt={post.author.name} width={48} height={48} className="h-12 w-12 rounded-full" /> <div> <p className="font-semibold">{post.author.name}</p> <p className="text-sm text-gray-600">{post.author.role}</p> </div> </div> <p className="mt-4 text-sm text-gray-600">{post.author.bio}</p> </div> {/* Related Posts */} <div className="rounded-2xl border border-gray-200 p-6"> <h3 className="text-sm font-semibold text-gray-900">Related articles</h3> <ul className="mt-4 space-y-4"> {relatedPosts.map((relatedPost) => ( <li key={relatedPost.slug}> <Link href={`/blog/${relatedPost.slug}`} className="group flex gap-x-4" > <Image src={relatedPost.image} alt={relatedPost.title} width={64} height={64} className="h-16 w-16 flex-none rounded object-cover" /> <div className="flex-auto"> <p className="text-sm font-semibold text-gray-900 group-hover:text-indigo-600"> {relatedPost.title} </p> <p className="mt-1 text-xs text-gray-600"> {formatDate(relatedPost.date)} </p> </div> </Link> </li> ))} </ul> </div> </div> </aside> </div> </div> ) } ``` ## Pattern C: Centered Article Layout (Simple) ### Characteristics - Single column, centered content - Maximum width for optimal readability - No sidebar distractions - Focus on content - **Common in**: Minimal blogs, personal sites ### Structure ```tsx export default async function ArticlePage({ params }: { params: { slug: string } }) { const post = await getPostBySlug(params.slug) return ( <article className="mx-auto max-w-2xl px-6 py-24"> <header className="text-center"> <time dateTime={post.date} className="text-sm text-gray-500"> {formatDate(post.date)} </time> <h1 className="mt-4 text-4xl font-bold tracking-tight text-gray-900 sm:text-5xl"> {post.title} </h1> <p className="mt-6 text-xl leading-8 text-gray-600"> {post.excerpt} </p> </header> {post.image && ( <div className="mt-10"> <Image src={post.image} alt={post.title} width={1200} height={630} className="aspect-video w-full rounded-2xl object-cover" /> </div> )} <div className="prose prose-lg prose-indigo mx-auto mt-10"> <MDXRemote source={post.content} /> </div> <footer className="mt-16 border-t border-gray-200 pt-8"> <div className="flex items-center"> <Image src={post.author.avatar} alt={post.author.name} width={48} height={48} className="h-12 w-12 rounded-full" /> <div className="ml-4"> <p className="font-semibold text-gray-900">{post.author.name}</p> <p className="text-sm text-gray-600">{post.author.role}</p> </div> </div> </footer> </article> ) } ``` ## MDX Content Loading ### Using next-mdx-remote ```bash npm install next-mdx-remote gray-matter ``` ```tsx // lib/blog.ts import fs from 'fs' import path from 'path' import matter from 'gray-matter' const postsDirectory = path.join(process.cwd(), 'content/blog') export async function getPostBySlug(slug: string) { const fullPath = path.join(postsDirectory, `${slug}.mdx`) const fileContents = fs.readFileSync(fullPath, 'utf8') const { data, content } = matter(fileContents) return { slug, content, ...data, } } export async function getAllPosts() { const fileNames = fs.readdirSync(postsDirectory) const posts = fileNames.map((fileName) => { const slug = fileName.replace(/\.mdx$/, '') return getPostBySlug(slug) }) return posts.sort((a, b) => (a.date > b.date ? -1 : 1)) } ``` ## RSS Feed Generation ```tsx // app/blog/rss.xml/route.ts import { getAllPosts } from '@/lib/blog' import RSS from 'rss' export async function GET() { const posts = await getAllPosts() const feed = new RSS({ title: 'My Blog', description: 'Latest articles from my blog', feed_url: 'https://example.com/blog/rss.xml', site_url: 'https://example.com', language: 'en', }) posts.forEach((post) => { feed.item({ title: post.title, description: post.excerpt, url: `https://example.com/blog/${post.slug}`, date: post.date, author: post.author.name, }) }) return new Response(feed.xml(), { headers: { 'Content-Type': 'application/xml', }, }) } ``` ## Typography Configuration ### Tailwind Typography Plugin ```bash npm install @tailwindcss/typography ``` ```js // tailwind.config.js module.exports = { plugins: [ require('@tailwindcss/typography'), ], } ``` ### Usage ```tsx <div className="prose prose-lg prose-indigo mx-auto"> <MDXRemote source={content} /> </div> ``` ### Custom Typography Styles ```css /* globals.css */ .prose { @apply text-gray-700; } .prose h2 { @apply mt-12 text-3xl font-bold tracking-tight; } .prose h3 { @apply mt-8 text-2xl font-semibold; } .prose a { @apply text-indigo-600 no-underline hover:text-indigo-500; } .prose code { @apply rounded bg-gray-100 px-1.5 py-0.5 text-sm font-mono text-gray-900; } ``` ## Common Features ### Reading Time Calculation ```tsx export function calculateReadingTime(content: string): string { const wordsPerMinute = 200 const words = content.split(/\s+/).length const minutes = Math.ceil(words / wordsPerMinute) return `${minutes} min read` } ``` ### Share Buttons ```tsx function ShareButtons({ url, title }: { url: string; title: string }) { return ( <div className="flex gap-x-4"> <a href={`https://twitter.com/intent/tweet?url=${url}&text=${title}`} target="_blank" rel="noopener noreferrer" className="text-gray-400 hover:text-gray-500" > <span className="sr-only">Share on Twitter</span> {/* Twitter icon */} </a> {/* More share buttons */} </div> ) } ``` ### Table of Contents ```tsx function TableOfContents({ headings }: { headings: { id: string; text: string; level: number }[] }) { return ( <nav className="space-y-2"> <h3 className="text-sm font-semibold">On this page</h3> <ul className="space-y-2 text-sm"> {headings.map((heading) => ( <li key={heading.id} style={{ paddingLeft: `${(heading.level - 2) * 1}rem` }}> <a href={`#${heading.id}`} className="text-gray-600 hover:text-gray-900" > {heading.text} </a> </li> ))} </ul> </nav> ) } ``` ## SEO Metadata ```tsx // app/blog/[slug]/page.tsx import { type Metadata } from 'next' export async function generateMetadata({ params }: { params: { slug: string } }): Promise<Metadata> { const post = await getPostBySlug(params.slug) return { title: post.title, description: post.excerpt, openGraph: { title: post.title, description: post.excerpt, type: 'article', publishedTime: post.date, authors: [post.author.name], images: [{ url: post.image }], }, twitter: { card: 'summary_large_image', title: post.title, description: post.excerpt, images: [post.image], }, } } ``` ## Dependencies - `next-mdx-remote` - MDX rendering - `gray-matter` - Frontmatter parsing - `@tailwindcss/typography` - Typography styles - `rss` - RSS feed generation (optional)

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/CaullenOmdahl/Nextjs-React-Tailwind-Assistant'

If you have feedback or need assistance with the MCP directory API, please join our Discord server