You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Next.js 13应用生成读取本地数据静态页面并实现搜索过滤

解决Next.js App Router静态博客的搜索过滤问题

针对你的场景(静态站点部署在S3+CloudFront,无法使用SSR,需要搜索过滤功能),有两种可行方案,都完全适配静态站点需求:

方案一:构建时预生成文章元数据JSON(推荐,适合仅2篇文章的场景)

核心思路是在构建阶段把所有文章的元数据(标题、标签、内容等)提前生成一个静态JSON文件,客户端直接读取这个文件做搜索过滤,完全避开客户端无法访问fs的问题。

步骤:

  1. 编写预构建脚本:创建一个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();
  1. 配置预构建命令:在package.json里添加prebuild脚本,确保构建前自动生成JSON文件:
"scripts": {
  "prebuild": "node scripts/generate-posts-meta.js",
  "build": "next build",
  "dev": "next dev",
  "start": "next start"
}
  1. 客户端实现搜索过滤:创建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内容。

步骤:

  1. 确保MD文件可访问:所有MD文件放在public/posts目录下,这样可以通过/posts/[id].md的URL访问。

  2. 客户端实现解析与搜索:在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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.20 22:44:56