2025-07-21 by Remi Kristelijn
Now that we have a solid foundation for our Next.js blog, let's explore the next steps to make it even more powerful and user-friendly. In this post, I'll discuss several enhancements that will take your blog to the next level.
Our blog currently uses basic image handling with Next.js Image component and unoptimized images for Cloudflare deployment.
1// Using Cloudflare Images for optimization 2import Image from 'next/image'; 3 4export default function OptimizedImage({ src, alt, ...props }) { 5 return ( 6 <Image 7 src={`https://imagedelivery.net/your-account/${src}/w=800`} 8 alt={alt} 9 width={800} 10 height={600} 11 {...props} 12 /> 13 ); 14}
Pros:
Cons:
1// next.config.ts 2const nextConfig = { 3 images: { 4 loader: 'custom', 5 loaderFile: './src/lib/image-loader.ts', 6 }, 7}; 8 9// src/lib/image-loader.ts 10export default function imageLoader({ src, width, quality }) { 11 return `https://your-cdn.com/${src}?w=${width}&q=${quality || 75}`; 12}
Pros:
Cons:
1// Pre-optimize images at build time 2import sharp from 'sharp'; 3import fs from 'fs'; 4import path from 'path'; 5 6export async function optimizeImages() { 7 const imagesDir = path.join(process.cwd(), 'src/content/images'); 8 const outputDir = path.join(process.cwd(), 'public/optimized'); 9 10 // Process all images in the content directory 11 const files = fs.readdirSync(imagesDir); 12 13 for (const file of files) { 14 if (file.match(/\.(jpg|jpeg|png|webp)$/i)) { 15 await sharp(path.join(imagesDir, file)) 16 .resize(800, 600, { fit: 'inside' }) 17 .webp({ quality: 80 }) 18 .toFile(path.join(outputDir, `${file}.webp`)); 19 } 20 } 21}
Pros:
Cons:
Start with Option C for simplicity, then migrate to Option A (Cloudflare Images) as your blog grows.
1// Install: npm install fuse.js 2import Fuse from 'fuse.js'; 3import { useState, useMemo } from 'react'; 4 5const fuseOptions = { 6 keys: ['title', 'excerpt', 'content'], 7 threshold: 0.3, 8 includeScore: true, 9}; 10 11export default function SearchComponent({ posts }) { 12 const [searchTerm, setSearchTerm] = useState(''); 13 14 const fuse = useMemo(() => new Fuse(posts, fuseOptions), [posts]); 15 const searchResults = useMemo(() => { 16 if (!searchTerm) return posts; 17 return fuse.search(searchTerm).map(result => result.item); 18 }, [searchTerm, fuse, posts]); 19 20 return ( 21 <div> 22 <input 23 type="text" 24 value={searchTerm} 25 onChange={(e) => setSearchTerm(e.target.value)} 26 placeholder="Search posts..." 27 /> 28 <div> 29 {searchResults.map(post => ( 30 <PostCard key={post.id} post={post} /> 31 ))} 32 </div> 33 </div> 34 ); 35}
Pros:
Cons:
1// Install: npm install algoliasearch 2import algoliasearch from 'algoliasearch'; 3 4const client = algoliasearch('YOUR_APP_ID', 'YOUR_SEARCH_KEY'); 5const index = client.initIndex('posts'); 6 7export async function searchPosts(query: string) { 8 const { hits } = await index.search(query, { 9 attributesToRetrieve: ['title', 'excerpt', 'slug', 'date'], 10 hitsPerPage: 10, 11 }); 12 13 return hits; 14}
Pros:
Cons:
1// Using SQLite with FTS5 for full-text search 2import Database from 'better-sqlite3'; 3 4const db = new Database('blog.db'); 5 6// Create FTS5 virtual table 7db.exec(` 8 CREATE VIRTUAL TABLE IF NOT EXISTS posts_fts USING fts5( 9 title, excerpt, content, slug 10 ); 11`); 12 13export function searchPosts(query: string) { 14 const stmt = db.prepare(` 15 SELECT * FROM posts_fts 16 WHERE posts_fts MATCH ? 17 ORDER BY rank 18 `); 19 20 return stmt.all(query); 21}
Pros:
Cons:
Start with Option A (Fuse.js) for simplicity, then upgrade to Option B (Algolia) when you need more advanced features.
Create a content management system for dynamic pages:
1// src/lib/content.ts 2import fs from 'fs'; 3import path from 'path'; 4import matter from 'gray-matter'; 5 6const CONTENT_DIR = path.join(process.cwd(), 'src/content'); 7 8export interface PageContent { 9 title: string; 10 content: string; 11 lastModified: string; 12} 13 14export function getPageContent(pageName: string): PageContent | null { 15 const filePath = path.join(CONTENT_DIR, `${pageName}.mdx`); 16 17 if (!fs.existsSync(filePath)) { 18 return null; 19 } 20 21 const fileContent = fs.readFileSync(filePath, 'utf8'); 22 const { data, content } = matter(fileContent); 23 const stats = fs.statSync(filePath); 24 25 return { 26 title: data.title || pageName, 27 content, 28 lastModified: stats.mtime.toISOString(), 29 }; 30}
1// src/app/page.tsx 2import { getPageContent } from '@/lib/content'; 3import { notFound } from 'next/navigation'; 4 5export default function Home() { 6 const homeContent = getPageContent('home'); 7 8 if (!homeContent) { 9 notFound(); 10 } 11 12 return ( 13 <Box sx={{ minHeight: '100vh', display: 'flex', flexDirection: 'column' }}> 14 <Header title={homeContent.title} showBlogPostsButton={true} /> 15 <Container maxWidth="md" sx={{ flex: 1, py: 4 }}> 16 <ReactMarkdown>{homeContent.content}</ReactMarkdown> 17 </Container> 18 <Footer /> 19 </Box> 20 ); 21}
1// src/components/Footer.tsx 2import { getPageContent } from '@/lib/content'; 3 4export default function Footer() { 5 const footerContent = getPageContent('footer'); 6 7 return ( 8 <Box component="footer" sx={{ py: 3, px: 2, mt: 'auto' }}> 9 <Container maxWidth="sm"> 10 {footerContent ? ( 11 <ReactMarkdown>{footerContent.content}</ReactMarkdown> 12 ) : ( 13 <Typography variant="body1" align="center"> 14 Built with Next.js and Material-UI 15 </Typography> 16 )} 17 </Container> 18 </Box> 19 ); 20}
src/content/
├── posts/
│ └── ... (blog posts)
├── home.mdx
├── footer.mdx
└── about.mdx
1// Ensure proper heading hierarchy 2export default function PostContent({ post }: PostContentProps) { 3 return ( 4 <article> 5 <header> 6 <h1>{post.title}</h1> 7 <time dateTime={post.date}> 8 {new Date(post.date).toLocaleDateString()} 9 </time> 10 </header> 11 <main> 12 <ReactMarkdown>{post.content}</ReactMarkdown> 13 </main> 14 </article> 15 ); 16}
1// Ensure all interactive elements are keyboard accessible 2export default function Navigation({ title, showHome, showBack }: NavigationProps) { 3 return ( 4 <AppBar position="static" color="default" elevation={1}> 5 <Toolbar> 6 {showHome && ( 7 <Button 8 component={Link} 9 href="/" 10 startIcon={<HomeIcon />} 11 aria-label="Go to home page" 12 > 13 Home 14 </Button> 15 )} 16 {/* ... other navigation items */} 17 </Toolbar> 18 </AppBar> 19 ); 20}
1// src/lib/theme.ts 2export const theme = createTheme({ 3 palette: { 4 primary: { 5 main: '#1976d2', // Ensure sufficient contrast 6 }, 7 text: { 8 primary: '#000000', // High contrast for readability 9 secondary: '#666666', // Meets AA standards 10 }, 11 }, 12 components: { 13 MuiButton: { 14 styleOverrides: { 15 root: { 16 // Ensure button text meets contrast requirements 17 color: '#ffffff', 18 backgroundColor: '#1976d2', 19 }, 20 }, 21 }, 22 }, 23});
1// Add proper ARIA labels and descriptions 2export default function SearchComponent({ posts }) { 3 return ( 4 <div> 5 <label htmlFor="search-input" className="sr-only"> 6 Search blog posts 7 </label> 8 <input 9 id="search-input" 10 type="text" 11 aria-describedby="search-help" 12 placeholder="Search posts..." 13 /> 14 <div id="search-help" className="sr-only"> 15 Type to search through blog posts by title, excerpt, or content 16 </div> 17 </div> 18 ); 19}
1// Ensure proper focus management 2export default function PostCard({ post }: PostCardProps) { 3 return ( 4 <Card 5 component={Link} 6 href={`/posts/${post.slug}`} 7 tabIndex={0} 8 onKeyDown={(e) => { 9 if (e.key === 'Enter' || e.key === ' ') { 10 e.preventDefault(); 11 window.location.href = `/posts/${post.slug}`; 12 } 13 }} 14 > 15 {/* Card content */} 16 </Card> 17 ); 18}
These enhancements will transform your blog from a basic content platform into a professional, feature-rich website. Start with the foundation improvements and gradually add more advanced features as your needs grow.