Next.js 13应用生成读取本地数据静态页面并实现搜索过滤
解决Next.js App Router静态博客的搜索过滤问题
针对你的场景(静态站点部署在S3+CloudFront,无法使用SSR,需要搜索过滤功能),有两种可行方案,都完全适配静态站点需求:
方案一:构建时预生成文章元数据JSON(推荐,适合仅2篇文章的场景)
核心思路是在构建阶段把所有文章的元数据(标题、标签、内容等)提前生成一个静态JSON文件,客户端直接读取这个文件做搜索过滤,完全避开客户端无法访问fs的问题。
步骤:
- 编写预构建脚本:创建一个Node.js脚本,遍历本地
public/posts下的所有MD文件,用你现有的逻辑解析内容,生成包含所有文章信息的JSON文件:
// scripts/generate-posts-meta.js const fs = require('fs'); const path = require('path'); const matter = require('gray-matter'); const remark = require('remark'); const html = require('remark-html'); async function generatePostsMeta() { const postsDir = path.join(process.cwd(), 'public/posts'); const postFiles = fs.readdirSync(postsDir).filter(file => file.endsWith('.md')); const postsMeta = await Promise.all( postFiles.map(async file => { const id = file.replace('.md', ''); const fullPath = path.join(postsDir, file); const fileContents = fs.readFileSync(fullPath, 'utf8'); const matterResult = matter(fileContents); const processedContent = await remark().use(html).process(matterResult.content); const contentHtml = processedContent.toString(); return { id, title: matterResult.data.title, date: matterResult.data.date, tags: matterResult.data.tags, contentHtml }; }) ); fs.writeFileSync( path.join(process.cwd(), 'public/posts-meta.json'), JSON.stringify(postsMeta) ); } generatePostsMeta();
- 配置预构建命令:在
package.json里添加prebuild脚本,确保构建前自动生成JSON文件:
"scripts": { "prebuild": "node scripts/generate-posts-meta.js", "build": "next build", "dev": "next dev", "start": "next start" }
- 客户端实现搜索过滤:创建
use client组件,通过fetch读取生成的JSON文件,实现搜索逻辑:
'use client'; import { useState, useEffect } from 'react'; export default function SearchFilter() { const [posts, setPosts] = useState([]); const [searchQuery, setSearchQuery] = useState(''); const [filteredPosts, setFilteredPosts] = useState([]); // 加载所有文章元数据 useEffect(() => { async function loadPosts() { const res = await fetch('/posts-meta.json'); const data = await res.json(); setPosts(data); setFilteredPosts(data); } loadPosts(); }, []); // 根据搜索词过滤文章 useEffect(() => { const lowerQuery = searchQuery.toLowerCase(); setFilteredPosts( posts.filter(post => post.title.toLowerCase().includes(lowerQuery) || post.tags.some(tag => tag.toLowerCase().includes(lowerQuery)) || post.contentHtml.toLowerCase().includes(lowerQuery) ) ); }, [searchQuery, posts]); return ( <div className="search-container"> <input type="text" placeholder="搜索标题、标签或内容..." value={searchQuery} onChange={(e) => setSearchQuery(e.target.value)} className="search-input" /> <div className="post-list"> {filteredPosts.map(post => ( <div key={post.id} className="post-item"> <h3>{post.title}</h3> <p className="post-date">{post.date}</p> <div className="post-tags"> {post.tags.map(tag => ( <span key={tag} className="tag">{tag}</span> ))} </div> </div> ))} </div> </div> ); }
方案二:客户端直接Fetch并解析MD文件
如果不想额外生成JSON文件,可以把MD文件作为静态资源部署,客户端通过URL fetch文件内容后,用客户端兼容的库解析MD内容。
步骤:
确保MD文件可访问:所有MD文件放在
public/posts目录下,这样可以通过/posts/[id].md的URL访问。客户端实现解析与搜索:在
use client组件中,fetch MD文件并使用客户端版的gray-matter和remark解析:
'use client'; import { useState, useEffect } from 'react'; import matter from 'gray-matter'; import { remark } from 'remark'; import html from 'remark-html'; // 硬编码文章ID(仅2篇的场景下简单直接) const postIds = ['post1', 'post2']; export default function SearchFilter() { const [posts, setPosts] = useState([]); const [searchQuery, setSearchQuery] = useState(''); const [filteredPosts, setFilteredPosts] = useState([]); useEffect(() => { async function fetchPosts() { const postsData = await Promise.all( postIds.map(async id => { const res = await fetch(`/posts/${id}.md`); const mdContent = await res.text(); const matterResult = matter(mdContent); const processedContent = await remark().use(html).process(matterResult.content); const contentHtml = processedContent.toString(); return { id, title: matterResult.data.title, date: matterResult.data.date, tags: matterResult.data.tags, contentHtml }; }) ); setPosts(postsData); setFilteredPosts(postsData); } fetchPosts(); }, []); // 搜索过滤逻辑同方案一 useEffect(() => { const lowerQuery = searchQuery.toLowerCase(); setFilteredPosts( posts.filter(post => post.title.toLowerCase().includes(lowerQuery) || post.tags.some(tag => tag.toLowerCase().includes(lowerQuery)) || post.contentHtml.toLowerCase().includes(lowerQuery) ) ); }, [searchQuery, posts]); return ( <div className="search-container"> <input type="text" placeholder="搜索文章..." value={searchQuery} onChange={(e) => setSearchQuery(e.target.value)} /> <div className="post-list"> {filteredPosts.map(post => ( <div key={post.id} className="post-item"> <h3>{post.title}</h3> <p>{post.date}</p> <div>{post.tags.map(tag => <span key={tag}>{tag}</span>)}</div> </div> ))} </div> </div> ); }
注意事项:
- 方案二会增加客户端Bundle体积(需要打包
gray-matter和remark相关库),但仅2篇文章的场景下影响极小。 - 两种方案都完全符合静态站点要求,构建后所有资源都是静态文件,可直接部署到S3+CloudFront。
内容的提问来源于stack exchange,提问作者dsidler88
相关产品推荐
相关产品推荐

