Resolving NextJS Metadata Errors

frontblog
Table of Contents
  1. 1. Introduction
  2. 2. Attempts to Resolve the Issue
    1. 2.1. Separate Client Components
    2. 2.2. Change Routes
    3. 2.3. Import Issues
  3. 3. Follow-Up Actions
  4. References

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