vercel / next.js

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

Duplicate meta tags when using Head both in custom document and page #9794

Closed Manc closed 4 years ago

Manc commented 4 years ago

Bug report

Edit +++ ON HOLD I just had a lightbulb moment. I will post my discovery/misunderstanding in the next few hours. This ticket can probably be closed then, but I have to test my new theory first…

Describe the bug

Multiple (duplicate) meta tags such as charSet and viewport are rendered in <head> section of the HTML output when the <Head> component is used in both a custom _document and a page file.

Even relying on the default injection of <meta charSet="utf-8"/> (i.e. not specifying that tag at all) results in having two of the same tags in the output HTML, one from Document and one from the page.

I also end up with two viewport meta tags, one from Document and one from the page.

To Reproduce

I have created (failing) tests here: https://github.com/Manc/next.js/tree/test-head-document

  1. Clone repository git@github.com:Manc/next.js.git
  2. Check out branch test-head-document
  3. yarn install
  4. yarn testonly --testPathPattern "app-document" -t "It dedupes head tags with the same key"

Expected behavior

I expect the meta tags with the same key to override the meta tags from the custom Document (_document).

I expect only to have one charset and one viewport tag in the whole rendered HTML.

System information

Other

I'm pretty new to Next.js and don't really know how it all works yet. I hope the test I provided helps somebody more experienced to fix this or maybe give me a hint. Thanks!

My suspicion is that the Head component of the Document (import { Head } from 'next/document') and the Head component used in the page (import Head from 'next/head') might be completely separate and don't "communicate" with each other, each doing their own thing twice.

xiaokyo commented 4 years ago

I had the same problem

Manc commented 4 years ago

OK, here we go… As suspected earlier I finally worked it out.

Apparently, the correct way of using the two different (!) Head components is to prepare the meta tags etc. only in the page component or other components, but not in the _document.js file. In the _document only include it from next/document like <Head /> without any children.

To clarify…

// pages/_document.tsx

import Document, { Head, Main, NextScript } from 'next/document'; // We import `Head` from `next/document`!

class MyDocument extends Document {
    public render() {
        return (
            <html lang="en">
                <Head /> // Do not add any children here!
                <body>
                    <Main />
                    <NextScript />
                </body>
            </html>
        );
    }
}

export default MyDocument;
// pages/examplepage.tsx

import React from 'react';
import Head from 'next/head'; // We import `Head` from `next/head `!
import Layout from '../components/layout';

class ExamplePage extends React.Component {
    render() {
        return (
            <Layout>
                <Head>
                    <title>Example page</title>
                    <meta name="description" content="Example page description" />
                </Head>

                <h1>Example page</h1>
            </Layout>
        );
    }
}

export default ExamplePage;

And then in the Layout component you can define default meta tags and headers, but not in the _document itself.

I was used to the way react-helmet works. There you can set defaults at top level and override/add values in children, but you always use the same component. Here we use two different components with the same name, which, I guess, caused the confusion.

gothy commented 4 years ago

@Manc why have you closed this issue? It's clearly a bug.

_document.js is the place to specify the defaults for the whole site like charset and viewport.

Now we've got them duplicated with "defaults" later. Even using key prop like in this test scenario https://github.com/zeit/next.js/blob/18a9c7e371efc4c487f9c3599c3211ce30009d6c/test/integration/client-navigation/pages/head-duplicate-default-keys.js

doesn't help. In my case, I've got two viewport tags, first one is mine

<meta name="viewport" content="width=device-width, initial-scale=1"/>

defined in _document.js using next/head and then the default one later

<meta name="viewport" content="width=device-width,minimum-scale=1,initial-scale=1"/>

The same with charSet. Please, reopen this issue, maybe we'll get an explanation.

Manc commented 4 years ago

@gothy You might be right, it's probably best to get the opinion of the Next.js team, what the intended usage is.

If we are supposed to define defaults in the _document.js, which makes sense, then it looks like a bug indeed. If one is supposed to prepare all the head tags outside _document.js (as described in my previous comment), then it should be made clearer and maybe the Head component of next/head should not even accept children.

timneutkens commented 4 years ago

next/head should be used in _app / pages.

_document is only for the initial document HTML and if you're going to override on the page level it won't work as we simply won't know about it on the client-side as _document is server-side only.

gothy commented 4 years ago

@timneutkens thanks for clarifying!

I had the impression, _document.js should contain all of the basic\shared\non-dynamic things like charset, viewport, manifest, icons, etc.

If I move the <meta charSet="utf-8" /> to the _app.js, I'm getting

Error: A charset attribute on a meta element found after the first 1024 bytes.

from validator.w3.org . I'm not sure if that's super bad, but it's not clear how can I avoid this without putting this tag first inside the _document.js <Head> since there's a huge list of i18n strings is being embedded in _document.js. Anything listed in _app.js is serialized after initial portion during SSR.

I've seen this PR on deprecating next/head. It would be great if the new take on this static\dynamic head handling will take W3 validation into account.

And thanks for Next 🚀😀

timneutkens commented 4 years ago

As said _document is for the initial html, so head elements that never change are fine there. In general you don’t need to add a charset as next comes with UTF-8 by default.

gothy commented 4 years ago

Yes. The (W3 validator) issue arises only when you've got something big serialized into the head in _document.js, since charset meta tag always comes after that.

Manc commented 4 years ago

My experience was, if I remember correctly now, as soon as I start adding defaults in the _document head, I end up with duplicate meta tags.

@gothy I also wanted to follow the recommendation to set charSet as far at the top as possible. This does not seem to be guaranteed if you rely on Next's defaults.

My solution (workaround?) is, as described above, do not set any defaults in the _document head, only in pages etc. For example, I use a Layout component, used by all pages and I set my default meta tags there and override them in individual pages.

timneutkens commented 4 years ago

@Manc yeah that's correct, defaults in _document is fine, if you want to override however you need everything available client-side so that we can actually dedupe and recalculate the items.

timneutkens commented 4 years ago

I'll close the issue now that it's cleared up.

rscharfer commented 4 years ago

I think this is still a problem. We have to have some things in our document.js <Head> which will "push down" all other tags in the <head>. The result is that the charSet meta is too far down the document according to things like W3 validator. There is no solution for this currently. As we have seen, putting the meta on top of the mandatory scripts in document.js is not a solution.

nicoqh commented 4 years ago

Adding an example of what @rscharfer is referring to:

_document.js

<Html>
  <Head>
    <meta charSet="utf-8" /> {/* Trying to add this at the very top of <head> */}
    {/* scripts, RSS feed etc. */}
  </Head>
</Html>

This will lead to duplicate meta charset tags.

And without adding the tag explicitly, Next will add it after your custom tags, which in this example would be way too far down. Adding the tag in a page component doesn't work either; Next will append it.

nicklouloudakis commented 4 years ago

Is there a way to prevent the extra addition of the charSet meta tag in the lib/head.js file, even explicitly (for instance, by using a flag)? In general, its usage is problematic: the tag is inserted way too low in the head, therefore causing site validation to throw errors. This seems to be problematic, even if head is added to the _app.js file.

hk86 commented 4 years ago

I have the same issue. I set my meta viewport in _document.js. Still a second meta viewport is added:

<meta name="viewport" content="width=device-width"/>

I do not set it myself.

craighillwood commented 4 years ago

A solution I found for this was to leave the _document as is, but add the viewport metadata in a next/head component inside each page. This will cause the "page" viewport metadata to override the default one set by _document and leaves the charset metadata (set by _document) at the very top.

I ended up creating a custom Head component (that uses next/head) which already sets the viewport metadata (so I don't have to repeat it on every page).

I'm using the SSG of Next.js, so maybe this doesn't apply for SSR pages, don't know. Hope it helps 👍

switz commented 4 years ago

Also seeing a duplicate meta viewport overriding the one I specify:

image
consciousnessdev commented 4 years ago

A solution I found for this was to leave the _document as is, but add the viewport metadata in a next/head component inside each page. This will cause the "page" viewport metadata to override the default one set by _document and leaves the charset metadata (set by _document) at the very top.

I ended up creating a custom Head component (that uses next/head) which already sets the viewport metadata (so I don't have to repeat it on every page).

I'm using the SSG of Next.js, so maybe this doesn't apply for SSR pages, don't know. Hope it helps +1

Do you also use redux? because, i'd same problem when i declare next/head in pages/index.js it not show meta, but when i placing it in withReduxStore (custom wrapper for redux store like with-redux-store.js in this link : https://github.com/vercel/next.js/issues/8240), it generate double meta tag on detail page.

norfish commented 4 years ago

maybe next/head should not have the default meta, like viewport and charset

as overwrite in every page is not a good idea

TriStarGod commented 4 years ago

I keep seeing Next/Head with meta tags and style sheet refs in both _document and _app files in nextjs examples. Are there any advantages in setting it in _document?

sbstnjvf commented 4 years ago

I have the same issue. I set my meta viewport in _document.js. Still a second meta viewport is added:

<meta name="viewport" content="width=device-width"/>

I do not set it myself.

Also getting this. Did you find a fix @switz or @hk86?

nevnein commented 4 years ago

Either this should be reopened, or the examples should be cleared up: i.e. the Google Analytics example has script tags in the <Head> component of the _document, but doing so puts the aforementioned scripts before the charset declaration, and as noted in this issue there is with no way to override this without duplication.

So either offer a way to override, or document explicitly that you should avoid putting scripts (or anything actually) in the <Head> of the _document if you want it to come after the charset declaration (which should be in the first 1024 bytes).

Of course I'd rather have the override, as it solves even the viewport meta tag problem.

hk86 commented 4 years ago

I haven't found a fix. More a workaround. I set

import Head from 'next/head';
<Head>
  <meta key="viewport" name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0, viewport-fit=cover" />
</Head>   

at _app. If I set it at _document

import { Head } from 'next/document';
<Head>
  <meta key="viewport" name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0, viewport-fit=cover" />
</Head>   

I end up with a second viewport:

<meta key="viewport" name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0, viewport-fit=cover" />
<meta name="viewport" content="width=device-width"/>
valse commented 4 years ago

Either this should be reopened, or the examples should be cleared up: i.e. the Google Analytics example has script tags in the <Head> component of the _document, but doing so puts the aforementioned scripts before the charset declaration, and as noted in this issue there is with no way to override this without duplication.

So either offer a way to override, or document explicitly that you should avoid putting scripts (or anything actually) in the of the _document if you want it to come after the charset declaration (which should be in the first 1024 bytes).

Of course I'd rather have the override, as it solves even the viewport meta tag problem.

You're right and I'm putting head scripts in my Layout component with next/head

jaybytez commented 4 years ago

I updated on this issue: https://github.com/vercel/next.js/discussions/17020

We have this issue with our TMS. And realized anytime we have a script that does a document.head.append, a meta-tag gets duplicated in the Head. If we just add a script to our Layout, which basically does a document.createElement and then document.head.append, a meta-tag gets duplicated. If I add multiple calls to this, then multiple meta-tags get duplicated. The interesting part, is I am not even adding meta-tags to the Head, I am adding div tags to the head and it just randomly duplicates a meta-tag.

ghost commented 3 years ago

Hi guys , who is familiar with Head next js and know how all entries should have a key or name to prevent 2 entries.(avoid duplicating tags).

ghost commented 3 years ago

![Uploading image.png…]()

offero commented 3 years ago

I'm here trying to figure out how to specify these tags and I'm still confused.

gremo commented 3 years ago

Why the issue was closed? Is the default behavior? I't documented? For me _document.js was the right place to look for and put this kind of stuff, but as noticed... we get duplicate <meta> tag. Putting in _app.js solves the issue.

// _document.js
import Document, { Head, Html, Main, NextScript } from 'next/document';
import classNames from 'classnames';

export default class extends Document {
  static async getInitialProps(ctx) {
    const initialProps = await Document.getInitialProps(ctx);

    return { ...initialProps };
  }

  render() {
    return (
      <Html>
        <Head />
        <body className={classNames({
          'debug-screens': 'development' === process.env.NODE_ENV,
        })}>
          <Main />
          <NextScript />
        </body>
      </Html>
    );
  }
}
// _app.js
/* eslint-disable react/prop-types */
import '../styles/globals.css'
import Head from 'next/head';

export default function App({ Component, pageProps }) {
  return (
    <>
      <Head>
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
      </Head>
      <Component {...pageProps} />
    </>
  );
}
KODerFunk commented 3 years ago

@hk86, @gremo this is due to the fact that defaultHead() function generates a default set of meta-tags, and if you trace its use, it turns out that these tags will be overlapped only by meta-tags located in AppContainer >>> HeadManagerContext.Provider context.

And unfortunately, this applies only to next/head, not to { Head } from 'next/document'.

chozzz commented 3 years ago

The way I solved this is, I created a CustomHead extending head from next/document and use it in my _document.tsx.

DocumentHead.tsx

import React from "react";
import { Head } from "next/document";

function dedupeHead(elems: React.ReactElement<any, string>[]) {
  const result: React.ReactElement[] = [],
    mapper: { [key: string]: number } = {};

  if (elems) {
    for (let i = 0, len = elems.length; i < len; i++) {
      const elem = elems[i],
        type = elem.type,
        props = elem.props;

      /** Dedupe */
      switch (type) {
        case "meta":
          let key = props.itemprop || props.property || props.name;

          if (key) {
            if (Object.prototype.hasOwnProperty.call(mapper, key)) {
              // This meta is already in the result,
              // Replace the one in result with this
              const dupeMetaIdx = mapper[key];
              result[dupeMetaIdx] = elem;
            } else {
              // Save the elem's index in result.
              mapper[key] = result.push(elem) - 1;
            }
          } else {
            // Do not handle deduping for unknown case
            result.push(elem);
          }

          break;
        default:
          // Do not handle deduping for unknown case
          result.push(elem);
          break;
      }
    }
  }

  return result;
}

class DocumentHead extends Head {
  render() {
    this.context.head = dedupeHead(this.context.head);
    return super.render();
  }
}

export default DocumentHead;
kiril-daskalov commented 3 years ago

Up

seanislegend commented 3 years ago

I've also struggled with this one this week, and couldn't find a solution that was satisfactory. @chozzz's comment led me to what I was looking for, but I simplified the custom head to only remove the duplicate charset since that is was the only duplicated tag.

In _document:

class DocumentHead extends Head {
    render() {
        this.context.head = this.context.head.filter(item => !item.props?.charSet);
        return super.render();
    }
}

Use this DocumentHead component instead of Next's Head component (the next/document version, of course). From here you simply need to add the charset tag as the first child of DocumentHead and this ensures it appears as close to the document start as possible:

render() {
        return (
            <Html lang="en">
                <DocumentHead>
                    <meta charSet="utf-8" />
                    ... etc

It seems clear that either charset should not be provided by default or there should be support for opting out of using it.

jcubic commented 2 years ago

If this is not a bug, please document at least how to use the library. Right now you can't set <meta charset/> as the first tag.

Why don't add a flag to make Next.js don't include any default meta tags?

The only problematic code is this:

export function defaultHead(inAmpMode = false): JSX.Element[] {
  const head = [<meta charSet="utf-8" />]
  if (!inAmpMode) {
    head.push(<meta name="viewport" content="width=device-width" />)
  }
  return head
}

While searching the repo found this comment:

/**
 * This component injects elements to `<head>` of your page.
 * To avoid duplicated `tags` in `<head>` you can use the `key` property, which will make sure every tag is only rendered once.
 */

But this in _document doesn't work:

<meta charSet="utf-8" key="charset" />

You still get duplicated charsSet.

chozzz commented 2 years ago

As of next.js 12 - I no longer use my solution above. Adding key props to the meta tags solve the issue for me.

andiemmadavieswilcox commented 2 years ago

@chozzz, same here, doing the following (specifying the key prop) in Next.js 12 has fixed it:

Code screenshot

Dom screenshot