
How to Implement Search Functionality in a Static MDX Blog (Next.js App Router)
Introduction
If you're running a static blog powered by MDX and Next.js App Router, you might think adding search functionality to your MDX blog is complicated. The good news? It's simpler than you think. In this guide, I'll walk you through implementing search in MDX blogs step by step so your readers can easily find the content they're looking for.
By the end of this tutorial, you'll have a fully functional search feature that indexes your MDX blog posts and delivers instant results to your visitors.
Why Search Matters for Your MDX Blog
Before diving into code, let's understand why implementing search in static blogs matters:
- Better user experience: Readers find relevant posts without endless scrolling
- Increased engagement: Search encourages visitors to explore more content
- SEO benefits: Reduced bounce rates and increased time on site signal quality to search engines
- No backend required: Keep your static blog truly static with client-side search
Prerequisites
To follow this guide on how to add search to an MDX blog, you'll need:
- A Next.js project with App Router configured
- MDX blog posts already set up in your project
- Basic knowledge of React and Next.js
- Node.js and npm installed
If you don't have an MDX blog yet, check the Next.js documentation to set one up first.
Step 1: Install Required Dependencies
Start by installing the search library we'll use. For this tutorial, we're using flexsearch, which is lightweight and perfect for static content indexing.
npm install flexsearchOptionally, you can use fuse.js as an alternative, but flexsearch offers better performance for large blogs.
npm install fuse.js # Alternative optionStep 2: Create a Blog Post Metadata Structure
To enable search functionality in MDX files, you need consistent metadata in your MDX posts. Update your blog posts with frontmatter that includes searchable fields.
Create or update your MDX files with frontmatter like this:
---
title: Getting Started with Next.js
slug: getting-started-nextjs
date: 2024-01-15
description: Learn the basics of Next.js and build your first application
category: Next.js
---
# Getting Started with Next.js
Your blog content here...The slug, title, description, and category fields are crucial for building search indexes for static blogs.
Step 3: Build the Search Index Generation Script
Create a script that generates a searchable index from your MDX files. This is the core of implementing search functionality in static MDX blogs.
Create a new file: scripts/build-search-index.js
const fs = require('fs');
const path = require('path');
const matter = require('gray-matter');
// Make sure to install gray-matter: npm install gray-matter
const blogDir = path.join(process.cwd(), 'content/blog');
const outputFile = path.join(process.cwd(), 'public/search-index.json');
function extractTextContent(mdxContent) {
// Remove MDX/JSX syntax
let text = mdxContent
.replace(/<[^>]*>/g, '') // Remove JSX tags
.replace(/```[\s\S]*?```/g, '') // Remove code blocks
.replace(/`[^`]*`/g, ''); // Remove inline code
return text.substring(0, 500); // Limit to first 500 characters
}
function buildSearchIndex() {
const posts = [];
const files = fs.readdirSync(blogDir).filter(file => file.endsWith('.mdx'));
files.forEach(file => {
const filePath = path.join(blogDir, file);
const fileContent = fs.readFileSync(filePath, 'utf-8');
const { data, content } = matter(fileContent);
const searchablePost = {
id: data.slug,
title: data.title,
description: data.description,
slug: data.slug,
category: data.category || 'Uncategorized',
content: extractTextContent(content),
date: data.date,
};
posts.push(searchablePost);
});
// Write the index file
fs.writeFileSync(outputFile, JSON.stringify(posts, null, 2));
console.log(`✅ Search index built with ${posts.length} posts`);
}
buildSearchIndex();Install the required dependency:
npm install gray-matterAdd this script to your package.json:
{
"scripts": {
"build-index": "node scripts/build-search-index.js",
"build": "npm run build-index && next build"
}
}This ensures your search index generates before each build.
Step 4: Create a Search Hook
Now let's create a custom React hook that handles the search logic. Create hooks/useSearch.js:
'use client';
import { useState, useEffect } from 'react';
import FlexSearch from 'flexsearch';
export function useSearch() {
const [index, setIndex] = useState(null);
const [posts, setPosts] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
async function initializeSearch() {
try {
// Fetch the pre-built search index
const response = await fetch('/search-index.json');
const allPosts = await response.json();
setPosts(allPosts);
// Create a FlexSearch index
const flexIndex = new FlexSearch.Document({
tokenize: 'forward',
fields: {
title: { boost: 10 },
description: { boost: 5 },
content: {},
category: { boost: 3 },
},
});
// Add all posts to the index
allPosts.forEach((post) => {
flexIndex.add(post.id, post);
});
setIndex(flexIndex);
} catch (error) {
console.error('Failed to initialize search:', error);
} finally {
setLoading(false);
}
}
initializeSearch();
}, []);
const search = (query) => {
if (!index || !query.trim()) {
return [];
}
// Search across all fields
const results = index.search(query, {
limit: 10,
enrich: true,
});
// Flatten results from multiple fields
const uniqueResults = {};
results.forEach((result) => {
result.result.forEach((id) => {
uniqueResults[id] = posts.find((post) => post.id === id);
});
});
return Object.values(uniqueResults);
};
return { search, loading, posts };
}This hook initializes search functionality in your MDX blog and provides a search function for querying.
Step 5: Build the Search Component
Create a reusable search component: components/BlogSearch.jsx
'use client';
import { useState, useEffect } from 'react';
import Link from 'next/link';
import { useSearch } from '@/hooks/useSearch';
export function BlogSearch() {
const [query, setQuery] = useState('');
const [results, setResults] = useState([]);
const [showResults, setShowResults] = useState(false);
const { search, loading } = useSearch();
useEffect(() => {
if (query.trim()) {
const searchResults = search(query);
setResults(searchResults);
setShowResults(true);
} else {
setResults([]);
setShowResults(false);
}
}, [query, search]);
return (
<div className="relative w-full max-w-md">
<input
type="text"
placeholder="Search blog posts..."
value={query}
onChange={(e) => setQuery(e.target.value)}
onFocus={() => query && setShowResults(true)}
onBlur={() => setTimeout(() => setShowResults(false), 200)}
className="w-full px-4 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
disabled={loading}
/>
{showResults && results.length > 0 && (
<div className="absolute top-full left-0 right-0 mt-2 bg-white border rounded-lg shadow-lg z-50">
{results.map((post) => (
<Link
key={post.id}
href={`/blog/${post.slug}`}
className="block px-4 py-3 hover:bg-gray-100 border-b last:border-b-0"
onClick={() => {
setQuery('');
setShowResults(false);
}}
>
<h3 className="font-semibold text-gray-900">{post.title}</h3>
<p className="text-sm text-gray-600">{post.description}</p>
</Link>
))}
</div>
)}
{showResults && query && results.length === 0 && !loading && (
<div className="absolute top-full left-0 right-0 mt-2 bg-white border rounded-lg shadow-lg p-4">
<p className="text-gray-500">No posts found for "{query}"</p>
</div>
)}
</div>
);
}This component provides the user interface for searching your MDX blog.
Step 6: Add the Search Component to Your Layout
Now integrate the search component into your blog layout. Update your app/layout.jsx:
import { BlogSearch } from '@/components/BlogSearch';
export const metadata = {
title: 'My Blog',
description: 'A blog about web development',
};
export default function RootLayout({ children }) {
return (
<html lang="en">
<body>
<header className="bg-white shadow-sm">
<div className="max-w-4xl mx-auto px-4 py-4 flex items-center justify-between">
<h1 className="text-2xl font-bold">My Blog</h1>
<BlogSearch />
</div>
</header>
<main className="max-w-4xl mx-auto px-4 py-8">
{children}
</main>
</body>
</html>
);
}Step 7: Test Your Search Implementation
Run your development server:
npm run devThen build your search index:
npm run build-indexVisit your blog and test the search functionality. Try searching for keywords from your blog posts. You should see instant results appearing as you type.
Performance Optimization Tips
To keep your MDX blog search running smoothly:
Limit indexed content: Only index essential fields (title, description, category) to reduce file size Cache the index: The search index loads once and stays in memory Debounce search queries: Add a small delay before searching to reduce processing Use production-ready FlexSearch: Check for the latest version to get performance improvements
Here's a debounced search example:
useEffect(() => {
const timer = setTimeout(() => {
if (query.trim()) {
const results = search(query);
setResults(results);
}
}, 300);
return () => clearTimeout(timer);
}, [query, search]);Troubleshooting Common Issues
Search index not found: Make sure you ran npm run build-index before starting your dev server
Search returns no results: Verify your MDX files have the correct frontmatter with slug, title, and description fields
Search is slow: Try reducing the content field size or limiting the number of indexed posts
'use client' error: Ensure both the hook and component use the 'use client' directive for client-side rendering
SEO Best Practices for Your Blog
Implementing search functionality in your MDX blog also improves SEO. Here's why:
- Reduced bounce rates signal quality to search engines
- Longer session duration improves rankings
- Internal search behavior helps Google understand your content structure
- Schema markup for blog posts improves discoverability
Add this to your blog post pages for better SEO:
export const metadata = {
title: 'Your Blog Post Title | My Blog',
description: 'Your engaging blog post description here',
openGraph: {
type: 'article',
title: 'Your Blog Post Title',
description: 'Your engaging description',
url: 'https://yourdomain.com/blog/your-post',
},
};Conclusion
You now have a fully functional search feature for your Next.js MDX blog. This implementation is lightweight, requires no backend infrastructure, and delivers instant results to your readers.
The beauty of search in static MDX blogs is its simplicity and performance. Your blog remains truly static while providing a dynamic, modern user experience.
Start with this foundation and customize it to match your design and specific needs. Happy blogging!

Malik Saqib
I craft short, practical AI & web dev articles. Follow me on LinkedIn.