Resolving NextJS Metadata Errors
- front
- blog
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