Resolving NextJS Metadata Errors

Table of Contents

1. Introduction

This blog is currently undergoing migration to the app router of Next.js 13. A detailed discussion of this process will be posted later. Here, I summarize a minor issue that arose during the migration and consumed considerable time.

The page structure configured with the app router is organized as follows:

app
├── (doc)
│   ├── about
│   │   ├── page.tsx
│   ├── posts
│   │   ├── [slug]
│   │   │   ├── page.tsx
│   ├── layout.tsx
├── (page)
│   ├── posts
│   │   ├── all
│   │   │   ├── page.tsx
│   │   │   ├── [page]
│   │   │   │   ├── page.tsx
│   │   ├── tag/[tag]
│   │   │   ├── page.tsx
│   │   │   ├── [page]
│   │   │   │   ├── page.tsx
│   │   ├── page.tsx
├── main page

The /posts page requires client state management due to its search functionality, thus it has been managed as a client component. Consequently, APIs like generateMetadata, which are only usable in server components, were not utilized.

Unexpectedly, an error occurred on the /posts/tag/[tag]/[page] page, which is constructed as a server component.

You are attempting to export "generateMetadata" from a component marked with "use client", which is disallowed. Either remove the export, or the "use client" directive. Read more: https://nextjs.org/docs/getting-started/react-essentials#the-use-client-directive

I made several attempts to resolve this issue, which I outline below.

2. Attempts to Resolve the Issue

2.1. Separate Client Components

The generateMetadata API can only be used on the server side. Based on the error message, I speculated that the rendering of the /posts page in use client mode might be influencing the situation, so I attempted to change that first. The current structure of /posts/page.tsx is as follows:

'use client';
/* imports omitted */
function PostSearchPage() {
  const searchPosts: CardProps[] = getSearchPosts();
  const [searchKeyword, debouncedKeyword, setSearchKeyword] = useSearchKeyword();
  const [filteredPostList, setFilteredPostList] = useState<CardProps[]>(searchPosts);
  const [page, setPage] = useState<number>(1);
  const debouncedPage = useDebounce(page.toString(), 300);

  const infiniteScrollRef = useRef<HTMLDivElement>(null);
  const totalPage = Math.ceil(filteredPostList.length / ITEMS_PER_PAGE);

  const onKeywordChange = useCallback((event: ChangeEvent<HTMLInputElement>) => {
    setSearchKeyword(event.target.value);
  }, [setSearchKeyword]);

  useEffect(() => {
    setFilteredPostList(filterPostsByKeyword(searchPosts, debouncedKeyword));
  }, [debouncedKeyword]);

  useInfiniteScroll(infiniteScrollRef, useCallback(()=>{
    if (page < totalPage) {
      setPage(prev=>prev + 1);
    }
  }, [debouncedPage, totalPage]));

  return (
    <>
      <Title heading='h2' size='md'>Search All Posts</Title>
      <SearchConsole 
        value={searchKeyword}
        onChange={onKeywordChange}
      />
      {filteredPostList.length === 0 ?
        <p>No search results found.</p> : null
      }
      <PostList postList={filteredPostList.slice(0, ITEMS_PER_PAGE * page)} />
      <div ref={infiniteScrollRef} />
    </>
  );
}

To address this, I created a new component named SearchPageBody to handle the client rendering requirements and separated it into a pageBody.tsx file. This component will render in use client mode, while /posts/page.tsx was modified for server-side rendering.

// posts/page.tsx
/* imports omitted */

function PostSearchPage() {

  return (
    <>
      <Title heading='h2' size='md'>Search All Posts</Title>
      <SearchPageBody />
    </>
  );
}

However, the error still persisted.

2.2. Change Routes

Upon reviewing the error message again:

You are attempting to export "generateMetadata" from a component marked with "use client", which is disallowed. Either remove the export, or the "use client" directive. Read more: https://nextjs.org/docs/getting-started/react-essentials#the-use-client-directive

    ,-[/Users/kimsunghyun/Desktop/nextjs-blog/src/app/(page)/posts/tag/[tag]/[page]/page.tsx:86:1]
 86 |   return paths;
 87 | }
 88 | 
 89 | export async function generateMetadata({ params }: Props): Promise<Metadata> {
    :                       ^^^^^^^^^^^^^^^^
    ... abbreviated generateMetadata content...

The file path indicated seems to provide information about where the problem arises, targeting /posts/tag/[tag]/[page]. Next.js documentation states that metadata is generated by traversing from the root segment until reaching the current page's page.js. Therefore, I hypothesized that the metadata generation for /posts/tag/[tag]/[page] might be influenced by traversing the /posts route, thus causing the error.

I attempted to relocate the rendering component of /posts to a different directory. I renamed /posts/page.tsx to /search. Nevertheless, the error merely changed the file path to ./src/app/(page)/search/page.tsx.

Notably, when accessing a dynamic route such as /posts/tag/study/2, the bug did not occur, even though generateMetadata is located in /posts/tag/[tag]/[page]/page.tsx, indicating that the issue was confined to /posts.

2.3. Import Issues

Through these experiments, I surmised that there might be a dependency from the /posts path to the /posts/tag/[tag]/[page] segment.

When the imports create a static dependency graph, the /posts route references /posts/tag/[tag]/[page], which contains generateMetadata, leading to the assumption that /posts is utilizing generateMetadata, thus resulting in the aforementioned error.

Indeed, examining the imports in /posts/page.tsx revealed...

'use client';

import { useCallback, ChangeEvent, useEffect, useState, useRef } from 'react';

import Title from '@/components/atoms/title';
import SearchConsole from '@/components/molecules/searchConsole';
import { CardProps } from '@/components/organisms/card';
import PostList from '@/components/templates/postList';
import filterPostsByKeyword from '@/utils/filterPosts';
import { getSearchPosts } from '@/utils/post';
import { useDebounce } from '@/utils/useDebounce';
import { useInfiniteScroll } from '@/utils/useInfiniteScroll';
import useSearchKeyword from '@/utils/useSearchKeyword';

/* This part is the issue */
import { ITEMS_PER_PAGE } from './tag/[tag]/[page]/page';

function PostSearchPage() {
  /* Implementation of the search page component */
}

Thus, adjusting the aforementioned section resolves the problem. It turned out to be a trivial cause.

'use client';

/* Previous imports omitted */
import useSearchKeyword from '@/utils/useSearchKeyword';

const ITEMS_PER_PAGE = 10;

function PostSearchPage() {
  /* Implementation of the search page component */
}

3. Follow-Up Actions

Since the ITEMS_PER_PAGE variable is used in many places, defining it in each file is not ideal. Instead, it was placed in the src/utils/post.ts file, where the functions for retrieving posts reside.

// src/utils/post.ts
/* Number of posts displayed per page */
export const ITEMS_PER_PAGE = 10;
/* First page */
export const FIRST_PAGE = 1;

I then updated all import paths using this variable accordingly. For instance:

// src/app/(page)/posts/page.tsx
import { getSearchPosts, ITEMS_PER_PAGE } from '@/utils/post';

As a result, the generateMetadata-related error vanished. For similar reasons, the FIRST_PAGE variable was also moved to src/utils/post.ts, as it was originally placed in an odd location like src/app/(page)/posts/all/page.tsx.

This seemingly trivial reason led to a bug that consumed nearly two days, reinforcing the importance of properly organizing even the smallest variables within the structure.

References

Next.js optimizing metadata documentation: https://nextjs.org/docs/app/building-your-application/optimizing/metadata