vercel / next.js

The React Framework
https://nextjs.org
MIT License
123.02k stars 26.28k forks source link

[NEXT-1195] Unable to build when using metadata and a dynamic import (await import(''), not next/dynamic) #47387

Open HMilbradt opened 1 year ago

HMilbradt commented 1 year ago

Verify canary release

Provide environment information

Operating System:
      Platform: linux
      Arch: x64
      Version: #22 SMP Tue Jan 10 18:39:00 UTC 2023
    Binaries:
      Node: 16.17.0
      npm: 8.15.0
      Yarn: 1.22.19
      pnpm: 7.1.0
    Relevant packages:
      next: 13.2.5-canary.12
      eslint-config-next: N/A
      react: 18.2.0
      react-dom: 18.2.0

(Note: This is straight from the Codesandbox instance)

Which area(s) of Next.js are affected? (leave empty if unsure)

MDX (@next/mdx)

Link to the code that reproduces this issue

https://codesandbox.io/p/github/HMilbradt/next-bug/main?file=%2FREADME.md&workspace=%257B%2522activeFileId%2522%253A%2522clfj5a7i80000g3f115xy7jew%2522%252C%2522openFiles%2522%253A%255B%2522%252FREADME.md%2522%255D%252C%2522sidebarPanel%2522%253A%2522EXPLORER%2522%252C%2522gitSidebarPanel%2522%253A%2522COMMIT%2522%252C%2522spaces%2522%253A%257B%2522clfj5a9m9001d356ruhenriv3%2522%253A%257B%2522key%2522%253A%2522clfj5a9m9001d356ruhenriv3%2522%252C%2522name%2522%253A%2522Default%2522%252C%2522devtools%2522%253A%255B%257B%2522key%2522%253A%2522clfj5a9ma001e356rnp4xvztx%2522%252C%2522type%2522%253A%2522PROJECT_SETUP%2522%252C%2522isMinimized%2522%253Afalse%257D%255D%257D%257D%252C%2522currentSpace%2522%253A%2522clfj5a9m9001d356ruhenriv3%2522%252C%2522spacesOrder%2522%253A%255B%2522clfj5a9m9001d356ruhenriv3%2522%255D%252C%2522hideCodeEditor%2522%253Afalse%257D

To Reproduce

Simply open the app. It will break immediately.

To show that MDX is actually working, you can also remove the metadata from the "broken" page.tsx, or remove the dynamic import.

Describe the Bug

The build throws saying that you're attempting to use metadata in a client component. I'm assuming that the use of the dynamic import automatically means I'm using a client component.

Expected Behavior

I would expect that this would work out of the box. MDX renders fine when it's imported normally, and while this might be somewhat uncommon to do, it's incredibly useful for building out a blog based on MDX.

Note that this example is an overly simplified version of what I'm doing. In practice, I have a number of content files that I want to co-locate in my app directory, and in order to show my list of this content I need to dynamically load them in.

Thanks!

Which browser are you using? (if relevant)

No response

How are you deploying your application? (if relevant)

No response

NEXT-1195

HMilbradt commented 1 year ago

If anyone stumbles upon this, I ended up working around this by instead using contentlayer and it's local filesystem. You give up some of the flexiblity of direct imports in your MDX files, but you gain type safety and the ability to use it properly with metadata.

Would still love to be able to use the simpler next version of this though, as I'm no longer able to optimize images without significant work.

schardev commented 1 year ago

Facing somewhat similar issue where if I import(...) a MDX file that has a dynamic import path (based on the page slug) then it will straight throw up an error. Although using a static import path inside dynamic import works.

~Edit: Repoduction - https://codesandbox.io/p/sandbox/goofy-chatterjee-vq74xd~

loukamb commented 1 year ago

Next.js 13 being "stable" is an enormous joke. I manage a blog for a large company and part of my work is making sure its using the latest technologies for pretty much everything, and gradually upgrading to Next.js 13 is impossible because it completely botched dynamic importing in generateStaticParams (in this case, mdx). Nobody wants to create a page.tsx file for every blog post. It's as if Next.js didn't account for this possibility and just wanted to release v13 ASAP, maybe because of competitive pressure from other frameworks. Corporate-driven desire to satisfy stakeholders over actual framework usability. Nuts.

tannerabread commented 1 year ago

I got this working with a dynamic page like follows:

// @/app/blog/[id]/page.tsx

import dynamic from "next/dynamic";
import { convertDate, getAllPostsMeta, getPageData } from "@/lib/posts";

export default async function Post({ params }: { params: { id: string } }) {
  const { id } = params;

  const { meta } = await getPageData(`${id}.mdx`);
  const { title, author, date } = meta;
  const convertedDate = convertDate(date);

  const Post = dynamic(() => import(`../posts/${id}.mdx`));

  if (!Post) return <div>Loading...</div>;

  return (
    <div key={id}>
      <h1>{title}</h1>
      <h3>
        {author} - {convertedDate}
      </h3>
      <Post />
    </div>
  );
}

// generate route segments
export async function generateStaticParams() {
  const posts = await getAllPostsMeta();

  return posts;
}
// @/lib/posts.ts

import fs from "fs";
import path from "path";

export interface MetaData {
  [key: string]: any;
}

const postsDirectory: string = path.join(process.cwd(), "app/blog/posts");
const fileNames: string[] = fs.readdirSync(postsDirectory);

export async function getPageData(id: string): Promise<MetaData> {
  const { meta } = require(`@/app/blog/posts/${id}`);
  const postData: MetaData = {
    meta: { ...meta, id: id.replace(/\.mdx/, "") },
  };
  return postData;
}

export async function getAllPostsMeta(): Promise<MetaData[]> {
  let posts = [];

  for (const file of fileNames) {
    const { meta } = await getPageData(file);
    posts.push(meta);
  }
  posts.sort((a: MetaData, b: MetaData) => {
    return a.date < b.date ? 1 : -1;
  });
  return posts;
}

export function convertDate(date: string): string {
  return new Date(date).toDateString();
}

I only have one page.tsx for all posts now

And a .mdx file would look like the following:

export const meta = {
  title: 'Post title',
  author: 'tannerabread',
}

## Introduction

Some content here
mtr1990 commented 10 months ago

@tannerabread

I got this working with a dynamic page like follows:

// @/app/blog/[id]/page.tsx

import dynamic from "next/dynamic";
import { convertDate, getAllPostsMeta, getPageData } from "@/lib/posts";

export default async function Post({ params }: { params: { id: string } }) {
  const { id } = params;

  const { meta } = await getPageData(`${id}.mdx`);
  const { title, author, date } = meta;
  const convertedDate = convertDate(date);

  const Post = dynamic(() => import(`../posts/${id}.mdx`));

  if (!Post) return <div>Loading...</div>;

  return (
    <div key={id}>
      <h1>{title}</h1>
      <h3>
        {author} - {convertedDate}
      </h3>
      <Post />
    </div>
  );
}

// generate route segments
export async function generateStaticParams() {
  const posts = await getAllPostsMeta();

  return posts;
}
// @/lib/posts.ts

import fs from "fs";
import path from "path";

export interface MetaData {
  [key: string]: any;
}

const postsDirectory: string = path.join(process.cwd(), "app/blog/posts");
const fileNames: string[] = fs.readdirSync(postsDirectory);

export async function getPageData(id: string): Promise<MetaData> {
  const { meta } = require(`@/app/blog/posts/${id}`);
  const postData: MetaData = {
    meta: { ...meta, id: id.replace(/\.mdx/, "") },
  };
  return postData;
}

export async function getAllPostsMeta(): Promise<MetaData[]> {
  let posts = [];

  for (const file of fileNames) {
    const { meta } = await getPageData(file);
    posts.push(meta);
  }
  posts.sort((a: MetaData, b: MetaData) => {
    return a.date < b.date ? 1 : -1;
  });
  return posts;
}

export function convertDate(date: string): string {
  return new Date(date).toDateString();
}

I only have one page.tsx for all posts now

And a .mdx file would look like the following:

export const meta = {
  title: 'Post title',
  author: 'tannerabread',
}

## Introduction

Some content here

How do you make it work?

Apparently

const Post = dynamic(() => import(`../posts/${id}.mdx`));

Doesn't work. It always returns error code 500

It will just work like this as static file

const Post = dynamic(() => import(`../posts/post-1.mdx`));

Maybe share a full repo?