vercel / next.js

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

Add `app/` directory support for custom title in Error page #45620

Open Josehower opened 1 year ago

Josehower commented 1 year ago

Describe the feature you'd like to request

I would like to have the possibility to control the page title, description etc. in the error and not-found page.

currently Its possible to get around not-found page by creating a conditional in metadata function for the not-found page but is still not nice.

Describe the solution you'd like

I would like not-found.js and error.js accept metadata and generateMetadata() similar to page.js and layout.js

// app/entity/[entityId]/not-found.js

export async function generateMetadata(props) {
  return {
      title: `entity ${props.params.entityId} not found`,
      description: `There is no entity with id: ${props.params.entityId}`
    }
}

export default function EntityNotFound() {
  return <div>Sorry this Entity was not found</div>;
}
// app/error.js

export const metadata = {
      title: "Error",
      description: "ups something went wrong"
    }

export default function Error() {
  return <div>Sorry this Entity was not found</div>;
}

Describe alternatives you've considered

for not-found, maybe allow to pass the metadata object as an argument for the function

export default async function AnimalPage(props) {
  const singleEntity = await getAnimalById(props.params.entityId);

  if (!singleEntity) {

    const metadata = {
      title: `entity ${props.params.entityId} not found`,
      description: `There is no entity with id: ${props.params.entityId}`
    }
    notFound(metadata);
  }

return <>
... rest of the component

}
karlhorky commented 1 year ago

Interesting, seems like Shu was able to get a document.title on this example page

Screenshot 2023-03-29 at 17 54 29

This was part of this PR:

Not sure how this was achieved, I've asked over here:

karlhorky commented 1 year ago

I was actually having a different problem to this one just now - the metadata from the root layout is also not currently (as of next@13.2.5-canary.20) applied to not-found.tsx 404 pages (maybe also error.tsx?) 😯

StackBlitz: https://stackblitz.com/edit/vercel-next-js-znhsus?file=app%2Flayout.tsx&file=app%2Fnot-found.tsx&file=package-lock.json

2023-03-29_18-08

karlhorky commented 1 year ago

Workaround

I guess for a static title, this hack can be used with the App Router (doesn't result in a hydration error, interestingly enough):

// app/not-found.tsx
export default function NotFound() {
  return (
    <div>
      {/*
        No support for metadata in not-found.tsx yet
        https://github.com/vercel/next.js/pull/47328#issuecomment-1488891093
      */}
      <title>Not Found | example.com</title>
    </div>
  );
}

Maybe this is what it will look like in the future once React Float more fully lands...

Or @gnoff @sebmarkbage should this already work now with the current state of React Float + Next.js? Was really surprised by lack of hydration error in both dev and production 🤔

Update Feb 2024: Maybe this is actually the new Document Metadata feature mentioned in the React Labs Feb 2024 "What We've Been Working On" blog post:

Document Metadata: we added built-in support for rendering <title>, <meta>, and metadata <link> tags anywhere in your component tree. These work the same way in all environments, including fully client-side code, SSR, and RSC. This provides built-in support for features pioneered by libraries like React Helmet.

huozhi commented 1 year ago

We're thinking to support it but might only support metadata as you could keep use notFound() use inside generateMetadata() which could cause more issues

karlhorky commented 1 year ago

but might only support metadata as you could keep use notFound() use inside generateMetadata() which could cause more issues

Oh too bad, so no way to generate dynamic titles / descriptions based on the requested content 🙁

Maybe there would still be some ways to support generateMetadata() eg:

  1. If notFound() used within generateMetadata() in not-found.tsx / error.tsx, throw an error
  2. Also include a lint rule to warn users before it happens
huozhi commented 1 year ago

@karlhorky error checking is possible to be added there to avoid using notFound in error/not-found page convention. Just curious, mind sharing the dynamic title/description case for your not-found page?

karlhorky commented 1 year ago

Sure, I was thinking of something kind of similar to the one in the issue description:

// app/animals/[animalId]/not-found.js

export async function generateMetadata(props) {
  return {
    title: `Animal id ${props.params.animalId} not found`,
    description: `There is no animal with id "${props.params.animalId}"`,
  };
}

export default function AnimalNotFound(props) {
  return <div>Sorry, an animal with id "{props.params.animalId}" was not found</div>;
}
huozhi commented 1 year ago

I see, dynamic routes with params makes sense. We'll think of support for that case

jahirfiquitiva commented 1 year ago

I'm also hoping this feature is added soon. We were able to do this in the pages directory using <Head>, so it'd be nice to have it with the app directory too 😄

lindesvard commented 1 year ago

Would love this feature as well. Running a i18n site with this folder structure -> [locale]/not-found.tsx

I want to be able to get the locale param so we can fetch correct translations for not-found page.

tobobo commented 1 year ago

A related issue I don't see mentioned here is that without metadata in error pages, the viewport meta tag is missing and mobile layouts are broken.

bitfrost commented 1 year ago

I have something working for me. I determine if I am in a 404 state in generateMetadata for a route like. [blah]/page.tsx and return something like { title: 'Not Found' } in [blah]/not-found.tsx I just have the error markup. The meta data from page.tsx is present when I get my 404 hit I get the metadata set in page.tsx + added by the notFound() call. (notFound() is called in page.tsx's component tree, not in generateMetadata of [blah]/page.tsx

bacvietswiss commented 1 year ago

Currently, I need to manually add <meta name="viewport" content="width=device-width, initial-scale=1" ></meta> into the not found page in order to make it responsive

leonbe02 commented 1 year ago

@huozhi Any ETA on when params support will be added to the not-found.js page? I find it hard to believe that Next doesn't support localized 404 pages out of the box, that seems like a pretty important feature.

huozhi commented 1 year ago

Currently, I need to manually add <meta name="viewport" content="width=device-width, initial-scale=1" ></meta> into the not found page in order to make it responsive

@bacvietswiss that issue is fixed in #50044

huozhi commented 1 year ago

@leonbe02 Can you file another issue about that as the original issue here is related to metadata support

leonbe02 commented 1 year ago

@leonbe02 Can you file another issue about that as the original issue here is related to metadata support

I found an existing ticket that outlines the issue: https://github.com/vercel/next.js/issues/48763

sayhicoelho commented 1 year ago

@huozhi The not found page is refreshing every 3 seconds and we don't have <link rel="icon" href="/favicon.ico" type="image/x-icon" sizes="any"> tag.

Mohhaliim commented 1 year ago

@sayhicoelho have you ever fixed the refreshing thing?

sayhicoelho commented 1 year ago

@sayhicoelho have you ever fixed the refreshing thing?

No. I give up Next for now. I had a lot of problems and a lot of weird bugs.

MuhammadMinhaj commented 1 year ago

Regarding the custom metadata for the not-found.js page, maybe I've found a workaround!

Currently, we are facing a problem where we throw a notFound() error when no page is found in our dynamic route. At that time, we don't see the meta title and description of the not-found.js page. Instead, we see the meta data of the current page. I believe this is logical and not a bug in Next.js. In the case of a dynamic route, the meta data of the not-found.js page should also be dynamic, as it might be useful in some scenarios.

So, in the case of a dynamic route, we can set the meta data for the not-found.js page dynamically from page.js. It's important to note that the generateMetadata function should not throw a notFound error. Instead, the page component should throw a notFound error. For example, you can see the screenshot below. I hope this solution works.

My folder structure:

app/[locale]/[...slugs]/page.js
app/[locale]/[...slugs]/not-found.js
app/[locale]/[...slugs]/error.js

Screenshot 2023-07-09 221946

itsjavi commented 1 year ago

@MuhammadMinhaj nice, but the workaround still doesn't solve de problem for the catch-all unmatched routes

p00000001 commented 1 year ago

@itsjavi One possible workaround for that is similar to https://github.com/vercel/next.js/issues/48763#issuecomment-1623745516

...you can use a catch all route and put a page.js under it, and then populate generateMetadata.

A possibly simpler interim solution to keep everything in not-found.js may be to just update the title in the client side DOM e.g.

useEffect(() => document.title = "Page Not Found")

...as I suppose for error pages other meta tags are probably less important and don't really need SSR support.

However, I agree with this issue that error pages should have their own generateMetadata method, as it would be a cleaner and more consistent.

huozhi commented 1 year ago

Did some research around metadata support for not-found and error convention. It's possible to support in not-found when it's server components as currently metadata is only available but for error it has to be client components then it's hard to support it there as it's a boundary. To support that it requires more changes in next.js to make them possible.

levipadre commented 1 year ago

Should generateMetadata work in not-found file then? I'm testing in v13.4.19 and it's nothing.

DennieMello commented 1 year ago

Should generateMetadata work in not-found file then? I'm testing in v13.4.19 and it's nothing.

It only doesn’t work during development, if you make a build, the metadata is displayed correctly. I also noticed that if you specify robots, it is duplicated on the page 2 times. Initially, next adds robots to the page with the value noindex.

pfurini commented 1 year ago

Should generateMetadata work in not-found file then? I'm testing in v13.4.19 and it's nothing.

It only doesn’t work during development, if you make a build, the metadata is displayed correctly. I also noticed that if you specify robots, it is duplicated on the page 2 times. Initially, next adds robots to the page with the value noindex.

Nope.. at least for me, I'm testing 14.0 and my issue is still there. I have title and description defined statically in a parent layout file, and then in a child route an error file that simply tries to redefine them statically. When the error page loads, it shows for a fraction of second the correct metadata, and then it reverts back to the layout (parent) one. This happens in both dev and prod builds.. I agree that Next has a ton of weird issues in apparently basic features, it makes me miss the good old client-server model...

Super-Kenil commented 1 year ago

Just like @karlhorky mentioned

<div>
      <title>Not Found</title>
  </div>

works. But I am using template

export const metadata: Metadata = {
  title: {
    template: '%s | My Website Name',
    default: 'My Website Name'
  }

So for someone like me, I had to write the below code

<div>
      <title>Not Found | My Website Name</title>
  </div>

The in my <em>not-found.tsx</em> replaces my template's fallback Title as well. I know its obvious, but I just wanted to let ya'll know</p> </div> </div> <div class="comment"> <div class="user"> <a rel="noreferrer nofollow" target="_blank" href="https://github.com/marcelhageman"><img src="https://avatars.githubusercontent.com/u/3671221?v=4" />marcelhageman</a> commented <strong> 10 months ago</strong> </div> <div class="markdown-body"> <p>Next.js 14.0.4 using App Router and <code>not-found.tsx</code>:</p> <pre><code class="language-tsx">export const metadata = { title: 'Page not found', description: 'This page could not be found', };</code></pre> <p>It works when I load the page, briefly, but the <code>page.tsx</code> in the same folder overwrites the title immediately.</p> <p>Tested in developer-mode. Obviously, that should not happen. </p> <p>Reading the docs: <a href="https://nextjs.org/docs/app/building-your-application/optimizing/metadata">https://nextjs.org/docs/app/building-your-application/optimizing/metadata</a></p> <p>...also doesn't clear up the issue. Pages like <code>not-found.tsx</code> aren't part of the hierarchy, I suppose.</p> </div> </div> <div class="comment"> <div class="user"> <a rel="noreferrer nofollow" target="_blank" href="https://github.com/marcelhageman"><img src="https://avatars.githubusercontent.com/u/3671221?v=4" />marcelhageman</a> commented <strong> 10 months ago</strong> </div> <div class="markdown-body"> <p>Meanwhile, to solve the issue of page titles on 404-pages, assuming you have:</p> <ul> <li>/[id] <ul> <li>/page.tsx</li> <li>/not-found.tsx</li> </ul></li> </ul> <p>Then your not-found metadata currently cannot come from your not-found.tsx file, instead, you are to put it into your page.tsx file:</p> <pre><code class="language-tsx">type Params = { id: string; }; const metadata: Metadata = { title: 'My page', description: 'Viewing a single page', }; export async function generateMetadata({ params, }: { params: Params; }): Promise<Metadata> { const exists = await pageExists(Number(params.id)); if (!exists) { return { title: 'Page not found', description: 'This page could not be found.', }; } return metadata; } export default async function Page({ params }: { params: Params }) { const exists = await pageExists(Number(params.id)); if (!exists) { notFound(); } return ( <p> Your page goes here. </p> ); }</code></pre> <p>My issue with this is that I want the not-found page to be exclusively responsible for deciding how a 404-situation would look and feel. Now I need to make two files responsible for it, and I even need to do <code>pageExists</code> in both the page component <em>and</em> the <code>generateMetadata</code> function. </p> <p>But at least it works :)</p> </div> </div> <div class="comment"> <div class="user"> <a rel="noreferrer nofollow" target="_blank" href="https://github.com/trevorblades"><img src="https://avatars.githubusercontent.com/u/1216917?v=4" />trevorblades</a> commented <strong> 6 months ago</strong> </div> <div class="markdown-body"> <blockquote> <p>It works when I load the page, briefly, but the page.tsx in the same folder overwrites the title immediately.</p> </blockquote> <p>I'm running into this issue now too, and it feels very unintuitive. I'd expect that when I call <code>notFound()</code> in a page, it's a signal to Next that this page should appear as nonexistent, and the entirety of my <code>not-found.tsx</code> route should render, metadata included.</p> <p>Having the underlying page title show up breaks that experience. Suppose I have a dynamic page that fetches some resource and checks to see if the current user should be able to view it. If they shouldn't, then I want to tell them that the resource doesn't exist by rendering our 404 page.</p> <p>If the name of the resource shows up in the page title, it feels like a leaking of information that the current user shouldn't have access to. Of course, I can do the additional check in the page's <code>generateMetadata</code> function as @marcelhageman suggests above, but that's definitely a hacky workaround to this unexpected behaviour.</p> <p>Edit: I figured out that I can get the <code>not-found.tsx</code> UI and metadata to render correctly by calling <code>notFound()</code> from within my <code>generateMetadata</code> function.</p> <pre><code class="language-ts">export const generateMetadata = async ({ params }: { params: id }): Promise<Metadata> => { const thing = await getThing(); if (!thing) { notFound(); } const { user } = await getCurrentUser(); if (!user || !hasAccess(user, thing)) { notFound(); } return { title: `Custom title for ${thing.name}` } }</code></pre> <p>However this also feels like a hack and unexpected behaviour. Just wanted to mention this here in case others need a workaround while this issue is being addressed.</p> </div> </div> <div class="comment"> <div class="user"> <a rel="noreferrer nofollow" target="_blank" href="https://github.com/shakibhasan09"><img src="https://avatars.githubusercontent.com/u/90200484?v=4" />shakibhasan09</a> commented <strong> 3 months ago</strong> </div> <div class="markdown-body"> <p>Any update so far?</p> <p><em>Edit by maintainer bot: Comment was <strong>automatically</strong> minimized because it was considered unhelpful. (If you think this was by mistake, let us know). Please only comment if it adds context to the issue. If you want to express that you have the same problem, use the upvote 👍 on the issue description or subscribe to the issue for updates. Thanks!</em></p> </div> </div> <div class="comment"> <div class="user"> <a rel="noreferrer nofollow" target="_blank" href="https://github.com/mobeigi"><img src="https://avatars.githubusercontent.com/u/5266113?v=4" />mobeigi</a> commented <strong> 2 months ago</strong> </div> <div class="markdown-body"> <p>This is my usecase and some notes on it:</p> <p>I have: <code>/app/not-found.tsx</code> <code>/app/blog/[...slug]/page.tsx</code></p> <p>When visiting: <a href="http://localhost:3000/blog/non-existant-page">http://localhost:3000/blog/non-existant-page</a></p> <p>The <code>/app/blog/[...slug]/page.tsx</code> route fires as expected.</p> <p><strong>/app/blog/[...slug]/page.tsx</strong></p> <pre><code class="language-tsx">export const generateMetadata = async ({ params }: { params: { slug: string[] } }): Promise<Metadata> => { const post = await getPostFromParams({ params }); if (!post) { console.warn('Failed to find post during generateMetadata.'); return { title: 'fallback'; }; } return { title: 'Blog!'; } } const BlogPostHandler = async ({ params }: { params: { slug: string[] } }) => { const post = await getPostFromParams({ params }); if (!post) { notFound(); return null; } //other logic }</code></pre> <p><strong>not-found.tsx:</strong></p> <pre><code class="language-tsx">import NotFoundPage from '@/containers/NotFoundPage'; import { Metadata } from 'next'; export const metadata: Metadata = { title: 'Not Found', }; const NotFound = () => <NotFoundPage />; export default NotFound;</code></pre> <p>The behaviour I observe is:</p> <ul> <li>title is originally <code>Not Found</code> which is the root layouts title (this is also what is present in the initial HTML)</li> <li>the title is then <code>fallback</code> which is what the <code>page.tsx</code> resolves to.</li> </ul> <p>Now if both your <code>page.tsx</code> and <code>not-found.tsx</code> page have the same meta data (i.e. static metadata object you return in both for the not found case) then this works pretty well. If not, there will be a change from either the layout meta data or <code>not-found.tsx</code> metadata to whatever the <code>page.tsx</code> metadata is.</p> <p>PS: You should probably use <code>notFound()</code> from inside the <code>generateMetadata</code>.</p> <p>It would be nice if we could export <code>metadata</code> as null or return <code>null</code> for <code>generateMetadata</code> to tell <code>next</code> to not use this metadata and fallback to the previously available meta data. </p> </div> </div> <div class="comment"> <div class="user"> <a rel="noreferrer nofollow" target="_blank" href="https://github.com/ArtemGrachov"><img src="https://avatars.githubusercontent.com/u/22654837?v=4" />ArtemGrachov</a> commented <strong> 1 month ago</strong> </div> <div class="markdown-body"> <p>Is it possible to set metadata for error.tsx file on SSR? I cannot find any information, generateMetadata function also does not work for client components (error.tsx forced to be client component).</p> </div> </div> <div class="comment"> <div class="user"> <a rel="noreferrer nofollow" target="_blank" href="https://github.com/Super-Kenil"><img src="https://avatars.githubusercontent.com/u/91980339?v=4" />Super-Kenil</a> commented <strong> 1 month ago</strong> </div> <div class="markdown-body"> <blockquote> <p>Is it possible to set metadata for error.tsx file on SSR? I cannot find any information, generateMetadata function also does not work for client components (error.tsx forced to be client component).</p> </blockquote> <p><strong>error.tsx</strong> is client only page, for now, there's no possible way to set <strong>metadata</strong> or even use <strong>generateMetaData</strong> to <strong>error.tsx</strong> page</p> <p>But you can use a hack like <a href="https://github.com/vercel/next.js/issues/45620#issuecomment-1827734000">this</a> </p> </div> </div> <div class="comment"> <div class="user"> <a rel="noreferrer nofollow" target="_blank" href="https://github.com/ArtemGrachov"><img src="https://avatars.githubusercontent.com/u/22654837?v=4" />ArtemGrachov</a> commented <strong> 1 month ago</strong> </div> <div class="markdown-body"> <p>@Super-Kenil Thanks for reply My goal was to keep at least default meta tags for error page in case if it is exposed to search engines. However, I noticed that error page has meta robots tag with "noindex" value, so the problem itself seems not actual from the beginning.</p> <p>It is logical that page with error must not be indexed, but still it is strange that there is no way to handle error page's meta tags on server side. All depends on project needs, it is not ok to just to remove such possibility at all.</p> </div> </div> <div class="page-bar-simple"> </div> <div class="footer"> <ul class="body"> <li>© <script> document.write(new Date().getFullYear()) </script> Githubissues.</li> <li>Githubissues is a development platform for aggregating issues.</li> </ul> </div> <script src="https://cdn.jsdelivr.net/npm/jquery@3.5.1/dist/jquery.min.js"></script> <script src="/githubissues/assets/js.js"></script> <script src="/githubissues/assets/markdown.js"></script> <script src="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.4.0/build/highlight.min.js"></script> <script src="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.4.0/build/languages/go.min.js"></script> <script> hljs.highlightAll(); </script> </body> </html>