Published
- 6 min read
Supercharging Your Next.js Blog with Static Data and Full-Text Search
Introduction
In the world of blogging, speed and ease of navigation are key. One way to dramatically enhance both is by using static data and implementing full-text search functionality.
In this guide, we will walk you through how to implement these features using Next.js, a popular React framework that enables server-side rendering and static site generation.
Our focus will be on how to use Lunr.js for full-text search, and how to leverage next.config.js to generate static data files from your CMS data.
Lunr.js: The Lightweight Search Engine
Firstly, we will need to install Lunr.js, a small, yet powerful search engine designed for use in the browser. Lunr.js provides full-text search in your browser, with no external dependencies, making it an ideal choice for implementing search functionality in your Next.js blog.
Install Lunr.js with the following command:
pnpm i lunr
What makes Lunr.js stand out is its simplicity, extensibility, and portability:
-
Simplicity: Lunr.js is designed to be small and full-featured, providing a great search experience without the need for external, server-side, search services.
-
Extensibility: With Lunr.js, you can add powerful language processors to deliver more accurate results to user queries, or tweak the built-in processors to better fit your content.
-
Portability: Lunr.js has no external dependencies and works both in your browser and on the server with Node.js.
Leveraging next.config.js for Static Data
Now that we have Lunr.js installed, we need to retrieve post data from our CMS and generate an index from the selected fields. This is where next.config.js comes into play. This configuration file allows us to customize various aspects of how Next.js works. For instance, with the trailingSlash configuration, we can specify whether URLs with trailing slashes are redirected to their counterparts without a trailing slash or vice versa. For further information, check the docs.
Here is the full configuration required to create our index file:
// next.config.js file
const path = require('path')
const fs = require('fs')
const lunr = require('lunr')
const createIndex = async () => {
const postsResponse = await fetch(
'https://YOUR-BLOG.com/api/blog-posts?populate=featured_image&populate=categories',
{
headers: {
Authorization: `Bearer ${process.env.STRAPI_TOKEN}`
}
}
)
const posts = (await postsResponse.json()).data
const index = lunr(function () {
// those are the fields that will be indexed
this.field('id')
this.field('meta')
this.field('title', { boost: 10 }) // boost will give more weight to the title
posts.forEach((post) => {
const { attributes } = post
const { meta, title, slug } = attributes
this.add({
id: slug,
meta: meta.toLowerCase(),
title: title.toLowerCase()
})
})
})
const indexFile = path.resolve(__dirname, './public/search-index.json')
const summaryFile = path.resolve(__dirname, './public/search-index-summary.json')
// we will need the post data to persist statically too, so we can use it on the search page
const searchSummary = posts.map((post) => {
const { attributes } = post
const { slug } = attributes
const featured_image = attributes?.featured_image?.data?.attributes
const { name, slug: categorySlug } = attributes?.categories?.data[0]?.attributes
// remove unused data
delete attributes.content
delete attributes.categories
delete attributes.id
return {
...attributes,
featured_image,
category: {
name,
slug: categorySlug
},
id: slug
}
})
fs.writeFileSync(indexFile, JSON.stringify(index))
fs.writeFileSync(summaryFile, JSON.stringify(searchSummary))
}
const nextConfig = async (phase, { defaultConfig }) => {
// a little check to not run this on every save while on dev mode
if (!fs.existsSync('./public/search-index.json')) {
await createIndex()
}
return {
...defaultConfig
// ... your existing config goes here
}
}
module.exports = nextConfig
This configuration fetches blog posts from our CMS, generates a search index using Lunr.js, and saves the index and summary of posts as static files in the public folder. It also cleans up the post data to keep only the necessary fields, reducing the size of the static files and improving the load times.
The createIndex() function is the heart of this configuration. It fetches post data from our CMS, generates a Lunr.js search index based on the id, meta, and title fields of the posts, and saves the index and a summary of the post data as static files.
The nextConfig() function uses the createIndex() function to generate the index and post summary files when they do not exist. This prevents unnecessary index generation during development.
Implementing Full-Text Search
With the static data and search index in place, the final step is to create a search component to allow users to search the blog posts. Here’s how it can be done:
// search.js file
import { useEffect, useState } from 'react'
import lunr from 'lunr'
const Search = () => {
const [index, setIndex] = useState(null)
const [filterResult, setFilterResult] = useState([])
const [posts, setPosts] = useState([])
useEffect(() => {
// load the index and posts data from the public folder
Promise.all([
fetch('/search-index.json').then((response) => response.json()),
fetch('/search-index-summary.json').then((response) => response.json())
]).then(([indexData, postsData]) => {
// load the index
const indexFromServer = lunr.Index.load(indexData)
setIndex(indexFromServer)
setPosts(postsData)
})
}, [])
const onSearch = (event) => {
const { value } = event.target
if (value?.trim() === '') {
setFilterResult([])
return
}
if (index) {
// split the search terms and remove any empty strings
const terms = value.trim().split(' ').filter(Boolean)
// create the query string, notice "~1" means fuzzy search
// "+" means "AND" search of each term
const query = terms.map((term) => `+${term}~1`).join(' ')
// results are returned in the order of most relevant to least relevant
let results = index.search(query)
// map the results to the posts
const fullResults = results.map((result) => posts.find((item) => item.id === result.ref))
setFilterResult(fullResults)
}
}
return (
<div>
<input onChange={onSearch} />
{filterResult?.map((result, index) => (
<div key={index}>
<h3>{result.title}</h3>
<p>{result.meta}</p>
</div>
))}
</div>
)
}
export default Search
This component first fetches the search index and post summaries from the public folder. It then sets up an onSearch function to execute whenever a search query is entered.
This function uses the Lunr.js search index to find the posts that best match the query and display them.
Frequently Asked Questions
What is Lunr.js?
Lunr.js is a small, full-text search library for use in the browser. It indexes JSON documents and provides a simple search interface for retrieving documents that best match text queries. It’s an excellent choice for web applications with all their data already on the client side, allowing the search to be performed locally and without the need for network overhead1.
What are the features of Lunr.js?
Lunr.js provides full-text search support for 14 languages, allows for boosting terms at query time or boosting entire documents at index time, scopes searches to specific fields, and supports fuzzy term matching with wildcards or edit distance1.
How does next.config.js work in Next.js?
next.config.js is a configuration file for Next.js where you can adjust various settings of your Next.js application. For example, with the trailingSlash configuration, you can specify whether URLs with trailing slashes are redirected to their counterparts without a trailing slash or vice versa2.
Conclusion
Implementing full-text search and leveraging static data in your Next.js blog can enhance your user experience, provide faster load times, and improve the overall functionality of your site. The combination of Next.js and Lunr.js makes this process straightforward and efficient, enabling you to supercharge your blog with these powerful features.