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.
// Using Cloudflare Images for optimization import Image from 'next/image'; export default function OptimizedImage({ src, alt, ...props }) { return ( <Image src={`https://imagedelivery.net/your-account/${src}/w=800`} alt={alt} width={800} height={600} {...props} /> ); }
Pros:
Cons:
// next.config.ts const nextConfig = { images: { loader: 'custom', loaderFile: './src/lib/image-loader.ts', }, }; // src/lib/image-loader.ts export default function imageLoader({ src, width, quality }) { return `https://your-cdn.com/${src}?w=${width}&q=${quality || 75}`; }
Pros:
Cons:
// Pre-optimize images at build time import sharp from 'sharp'; import fs from 'fs'; import path from 'path'; export async function optimizeImages() { const imagesDir = path.join(process.cwd(), 'src/content/images'); const outputDir = path.join(process.cwd(), 'public/optimized'); // Process all images in the content directory const files = fs.readdirSync(imagesDir); for (const file of files) { if (file.match(/\.(jpg|jpeg|png|webp)$/i)) { await sharp(path.join(imagesDir, file)) .resize(800, 600, { fit: 'inside' }) .webp({ quality: 80 }) .toFile(path.join(outputDir, `${file}.webp`)); } } }
Pros:
Cons:
Start with Option C for simplicity, then migrate to Option A (Cloudflare Images) as your blog grows.
// Install: npm install fuse.js import Fuse from 'fuse.js'; import { useState, useMemo } from 'react'; const fuseOptions = { keys: ['title', 'excerpt', 'content'], threshold: 0.3, includeScore: true, }; export default function SearchComponent({ posts }) { const [searchTerm, setSearchTerm] = useState(''); const fuse = useMemo(() => new Fuse(posts, fuseOptions), [posts]); const searchResults = useMemo(() => { if (!searchTerm) return posts; return fuse.search(searchTerm).map(result => result.item); }, [searchTerm, fuse, posts]); return ( <div> <input type="text" value={searchTerm} onChange={(e) => setSearchTerm(e.target.value)} placeholder="Search posts..." /> <div> {searchResults.map(post => ( <PostCard key={post.id} post={post} /> ))} </div> </div> ); }
Pros:
Cons:
// Install: npm install algoliasearch import algoliasearch from 'algoliasearch'; const client = algoliasearch('YOUR_APP_ID', 'YOUR_SEARCH_KEY'); const index = client.initIndex('posts'); export async function searchPosts(query: string) { const { hits } = await index.search(query, { attributesToRetrieve: ['title', 'excerpt', 'slug', 'date'], hitsPerPage: 10, }); return hits; }
Pros:
Cons:
// Using SQLite with FTS5 for full-text search import Database from 'better-sqlite3'; const db = new Database('blog.db'); // Create FTS5 virtual table db.exec(` CREATE VIRTUAL TABLE IF NOT EXISTS posts_fts USING fts5( title, excerpt, content, slug ); `); export function searchPosts(query: string) { const stmt = db.prepare(` SELECT * FROM posts_fts WHERE posts_fts MATCH ? ORDER BY rank `); return stmt.all(query); }
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:
// src/lib/content.ts import fs from 'fs'; import path from 'path'; import matter from 'gray-matter'; const CONTENT_DIR = path.join(process.cwd(), 'src/content'); export interface PageContent { title: string; content: string; lastModified: string; } export function getPageContent(pageName: string): PageContent | null { const filePath = path.join(CONTENT_DIR, `${pageName}.mdx`); if (!fs.existsSync(filePath)) { return null; } const fileContent = fs.readFileSync(filePath, 'utf8'); const { data, content } = matter(fileContent); const stats = fs.statSync(filePath); return { title: data.title || pageName, content, lastModified: stats.mtime.toISOString(), }; }
// src/app/page.tsx import { getPageContent } from '@/lib/content'; import { notFound } from 'next/navigation'; export default function Home() { const homeContent = getPageContent('home'); if (!homeContent) { notFound(); } return ( <Box sx={{ minHeight: '100vh', display: 'flex', flexDirection: 'column' }}> <Header title={homeContent.title} showBlogPostsButton={true} /> <Container maxWidth="md" sx={{ flex: 1, py: 4 }}> <ReactMarkdown>{homeContent.content}</ReactMarkdown> </Container> <Footer /> </Box> ); }
// src/components/Footer.tsx import { getPageContent } from '@/lib/content'; export default function Footer() { const footerContent = getPageContent('footer'); return ( <Box component="footer" sx={{ py: 3, px: 2, mt: 'auto' }}> <Container maxWidth="sm"> {footerContent ? ( <ReactMarkdown>{footerContent.content}</ReactMarkdown> ) : ( <Typography variant="body1" align="center"> Built with Next.js and Material-UI </Typography> )} </Container> </Box> ); }
src/content/ ├── posts/ │ └── ... (blog posts) ├── home.mdx ├── footer.mdx └── about.mdx
// Ensure proper heading hierarchy export default function PostContent({ post }: PostContentProps) { return ( <article> <header> <h1>{post.title}</h1> <time dateTime={post.date}> {new Date(post.date).toLocaleDateString()} </time> </header> <main> <ReactMarkdown>{post.content}</ReactMarkdown> </main> </article> ); }
// Ensure all interactive elements are keyboard accessible export default function Navigation({ title, showHome, showBack }: NavigationProps) { return ( <AppBar position="static" color="default" elevation={1}> <Toolbar> {showHome && ( <Button component={Link} href="/" startIcon={<HomeIcon />} aria-label="Go to home page" > Home </Button> )} {/* ... other navigation items */} </Toolbar> </AppBar> ); }
// src/lib/theme.ts export const theme = createTheme({ palette: { primary: { main: '#1976d2', // Ensure sufficient contrast }, text: { primary: '#000000', // High contrast for readability secondary: '#666666', // Meets AA standards }, }, components: { MuiButton: { styleOverrides: { root: { // Ensure button text meets contrast requirements color: '#ffffff', backgroundColor: '#1976d2', }, }, }, }, });
// Add proper ARIA labels and descriptions export default function SearchComponent({ posts }) { return ( <div> <label htmlFor="search-input" className="sr-only"> Search blog posts </label> <input id="search-input" type="text" aria-describedby="search-help" placeholder="Search posts..." /> <div id="search-help" className="sr-only"> Type to search through blog posts by title, excerpt, or content </div> </div> ); }
// Ensure proper focus management export default function PostCard({ post }: PostCardProps) { return ( <Card component={Link} href={`/posts/${post.slug}`} tabIndex={0} onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); window.location.href = `/posts/${post.slug}`; } }} > {/* Card content */} </Card> ); }
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.