How to Implement Search Functionality in a Static MDX Blog (Next.js App Router)

How to Implement Search Functionality in a Static MDX Blog (Next.js App Router)

|By Malik Saqib

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 flexsearch

Optionally, you can use fuse.js as an alternative, but flexsearch offers better performance for large blogs.

npm install fuse.js  # Alternative option

Step 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-matter

Add 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 dev

Then build your search index:

npm run build-index

Visit 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!

Author

Malik Saqib

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