Fails to find files due to URL decoding during build

In Next.js 15, when using generateStaticParams to generate static paths, next dev runs normally, but next build fails with an error stating that the file cannot be found (ENOENT: no such file or directory).

Specifically, the URL parameters generated by generateStaticParams are incorrectly decoded during the next build process, causing a path mismatch. For example:

  • generateStaticParams generates the path /detail/%E6%96%87%E7%AB%A0 (URL-encoded “文章”).
  • However, fs.readFileSync tries to read public/%E6%96%87%E7%AB%A0.md, while the actual file path should be public/文章.md.

This causes next build to fail, even though next dev runs successfully.

Error message:

> url-encode-error-next@0.1.0 build
> next build

   ▲ Next.js 15.1.6

   Creating an optimized production build ...
 ✓ Compiled successfully
 ✓ Linting and checking validity of types
 ✓ Collecting page data
Error occurred prerendering page "/detail/%E6%96%87%E7%AB%A0". Read more: https://nextjs.org/docs/messages/prerender-error
Error: ENOENT: no such file or directory, open 'D:\WorkSpace\My\Next\url-encode-error-next\public\%E6%96%87%E7%AB%A0.md'
    at Object.readFileSync (node:fs:448:20)
    at h (D:\WorkSpace\My\Next\url-encode-error-next\.next\server\app\detail\[name]\page.js:1:190139)
    at l (D:\WorkSpace\My\Next\url-encode-error-next\.next\server\app\detail\[name]\page.js:1:190247)
Export encountered an error on /detail/[name]/page: /detail/%E6%96%87%E7%AB%A0, exiting the build.
 ⨯ Static worker exited with code: 1 and signal: null

Minimal Reproducible Example

Steps to Reproduce

  1. Create a file public/文章.md and add some content to it.
  2. Implement generateStaticParams and file reading logic in app/detail/[name]/page.tsx:
import path from "node:path";
import * as fs from "node:fs";
import matter from "gray-matter";

export async function generateStaticParams() {
  const notesDir = path.join(process.cwd(), "public");
  const fileNames = fs.readdirSync(notesDir);
  return fileNames.map((fileName) => {
    const encodedName = encodeURIComponent(fileName.replace(/\.md$/, ""));
    return { name: encodedName };
  });
}

async function getMarkdownContent(name: string) {
  const decodedName = decodeURIComponent(name); // 解码路径参数
  const filePath = path.join(process.cwd(), "public", `${decodedName}.md`);
  const fileContent = fs.readFileSync(filePath, "utf8");
  const { content } = matter(fileContent);
  return content;
}

export default async function NoteDetail({ params }: { params: { name: string } }) {
  const { name } = params;
  const content = await getMarkdownContent(name);
  return <div dangerouslySetInnerHTML={{ __html: content }} />;
}

Hi @1940879828, welcome to the Vercel Community!

Thanks for posting your question here.

Have you tried using decodeURI function to solve this?

I tried, but it didn’t work.
I modified this section of my example:

- const encodedName = encodeURIComponent(fileName.replace(/\.md$/, ""))
+ const encodedName = encodeURI(fileName.replace(/\.md$/, ""))
- const decodedName = decodeURIComponent(name)
+ const decodedName = decodeURI(name)

In addition, I found that decodeURIComponent did not take effect after being deployed to vercel

Hi @1940879828, I see. I was able to deploy your page to https://anshuman-can-ship.vercel.app/detail/%E6%96%87%E7%AB%A0/

Is this what you wanted?

import path from "node:path";
import * as fs from "node:fs";
import matter from "gray-matter";

export async function generateStaticParams() {
  const notesDir = path.join(process.cwd(), "public");
  const fileNames = fs.readdirSync(notesDir);
  return fileNames
    .filter((f) => f.endsWith(".md"))
    .map((fileName) => {
      const encodedName = fileName.replace(/\.md$/, "");
      return { name: encodedName };
    });
}

async function getMarkdownContent(name: string) {
  const decodedName = decodeURIComponent(name); // 解码路径参数
  const filePath = path.join(process.cwd(), "public", `${decodedName}.md`);
  const fileContent = fs.readFileSync(filePath, "utf8");
  const { content } = matter(fileContent);
  return content;
}

export default async function NoteDetail({
  params,
}: {
  params: { name: string };
}) {
  const { name } = params;
  const content = await getMarkdownContent(name);
  return <div dangerouslySetInnerHTML={{ __html: content }} />;
}

ohhh,It work! thank you for your help I really appreciate the time and effort you put into solving it. Thanks again!

1 Like

This topic was automatically closed 7 days after the last reply. New replies are no longer allowed.