Consider utilizing 'fetch' or alternative techniques in place of 'fs' when reading markdown files specifically within NextJS

I'm currently working on a blog that includes math formulas in markdown pages. The markdown files are stored locally in a folder. Here is the code snippet from my Blogpage.tsx file-


import React from 'react'
import fs from "fs"
import Markdown from "react-markdown"
import rehypeKatex from "rehype-katex"
import remarkMath from "remark-math"
import rehypeRaw from "rehype-raw"
import matter from "gray-matter"
import 'katex/dist/katex.min.css'

// export const runtime = 'edge'
const getPostContent = (postID: string) => {
  const folder = "src/blogPosts";
  const file = `${folder}/${postID}.md`;
  const content = fs.readFileSync(file, "utf8");
  const matterResult = matter(content);
  return matterResult;
};

export const generateStaticParams = async () => {
  return [{ postID: "blog-post1" }]
};

const BlogPage = (props: any) => {

  const postID = props.params.postID;
  const post = getPostContent(postID)

  return (
    <>
      <h1>{post.data.title}</h1>
      <Markdown remarkPlugins={[remarkMath]} rehypePlugins={[rehypeKatex, rehypeRaw]}>
        {post.content}
      </Markdown>
    </>
  )
}

export default BlogPage

Localhost runs smoothly, but encountering an error when trying to deploy on Cloudflare pages.


19:18:58.294    ⚡️ Completed `npx vercel build`.
19:18:58.342    ⚡️ Invalid prerender config for /allblogs/[postID]
...
(remaining log messages)
...

Introducing export const runtime = 'edge' on the page leads to an error related to the 'fs' module:

Error: Module not found: Can't resolve 'fs'

Looks like there's a conflict between the edge runtime and the nodejs module. Trying an alternate approach using fetch:


const getPostContent = async (postID: string) => {
  const resource = await import(`${folder}/${postID}.md`);
  const res = await fetch(resource.default);
  if (!res.ok) {
      throw new Error(`${resource.default}: ${res.status} ${res.statusText}`);
  }
  return res.text();
};

This method throws an error -

⨯ unhandledRejection: Error: Cannot find module 

Seeking assistance with reading markdown files without utilizing 'fs'. Also looking for alternative ways to import markdown files without conflicting with the edge runtime.

Update

Solved issues with uploading to Cloudflare pages by utilizing NextJS static export feature.

Note: For NextJS version 13 or newer, use generateStaticParams() instead of getStaticProps() and getStaticPaths(). Details can be found here.

Still in search of a good alternative for fetching markdown files from local directories without relying on 'fs'. Open to suggestions!

Answer №1

If you're utilizing the @cloudflare/next-on-pages package along with dynamic routes using the app router (e.g., src\app\posts\[slug]\page.tsx), then making sure to set the dynamicParams flag to false might be necessary.

I encountered a similar issue to what you described, where my blog content consisted of mdx files on disk and I aimed to statically generate each blog page. However, during the build process, I started receiving errors prompting me to enable the edge runtime. Subsequently, when I did that, my library for loading the mdx content stopped working due to the unavailability of fs.

In my case, setting

export const dynamicParams = false;
resolved the issue by directing nextjs not to generate a cloudflare worker for handling requests for pages that weren't statically rendered. This action eliminated concerns about which runtime was being utilized.

Please refer to the examples provided below.

You can also check .

src\app\posts\[slug]\page.tsx

import { getArticleBySlug, getAllArticles } from "@/lib/articles"

/*
 * https://developers.cloudflare.com/pages/framework-guides/nextjs/deploy-a-nextjs-site/#generatestaticparams
 * 
 * When doing static site generation (SSG) in the /app directory and using the generateStaticParams function,
 * Next.js by default tries to handle requests for non statically generated routes on-demand. It does so by
 * creating a Node.js serverless function (which, as such, is incompatible with @cloudflare/next-on-pages).
 * 
 * In such cases you need to instruct Next.js not to do so by specifying dynamicParams = false.
 */
export const dynamicParams = false;

export async function generateStaticParams() {
    const posts = await getAllArticles();

    return posts.map((post) => ({
        slug: post.slug,
    }));
}

export default async function Page({ params }: { params: { slug: string } }) {
    const article = await getArticleBySlug(params.slug);
    return (
        <article.component />
    );
}

src\lib\articles.ts

import glob from 'fast-glob';

interface Article {
    title: string
    description: string
    authors: string[]
    date: string,
    featuredImage?: {
        url: string
    }
}

export interface ArticleWithSlug extends Article {
    slug: string
}

export interface ArticleWithComponent extends ArticleWithSlug {
    component: React.ComponentType
}

async function importArticle(
    articleFilename: string,
): Promise<ArticleWithComponent> {
    let post = (
        await import(`../content/posts/${articleFilename}`)
    ) as {
        default: React.ComponentType
        article: Article,
        metadata: Record<string, string>
    }

    const slug = articleFilename.replace(/(\/page)?\.mdx$/, '');

    return {
        component: post.default,
        slug,
        ...post.article
    }
}

export async function getArticleBySlug(slug: string) {
    return importArticle(`${slug}/page.mdx`);
}

export async function getAllArticles(): Promise<ArticleWithComponent[]> {
    const articleFilenames = await glob('*/page.mdx', {
        cwd: './src/content/posts',
    })

    const articles = (await Promise.all(
        articleFilenames.map(importArticle)
    ));

    return articles.sort((a, z) => +new Date(z.date) - +new Date(a.date))
}

Answer №2

If you're encountering a familiar challenge when deploying Next.js applications that utilize Node.js-specific modules such as fs to static hosting services like Cloudflare Pages, you're not alone.

Unfortunately, the fs module isn't accessible in browsers or serverless functions used by Cloudflare Pages for dynamic routes.

To resolve this issue, consider utilizing Next.js's getStaticProps and getStaticPaths functions to retrieve markdown files during the build phase rather than runtime execution.

This method allows you to pre-render each blog post as a static page, enabling direct serving by Cloudflare Pages without file system access requirements.

import React from 'react';
import fs from 'fs';
import path from 'path';
import Markdown from 'react-markdown';
import rehypeKatex from 'rehype-katex';
import remarkMath from 'remark-math';
import rehypeRaw from 'rehype-raw';
import matter from 'gray-matter';
import 'katex/dist/katex.min.css';

const postsDirectory = path.join(process.cwd(), 'src/blogPosts');

export async function getStaticPaths() {
  // Read all markdown files in the postsDirectory
  const filenames = fs.readdirSync(postsDirectory);

  // Create a route for each markdown file
  const paths = filenames.map((filename) => {
    return {
      params: {
        postID: filename.replace(/\.md$/, ''),
      },
    };
  });

  return {
    paths,
    fallback: false,
  };
}

export async function getStaticProps({ params }) {
  // Read the markdown file for the current postID
  const fullPath = path.join(postsDirectory, `${params.postID}.md`);
  const fileContents = fs.readFileSync(fullPath, 'utf8');

  // Parse the post metadata section
  const matterResult = matter(fileContents);

  // Pass the post data to the page via props
  return {
    props: {
      post: matterResult,
    },
  };
}

const BlogPage = ({ post }) => {
  return (
    <>
      <h1>{post.data.title}</h1>
      <Markdown
        remarkPlugins={[remarkMath]}
        rehypePlugins={[rehypeKatex, rehypeRaw]}
      >
        {post.content}
      </Markdown>
    </>
  );
};

export default BlogPage;

Let me break down the code above:

  1. getStaticPaths scans through your src/blogPosts folder to identify markdown files and create paths for them. This informs Next.js about the pages to generate during the build process.
  2. getStaticProps utilizes the provided postID from getStaticPaths, reads the associated markdown file, and delivers the content to your page as props.
  3. The BlogPage component accepts the post content as props and displays it on the page.

Hopefully, this explanation proves helpful to you!!!

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Using dual index variables in Angular 4's ngFor loop for iterating through arrays

Is there a way to generate unique index numbers for items printed only on Saturdays? Specifically, I want the index to start from 0 once Saturday begins and continue incrementing. The rest of the options should have the same index numbers. Any suggestions ...

What is the process for obtaining the directory path in a Next.js project that has been deployed to Vercel?

In my Next.js project, I have implemented internationalization using Fluent (). The localization data is stored in .ftl files within the locales directory. This is the abstract structure of my project: my-app/ ├─ src/ │ ├─ app/ │ ├─ loca ...

Initiate and terminate server using supertest

I've developed a server class that looks like this: import express, { Request, Response } from 'express'; export default class Server { server: any; exp: any; constructor() { this.exp = express(); this.exp.get('/' ...

I am looking to create a counter in NextJS that will store its value in a database for persistent storage. How can

In my NextJS and ReactJS project, I am creating a like-counter feature that will keep track of the number of likes a user can give. The maximum limit for likes is set to 100. The count is stored in a FaunaDB. While I have successfully displayed the curren ...

How can I assign two different colors based on the type in Typescript?

I am configuring a color property based on the nature of the display. colorStyle: { textAlign: "center", backgroundColor: "transparent", color: (theme.colors.BaseColor.Red as any).Red4, } The cu ...

Register with your Facebook account - Angular 4 Typescript

After extensive searching, I have yet to find a clear answer to my question... This is my first time delving into the world of Social Networks. I am curious to know if there are options for Facebook and Twitter sign-ins using Typescript. It seems that Go ...

Issue with Google Adsense - adsbygoogle.push() error: Pages can only support one AdSense head tag. The additional tag will be disregarded

After adding my script tag to the navbar component, I encountered an error on my site during the next js build: Google Adsense error -adsbygoogle.push() error: Only one AdSense head tag supported per page. The second tag is ignored <Head> ...

NextJs/Express Server Encounters Domain HTTPS Request Error

Yesterday, I successfully deployed my website on a VPS. Everything was working fine on the VPS IP until I installed certbot on my domain. Unfortunately, I encountered the following error: Mixed Content: The page at 'https://mydomain.com/' was loa ...

Is there a way to access window.location.hash through a Next.js application?

Whenever I use useEffect with window.location.hash, it consistently returns '0' incorrectly. This issue seems to be related to Server-Side Rendering. I am looking for a dependable way to extract the hash from the URL in my project. What would be ...

`AngularJS Voice Recognition Solutions`

In my quest to implement voice recognition in an AngularJS application I'm developing for Android and Electron, I've encountered some challenges. While I've already discovered a suitable solution for Android using ng-speech-recognition, fin ...

Exploring TypeScript Heartbeat functionality with Nodejs ws module

I am currently in the process of setting up a WebSocket server using NodeJs and TypeScript. The WebSocket server implementation I have chosen is from the ws package, supplemented by the @types/ws package for typings. My goal is to have the server send out ...

Attempting to incorporate alert feedback into an Angular application

I am having trouble referencing the value entered in a popup input field for quantity. I have searched through the documentation but haven't found a solution yet. Below is the code snippet from my .ts file: async presentAlert() { const alert = awa ...

Setting dynamic values within the constructor of a TypeScript class to the object instance

Can you help me troubleshoot an issue in this simplified class code snippet? I'm attempting to dynamically assign values to this by looping over key values in the constructor, but it's not working as expected. Could this be a syntax problem or is ...

Is there a way to apply a consistent style to all the fields of an object at once?

I have a formatting object named typography that includes various styles. One common issue I've encountered is that the line-height property is consistently set to 135%. Anticipating that this might change in the future, I am looking for a way to cent ...

How to link observables in HTTP requests using Angular 5?

I'm currently developing an application using Angular 5, and I want to segregate raw http calls into their own services so that other services can modify the responses as needed. This involves having a component, component service, and component data ...

"Converting array into a string in TypeScript/Javascript, but unable to perform operations

After generating a string with the correct structure that includes an array, I am able to navigate through the JSON on sites like However, when attempting to access the array, it turns out that the array itself is null. Here is the scenario: Firstly, th ...

The struggle of implementing useReducer and Context in TypeScript: A type error saga

Currently attempting to implement Auth using useReducer and Context in a React application, but encountering a type error with the following code snippet: <UserDispatchContext.Provider value={dispatch}> The error message reads as follows: Type &apos ...

Unending loop detected in the React useEffect hook during testing

This code snippet is from the latest version of NextJS (13). export default function Members() { // States const [users, setUsers] = useState<MemberType[]>([]); const [loading, setLoading] = useState(false); const searchParams = useSearchPara ...

Ways to showcase information based on the chosen option?

Is there a way to display data based on the selected value in a more efficient manner? Currently, when clicking on a value from the list on the left side, new data is added next to the existing data. However, I would like the new data to replace the existi ...

A TypeScript method for accessing deeply nested properties within an object

I'm currently working on a function that utilizes typings to extract values from a nested object. With the help of this post, I managed to set up the typing for two levels successfully. However, when I introduce a third (known) level between the exis ...