Designing for Accessibility
Best practices for creating inclusive digital experiences for everyone.
A clean, minimal blog template with beautiful typography, article layouts, categories, and newsletter signup. Perfect for writers and content creators.
A blog template designed with readability in mind. Beautiful typography, optimized reading experience, and all the features you need to grow your audience and engage readers.
Optimized line height, spacing, and font choices for reading
Organize content with flexible taxonomy system
Built-in signup forms to grow your email list
Engage readers with threaded discussions
Exploring emerging trends and technologies shaping the future of web development.
Best practices for creating inclusive digital experiences for everyone.
Architecture patterns and strategies for large-scale React projects.
Principles and practices for maintainable, readable code.
Get the latest posts delivered straight to your inbox.
Set up types for posts, authors, and categories.
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647// types/blog.ts export interface Post { id: string; slug: string; title: string; excerpt: string; content: string; coverImage: string; publishedAt: Date; updatedAt?: Date; author: Author; category: Category; tags: string[]; readingTime: number; featured?: boolean; } export interface Author { id: string; name: string; bio: string; avatar: string; twitter?: string; website?: string; } export interface Category { id: string; name: string; slug: string; description?: string; color: string; } export interface Comment { id: string; postId: string; author: { name: string; email: string; avatar?: string; }; content: string; createdAt: Date; parentId?: string; replies?: Comment[]; }
Build a beautiful, readable article page with proper typography.
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576// components/ArticleLayout.tsx function ArticleLayout({ post }: { post: Post }) { return ( <article className="max-w-3xl mx-auto px-6 py-12"> {/* Category & Reading Time */} <div className="flex items-center gap-4 mb-6"> <Link href={`/category/${post.category.slug}`} className="px-3 py-1 text-sm font-medium bg-slate-100 dark:bg-slate-800 rounded-full hover:bg-slate-200" > {post.category.name} </Link> <span className="text-sm text-slate-500">{post.readingTime} min read</span> </div> {/* Title */} <h1 className="text-4xl md:text-5xl font-bold text-slate-900 dark:text-white leading-tight mb-6"> {post.title} </h1> {/* Author & Date */} <div className="flex items-center gap-4 mb-8"> <img src={post.author.avatar} alt={post.author.name} className="w-12 h-12 rounded-full" /> <div> <Link href={`/author/${post.author.id}`} className="font-medium hover:underline"> {post.author.name} </Link> <p className="text-sm text-slate-500"> {new Date(post.publishedAt).toLocaleDateString("en-US", { month: "long", day: "numeric", year: "numeric", })} </p> </div> </div> {/* Cover Image */} <div className="aspect-[2/1] rounded-2xl overflow-hidden mb-12"> <img src={post.coverImage} alt={post.title} className="w-full h-full object-cover" /> </div> {/* Content */} <div className="prose prose-lg dark:prose-invert prose-slate max-w-none"> <MDXContent content={post.content} /> </div> {/* Tags */} <div className="flex flex-wrap gap-2 mt-12 pt-8 border-t border-slate-200 dark:border-slate-800"> {post.tags.map(tag => ( <Link key={tag} href={`/tag/${tag}`} className="px-3 py-1 text-sm text-slate-600 dark:text-slate-400 bg-slate-100 dark:bg-slate-800 rounded-full hover:bg-slate-200" > #{tag} </Link> ))} </div> {/* Author Bio */} <AuthorCard author={post.author} /> {/* Comments Section */} <CommentsSection postId={post.id} /> </article> ); }
Create a responsive grid of post cards with featured posts.
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071function PostGrid({ posts, featured }: { posts: Post[]; featured?: Post }) { return ( <div className="space-y-12"> {/* Featured Post */} {featured && ( <Link href={`/blog/${featured.slug}`} className="group block"> <div className="grid md:grid-cols-2 gap-8 items-center"> <div className="aspect-[4/3] rounded-2xl overflow-hidden"> <img src={featured.coverImage} alt={featured.title} className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-500" /> </div> <div> <span className="text-sm font-medium text-slate-500">Featured</span> <h2 className="text-3xl font-bold text-slate-900 dark:text-white mt-2 mb-4 group-hover:text-slate-700 dark:group-hover:text-slate-300"> {featured.title} </h2> <p className="text-slate-600 dark:text-slate-400 mb-4 line-clamp-3"> {featured.excerpt} </p> <div className="flex items-center gap-3"> <img src={featured.author.avatar} alt={featured.author.name} className="w-8 h-8 rounded-full" /> <span className="text-sm font-medium">{featured.author.name}</span> <span className="text-sm text-slate-500">·</span> <span className="text-sm text-slate-500">{featured.readingTime} min read</span> </div> </div> </div> </Link> )} {/* Post Grid */} <div className="grid md:grid-cols-2 lg:grid-cols-3 gap-8"> {posts.map(post => ( <Link key={post.id} href={`/blog/${post.slug}`} className="group"> <div className="aspect-[3/2] rounded-xl overflow-hidden mb-4"> <img src={post.coverImage} alt={post.title} className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-500" /> </div> <span className="text-sm font-medium text-slate-500">{post.category.name}</span> <h3 className="text-xl font-semibold text-slate-900 dark:text-white mt-1 mb-2 group-hover:text-slate-700 dark:group-hover:text-slate-300 line-clamp-2"> {post.title} </h3> <p className="text-slate-600 dark:text-slate-400 text-sm line-clamp-2"> {post.excerpt} </p> <div className="flex items-center gap-2 mt-4"> <img src={post.author.avatar} alt={post.author.name} className="w-6 h-6 rounded-full" /> <span className="text-sm text-slate-600 dark:text-slate-400"> {post.author.name} </span> </div> </Link> ))} </div> </div> ); }
Create a newsletter subscription form with validation.
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768function NewsletterSignup() { const [email, setEmail] = useState(""); const [status, setStatus] = useState<"idle" | "loading" | "success" | "error">("idle"); const handleSubmit = async (e: FormEvent) => { e.preventDefault(); setStatus("loading"); try { await subscribeToNewsletter(email); setStatus("success"); setEmail(""); } catch { setStatus("error"); } }; if (status === "success") { return ( <div className="bg-slate-100 dark:bg-slate-800 rounded-2xl p-8 text-center"> <div className="w-12 h-12 bg-emerald-100 rounded-full flex items-center justify-center mx-auto mb-4"> <svg className="w-6 h-6 text-emerald-600" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" /> </svg> </div> <h3 className="text-xl font-semibold mb-2">You're subscribed!</h3> <p className="text-slate-600 dark:text-slate-400"> Thanks for subscribing. Check your email for confirmation. </p> </div> ); } return ( <div className="bg-slate-100 dark:bg-slate-800 rounded-2xl p-8"> <h3 className="text-2xl font-bold text-slate-900 dark:text-white mb-2"> Subscribe to our newsletter </h3> <p className="text-slate-600 dark:text-slate-400 mb-6"> Get the latest posts delivered straight to your inbox. </p> <form onSubmit={handleSubmit} className="flex gap-3"> <input type="email" value={email} onChange={e => setEmail(e.target.value)} placeholder="Enter your email" required className="flex-1 px-4 py-3 bg-white dark:bg-slate-900 rounded-xl border border-slate-200 dark:border-slate-700 focus:outline-none focus:ring-2 focus:ring-slate-500" /> <button type="submit" disabled={status === "loading"} className="px-6 py-3 bg-slate-900 dark:bg-white text-white dark:text-slate-900 font-medium rounded-xl hover:bg-slate-800 dark:hover:bg-slate-100 disabled:opacity-50" > {status === "loading" ? "..." : "Subscribe"} </button> </form> {status === "error" && ( <p className="mt-3 text-sm text-red-600"> Something went wrong. Please try again. </p> )} </div> ); }
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237"use client"; import { useState } from "react"; import Link from "next/link"; // Types interface Post { id: string; slug: string; title: string; excerpt: string; coverImage: string; category: string; author: { name: string; avatar: string; }; readingTime: number; publishedAt: Date; } // Blog Home Page export default function BlogHome() { const [posts] = useState<Post[]>([]); return ( <div className="min-h-screen bg-white dark:bg-slate-950"> <Header /> <main className="max-w-5xl mx-auto px-6 py-12"> <FeaturedPost post={posts[0]} /> <PostGrid posts={posts.slice(1)} /> <NewsletterSignup /> </main> <Footer /> </div> ); } // Header Component function Header() { return ( <header className="border-b border-slate-200 dark:border-slate-800"> <div className="max-w-5xl mx-auto px-6 h-16 flex items-center justify-between"> <Link href="/" className="text-xl font-bold text-slate-900 dark:text-white"> theblog </Link> <nav className="hidden md:flex items-center gap-8"> {["Articles", "Categories", "About", "Newsletter"].map((item) => ( <Link key={item} href={`/${item.toLowerCase()}`} className="text-sm font-medium text-slate-600 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white transition-colors" > {item} </Link> ))} </nav> <button className="p-2 hover:bg-slate-100 dark:hover:bg-slate-800 rounded-lg"> <SearchIcon className="w-5 h-5 text-slate-600" /> </button> </div> </header> ); } // Featured Post Component function FeaturedPost({ post }: { post?: Post }) { if (!post) return null; return ( <Link href={`/blog/${post.slug}`} className="group block mb-16"> <div className="grid md:grid-cols-2 gap-8 items-center"> <div className="aspect-[4/3] rounded-2xl overflow-hidden"> <img src={post.coverImage} alt={post.title} className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-500" /> </div> <div> <span className="text-sm font-medium text-slate-500">Featured</span> <h1 className="text-3xl font-bold text-slate-900 dark:text-white mt-2 mb-4 group-hover:text-slate-700 dark:group-hover:text-slate-300 transition-colors"> {post.title} </h1> <p className="text-slate-600 dark:text-slate-400 mb-4 line-clamp-3"> {post.excerpt} </p> <AuthorInfo author={post.author} readingTime={post.readingTime} /> </div> </div> </Link> ); } // Post Grid Component function PostGrid({ posts }: { posts: Post[] }) { return ( <section className="mb-16"> <h2 className="text-2xl font-bold text-slate-900 dark:text-white mb-8">Latest Posts</h2> <div className="grid md:grid-cols-2 lg:grid-cols-3 gap-8"> {posts.map(post => ( <Link key={post.id} href={`/blog/${post.slug}`} className="group"> <div className="aspect-[3/2] rounded-xl overflow-hidden mb-4"> <img src={post.coverImage} alt={post.title} className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-500" /> </div> <span className="text-sm font-medium text-slate-500">{post.category}</span> <h3 className="text-xl font-semibold text-slate-900 dark:text-white mt-1 mb-2 group-hover:text-slate-700 dark:group-hover:text-slate-300 line-clamp-2 transition-colors"> {post.title} </h3> <p className="text-slate-600 dark:text-slate-400 text-sm line-clamp-2"> {post.excerpt} </p> <AuthorInfo author={post.author} readingTime={post.readingTime} size="sm" /> </Link> ))} </div> </section> ); } // Newsletter Signup Component function NewsletterSignup() { const [email, setEmail] = useState(""); const [status, setStatus] = useState<"idle" | "loading" | "success">("idle"); const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); setStatus("loading"); // Simulate API call await new Promise(r => setTimeout(r, 1000)); setStatus("success"); }; if (status === "success") { return ( <div className="bg-slate-100 dark:bg-slate-800 rounded-2xl p-8 text-center"> <div className="w-12 h-12 bg-emerald-100 rounded-full flex items-center justify-center mx-auto mb-4"> <CheckIcon className="w-6 h-6 text-emerald-600" /> </div> <h3 className="text-xl font-semibold mb-2">You're subscribed!</h3> <p className="text-slate-600 dark:text-slate-400"> Thanks for subscribing. Check your email for confirmation. </p> </div> ); } return ( <div className="bg-slate-100 dark:bg-slate-800 rounded-2xl p-8"> <div className="max-w-xl mx-auto text-center"> <h3 className="text-2xl font-bold text-slate-900 dark:text-white mb-2"> Subscribe to our newsletter </h3> <p className="text-slate-600 dark:text-slate-400 mb-6"> Get the latest posts delivered straight to your inbox. </p> <form onSubmit={handleSubmit} className="flex gap-3"> <input type="email" value={email} onChange={e => setEmail(e.target.value)} placeholder="Enter your email" required className="flex-1 px-4 py-3 bg-white dark:bg-slate-900 rounded-xl border border-slate-200 dark:border-slate-700 focus:outline-none focus:ring-2 focus:ring-slate-500" /> <button type="submit" disabled={status === "loading"} className="px-6 py-3 bg-slate-900 dark:bg-white text-white dark:text-slate-900 font-medium rounded-xl hover:bg-slate-800 disabled:opacity-50" > {status === "loading" ? "..." : "Subscribe"} </button> </form> </div> </div> ); } // Helper Components function AuthorInfo({ author, readingTime, size = "md" }: { author: { name: string; avatar: string }; readingTime: number; size?: "sm" | "md"; }) { return ( <div className={`flex items-center gap-${size === "sm" ? "2" : "3"} mt-4`}> <img src={author.avatar} alt={author.name} className={`rounded-full ${size === "sm" ? "w-6 h-6" : "w-8 h-8"}`} /> <span className={`font-medium ${size === "sm" ? "text-sm" : ""}`}>{author.name}</span> <span className="text-slate-500">·</span> <span className={`text-slate-500 ${size === "sm" ? "text-sm" : ""}`}>{readingTime} min read</span> </div> ); } function Footer() { return ( <footer className="border-t border-slate-200 dark:border-slate-800 mt-16"> <div className="max-w-5xl mx-auto px-6 py-12"> <div className="flex flex-col md:flex-row justify-between items-center gap-6"> <span className="text-xl font-bold">theblog</span> <div className="flex gap-6"> {["Twitter", "GitHub", "RSS"].map(social => ( <a key={social} href="#" className="text-sm text-slate-500 hover:text-slate-900 dark:hover:text-white"> {social} </a> ))} </div> <p className="text-sm text-slate-500">© 2024 All rights reserved.</p> </div> </div> </footer> ); } function SearchIcon({ className }: { className?: string }) { return ( <svg className={className} fill="none" viewBox="0 0 24 24" stroke="currentColor"> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" /> </svg> ); } function CheckIcon({ className }: { className?: string }) { return ( <svg className={className} fill="none" viewBox="0 0 24 24" stroke="currentColor"> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" /> </svg> ); }
Customize fonts and reading experience.
// tailwind.config.ts
module.exports = {
theme: {
extend: {
typography: {
DEFAULT: {
css: {
maxWidth: '65ch',
lineHeight: '1.75',
'h1, h2, h3': {
fontWeight: '700',
letterSpacing: '-0.025em',
},
a: {
color: '#3b82f6',
textDecoration: 'underline',
'&:hover': {
color: '#2563eb',
},
},
},
},
},
},
},
plugins: [require('@tailwindcss/typography')],
};Define colors for different content categories.
const categoryColors: Record<string, string> = {
technology: "bg-blue-100 text-blue-700",
design: "bg-pink-100 text-pink-700",
business: "bg-emerald-100 text-emerald-700",
lifestyle: "bg-purple-100 text-purple-700",
tutorial: "bg-amber-100 text-amber-700",
};
// Usage
<span className={`px-3 py-1 text-sm rounded-full ${categoryColors[category]}`}>
{category}
</span>Add social sharing buttons to posts.
const socialLinks = [
{
name: "Twitter",
getUrl: (post: Post) =>
`https://twitter.com/intent/tweet?text=${encodeURIComponent(post.title)}&url=${encodeURIComponent(window.location.href)}`,
icon: TwitterIcon,
},
{
name: "LinkedIn",
getUrl: (post: Post) =>
`https://linkedin.com/shareArticle?mini=true&url=${encodeURIComponent(window.location.href)}&title=${encodeURIComponent(post.title)}`,
icon: LinkedInIcon,
},
{
name: "Copy Link",
onClick: () => navigator.clipboard.writeText(window.location.href),
icon: LinkIcon,
},
];Generate an RSS feed for your blog.
// app/feed.xml/route.ts
import { getPosts } from "@/lib/posts";
export async function GET() {
const posts = await getPosts();
const feed = `<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
<channel>
<title>My Blog</title>
<link>https://myblog.com</link>
<description>My blog description</description>
${posts.map(post => `
<item>
<title>${post.title}</title>
<link>https://myblog.com/blog/${post.slug}</link>
<description>${post.excerpt}</description>
<pubDate>${new Date(post.publishedAt).toUTCString()}</pubDate>
</item>
`).join("")}
</channel>
</rss>`;
return new Response(feed, {
headers: {
"Content-Type": "application/xml",
},
});
}Copy this template and start sharing your stories. Focus on content while we handle the design.