Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Multiple viewport meta tags #13230

Closed
zvictor opened this issue May 22, 2020 · 22 comments · Fixed by #13452
Closed

Multiple viewport meta tags #13230

zvictor opened this issue May 22, 2020 · 22 comments · Fixed by #13452
Labels
good first issue Easy to fix issues, good for newcomers

Comments

@zvictor
Copy link

zvictor commented May 22, 2020

Bug report

Describe the bug

Custom viewport meta tags in the _document.js page are ignored by the browser because next.js pushes it's own viewport meta tag without checking if any exists already. As a result, we end up with this:

<head>
  ...
  <meta name="viewport" content="viewport-fit=cover"/> <!-- ignored 🤕 -->
  ...
  <meta name="viewport" content="width=device-width" />
  ...
</head>

I could trace the problem down to this part of the code:

https://github.com/zeit/next.js/blob/86160a5190c50ea315c7ba91d77dfb51c42bc65f/packages/next/next-server/lib/head.tsx#L13-L15

To Reproduce

  1. Define a viewport meta tag in the _document.js page.
  2. Open any page of your app and look for <meta name="viewport"

I also made a full reproducible example, but I faced issues with Codesandbox. If you decide to run the reproducible, please download the source code and run it on your machine instead of the online sandbox.

Screenshots

image

System information

  • OS: macOS
  • Browser: tested in chrome, safari and firefox
  • Version of Next.js: 9.4.2
  • Version of Node.js: 12.16.3
@timneutkens timneutkens added good first issue Easy to fix issues, good for newcomers help wanted labels May 25, 2020
@TyMick
Copy link
Contributor

TyMick commented May 25, 2020

Taking a stab at this one! My apologies if I'm over-documenting my investigation process. 🙈

I don't think the defaultHead function @zvictor mentioned...

https://github.com/zeit/next.js/blob/91b1548d3296dae4306ad68b97fd61d092c06ebe/packages/next/next-server/lib/head.tsx#L11-L17

...is the problem, because farther down, after these default head elements are added to the page's other head elements, duplicates are filtered out (e.g., only the first <meta name="viewport" element in the list should make it through).

Downloading @zvictor's example, I found that if I comment out the meta tag in _document.js (where { Head } is imported from "next/document")...

<Head>
  {/* <meta name="viewport" content="viewport-fit=cover" /> */}
</Head>

...and add it in a new Head component in index.js (importing Head from "next/head")...

export default () => (
  <>
    <Head>
      <meta name="viewport" content="viewport-fit=cover" />
    </Head>
    <Viewport />
    <Environment />
  </>
)

...it works correctly! Only the correct viewport element shows up on the page.

So the problem has to lie in "next/document" somewhere. I'll head there next!

@TyMick
Copy link
Contributor

TyMick commented May 26, 2020

Alright, in _document.tsx's Head component, both <meta name="viewport"s are being inserted into the <head> (among several other things) here:

<head {...this.props}>
  ...
  {children}
  {head}
  ...
</head>
  • <meta name="viewport" content="viewport-fit=cover" /> (the user-entered one) is part of children
    • children is this.props.children
  • <meta name="viewport" content="width=device-width" /> (the default one) is part of head
    • head comes from this.context._documentProps

I'm losing the trail a bit trying to track backwards, but I think this.context._documentProps ultimately comes from the individual page's headers as determined by next-server/lib/head.tsx, which includes the "next/head" export. So I'm guessing we can't just call those methods from head.tsx directly to deduplicate these meta tags or else we'd run into some circular dependencies or whatever that would be called.

As far as how to fix this... Is this something that should only be done in _app.js using "next/head"? We already warn users not to put a title tag in their _document.js because it leads to unexpected results.

if (child?.type === 'title' && !isReactHelmet) {
console.warn(
"Warning: <title> should not be used in _document.js's <Head>. https://err.sh/next.js/no-document-title"
)
}

I'm afraid I still haven't quite wrapped my head around the difference between _app.js and _document.js...

So which way should I move forward:

  1. Add an additional console warning not to include viewport meta tags in _document.js.
    • It may be best to discourage this anyway, if Document is only rendered in the server as the docs say. But is it really only rendered in the server if head elements added in _document.js show up in the client?
  2. Add some similar deduplicating functionality to _document.tsx to merge children (user-entered head elements from _document.js) and head (default head elements and user-entered head elements from _app.js or any individual page) before inserting.

Wait a minute, though: I just realized that if I do #‌2, user-entered head elements in _document.js will override matching user-entered head elements from _app.js or individual pages... Should adding children to <Head> in _document.js even be allowed?! I'm going to go compose my thoughts and probably ask the question over in Discussions.

@TyMick
Copy link
Contributor

TyMick commented May 26, 2020

I guess I'm not the only one musing about this: #12290

@TyMick
Copy link
Contributor

TyMick commented May 26, 2020

Alrighty, just started a dedicated discussion #13408. Its thesis: should we discourage adding any children to next/document's Head like we're currently discouraging adding <title> tags to _document.js?

@TyMick
Copy link
Contributor

TyMick commented May 26, 2020

But is it really only rendered in the server if head elements added in _document.js show up in the client?

Now I know what "render" actually means (or at least what it doesn't). 🤦🏼‍♂️ Learning! ✊🏼

@TyMick
Copy link
Contributor

TyMick commented May 26, 2020

Alright, I take from the response to #13408 that <meta name="viewport" content="viewport-fit=cover" /> isn't a weird thing to add to _document.js, after all. I'll get to brainstorming how to deduplicate cleanly.

@TyMick
Copy link
Contributor

TyMick commented May 27, 2020

...And as per further discussion in #13408, I'm now working on a PR to warn users when they add a viewport tag to next/document's <Head>, as viewport is supposed to be handled by next/head. 👍🏼

@zvictor
Copy link
Author

zvictor commented Jul 22, 2020

I would like to thank all the collaborators involved in solving this issue and releasing it in the new versions, in special @tywmick. Even though it doesn't seem that the issue was affecting him directly, he has put a lot of effort into investigating the issue and studying all possible solutions, not stopping at the first easy fix for it. Great job! 🎉

Open-source communities keep amazing me ❤️


I can confirm that I now (v9.4.5-canary.42) see the warning message in my app, as expected:

Warning: viewport meta tags should not be used in _document.js's . https://err.sh/next.js/no-document-viewport-meta

The only problem I found is that the error url is actually redirecting to a 404 destination.

@timneutkens
Copy link
Member

timneutkens commented Jul 23, 2020

The only problem I found is that the error url is actually redirecting to a 404 destination.

This is because the branch was not released yet, when it's on stable it will link correctly.

@nickatrokit
Copy link

Ok so where are we supposed to put our viewport tags? The warning without a solution is not very useful.

@timneutkens
Copy link
Member

Warning: viewport meta tags should not be used in _document.js's . https://err.sh/next.js/no-document-viewport-meta

@nickatrokit did you open the link attached to the warning, it says to put it in _app.

@nickatrokit
Copy link

@timneutkens So where is the proper place to put site-wide metadata tags? In _document.tsx or _app.tsx?

@chrishrtmn
Copy link

chrishrtmn commented Aug 5, 2020

@timneutkens So where is the proper place to put site-wide metadata tags? In _document.tsx or _app.tsx?

_app.js/_app.tsx

@nickatrokit
Copy link

@chrishrtmn This seems like an odd place to put them since developers like me are used to putting them next to the html tag which is in _document. So I think most people would start by putting them in _document.

@cyrilchapon
Copy link

cyrilchapon commented Nov 14, 2020

For those like me who had custom components for head's children in App; I found a pattern that doesn't change much code, and is very clean => use arrays inside a wrapping function.

Before :

const ViewportMetaLink = () => (
  <meta
    name='viewport'
    content='minimum-scale=1, initial-scale=1, width=device-width'
    key='viewport-meta'
  />
)

const SansSerifFontLink = () => (
  <link
    rel='stylesheet'
    href={FONTS.sansSerif.link}
    key='sans-serif-font-link'
  />
)

const App = ({ Component, pageProps }: AppProps) => (
  <>
    <Head>
      <ViewportMetaLink />
      <SansSerifFontLink />
    </Head>

    <div />
  </>
)

After :

const ViewportMetaLink = () => (
  <meta
    name='viewport'
    content='minimum-scale=1, initial-scale=1, width=device-width'
    key='viewport-meta'
  />
)

const SansSerifFontLink = () => (
  <link
    rel='stylesheet'
    href={FONTS.sansSerif.link}
    key='sans-serif-font-link'
  />
)

const links = () => ([ViewportMetaLink(), SansSerifFontLink()])

const App = ({ Component, pageProps }: AppProps) => (
  <>
    <Head>
      {links()}
    </Head>

    <div />
  </>
)

@willemmulder
Copy link

So I got this warning "viewport meta tags should not be used in _document.js's " yesterday, but even after reading through this thread I do not at all understand why it is bad practice to put viewport meta tags in the in _document.

For me it would be really useful if the documentation on _app and _document would be expanded to cover the differences between the two and the reasoning why we should put things like viewport metatags in either of those. Or is that documentation available and I'm just glaring over it...?

@TyMick
Copy link
Contributor

TyMick commented Dec 1, 2020

Copying what I wrote on #13408 in case someone who ends up here hasn't been there:

@willemmulder You're right, that does seem to violate the breakdown @‌lfades gave at the beginning of this thread. 🤔 The only reason I can see right now for the current behavior is because document/head doesn't currently have any deduplicating functionality like next/head, so it isn't able to override the default viewport meta tag.

If document/head were given the ability to override just the default viewport, that would solve one instance of multiple viewport tags (#13230) but allow for other instances of multiple viewport tags whenever a developer adds one to document/head and to next/head somewhere else. So a warning is probably necessary somewhere, but come to think of it, maybe the latter situation the better one to warn about.

It would take some extra work, but if we let document/head override just the default viewport, and then adjusted next/head so that it can override the default but can't override a viewport added to document/head, then Next.js could just warn people when they define multiple viewport tags themselves (in both document/head and next/head) instead of just warning them when they define a single viewport tag in a difficult place (document/head). A developer adding two of their own viewport tags does seem like the bigger blunder to me.

Now I'm intrigued. I'll open up a new issue (#19720) to see if anyone else thinks this is a good idea. 👍🏼

(More discussion happening there)

@xyy94813
Copy link

@Timer
It's weird, children of Document.Head should be render after the default head.

function reduceComponents(
headElements: Array<React.ReactElement<any>>,
props: WithInAmpMode
) {
return headElements
.reduce(
(list: React.ReactChild[], headElement: React.ReactElement<any>) => {
const headElementChildren = React.Children.toArray(
headElement.props.children
)
return list.concat(headElementChildren)
},
[]
)
.reduce(onlyReactElement, [])
.reverse()
.concat(defaultHead(props.inAmpMode))
.filter(unique())
.reverse()
.map((c: React.ReactElement<any>, i: number) => {
const key = c.key || i
return React.cloneElement(c, { key })
})
}

@genox
Copy link

genox commented Jan 11, 2021

For reference to others who might experience the same issue and try to google and don't find anything ---

Following the warning's advice, I moved the viewport meta tag to _app.js and wrapped it in <Head>. This caused theme-ui / emotion to report a missing/undefined styles property and the app doesn't load. This is the error message:

Unhandled Runtime Error

TypeError: can't access property "styles", _this$context4 is null
Call Stack
render
webpack-internal:///./node_modules/next/dist/pages/_document.js (434:20)
finishClassComponent
webpack-internal:///./node_modules/react-dom/cjs/react-dom.development.js (17485:31)
updateClassComponent
webpack-internal:///./node_modules/react-dom/cjs/react-dom.development.js (17435:44)
beginWork
webpack-internal:///./node_modules/react-dom/cjs/react-dom.development.js (19073:16)
callCallback
webpack-internal:///./node_modules/react-dom/cjs/react-dom.development.js (3945:14)
invokeGuardedCallbackDev
webpack-internal:///./node_modules/react-dom/cjs/react-dom.development.js (3994:16)
invokeGuardedCallback
webpack-internal:///./node_modules/react-dom/cjs/react-dom.development.js (4056:31)
beginWork$1
webpack-internal:///./node_modules/react-dom/cjs/react-dom.development.js (23959:28)
performUnitOfWork
webpack-internal:///./node_modules/react-dom/cjs/react-dom.development.js (22771:12)
workLoopSync
webpack-internal:///./node_modules/react-dom/cjs/react-dom.development.js (22702:22)
renderRootSync
webpack-internal:///./node_modules/react-dom/cjs/react-dom.development.js (22665:7)
performSyncWorkOnRoot
webpack-internal:///./node_modules/react-dom/cjs/react-dom.development.js (22288:18)
flushSyncCallbackQueueImpl/<
webpack-internal:///./node_modules/react-dom/cjs/react-dom.development.js (11327:26)
unstable_runWithPriority
webpack-internal:///./node_modules/scheduler/cjs/scheduler.development.js (646:12)
runWithPriority$1
webpack-internal:///./node_modules/react-dom/cjs/react-dom.development.js (11276:10)
flushSyncCallbackQueueImpl
webpack-internal:///./node_modules/react-dom/cjs/react-dom.development.js (11322:26)
flushSyncCallbackQueue
webpack-internal:///./node_modules/react-dom/cjs/react-dom.development.js (11309:3)
flushSync
webpack-internal:///./node_modules/react-dom/cjs/react-dom.development.js (22462:7)
scheduleRefresh
webpack-internal:///./node_modules/react-dom/cjs/react-dom.development.js (24424:14)
performReactRefresh/<
webpack-internal:///../../node_modules/react-refresh/cjs/react-refresh-runtime.development.js (304:17)
performReactRefresh
webpack-internal:///../../node_modules/react-refresh/cjs/react-refresh-runtime.development.js (293:26)
scheduleUpdate/<
webpack-internal:///../../node_modules/@next/react-refresh-utils/internal/helpers.js (124:35)
setTimeout handler*scheduleUpdate
webpack-internal:///../../node_modules/@next/react-refresh-utils/internal/helpers.js (119:15)
<unknown>
webpack-internal:///./src/pages/_app.tsx (255:43)
<unknown>
webpack-internal:///./src/pages/_app.tsx (271:30)
./src/pages/_app.tsx
file:/xxx/packages/nextjs/.next/static/webpack/pages/_app.97d42fadd87eaf2a8b4f.hot-update.js (188:1)
__webpack_require__
/_next/static/chunks/webpack.js (873:31)
hotApplyInternal
file:/xxx/packages/nextjs/.next/static/chunks/webpack.js (750:33)
hotApply
/_next/static/chunks/webpack.js (412:19)
hotUpdateDownloaded/<
file:/xxx/packages/nextjs/.next/static/chunks/webpack.js (387:22)
promise callback*hotUpdateDownloaded
/_next/static/chunks/webpack.js (386:15)
hotAddUpdateChunk
file:/xxx/packages/nextjs/.next/static/chunks/webpack.js (362:13)
<unknown>
webpackHotUpdateCallback@http://localhost:3000/_next/static/chunks/webpack.js?ts=1610383976102 (58:29)
<unknown>
/_next/static/webpack/pages/_app.97d42fadd87eaf2a8b4f.hot-update.js (1:21)

The position, wrapped within <ThemeProvider>, other provider components like a store or at the top-level of the component, doesn't matter. My best guess is that this might interfere with regular React contexts but I'm not eager to go down that rabbit hole for now.

However, I think this poses some food for thought. While I greatly appreciate that NextJS takes care of a lot things, I'm not sure it should assume anything about the presentational layer. Defaults, yes, if it makes examples easier without requiring boiler plate, that sure makes things look simple and easy! But give us the options to switch things off: Don't assume that a viewport meta tag is fine for everyone else, too. Or then establish a default that covers more ground so we can forget about managing it altogether, like (random link that covers viewport meta tag..) https://mobiforge.com/design-development/meta-viewport-best-practice

@DimitarNestorov
Copy link

For reference to others who might experience the same issue and try to google and don't find anything ---

Following the warning's advice, I moved the viewport meta tag to _app.js and wrapped it in <Head>. This caused theme-ui / emotion to report a missing/undefined styles property and the app doesn't load. This is the error message:

Unhandled Runtime Error

TypeError: can't access property "styles", _this$context4 is null
Call Stack
// ...

@genox You faced this issue because you used import { Head } from "next/document" instead of import Head from "next/head"

@genox
Copy link

genox commented Feb 25, 2021

@genox You faced this issue because you used import { Head } from "next/document" instead of import Head from "next/head"

@DimitarNestorov That was the problem, yes. Note to self: read properly.

@balazsorban44
Copy link
Member

This issue has been automatically locked due to no recent activity. If you are running into a similar issue, please create a new issue with the steps to reproduce. Thank you.

@vercel vercel locked as resolved and limited conversation to collaborators Jan 28, 2022
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
good first issue Easy to fix issues, good for newcomers
Projects
None yet
Development

Successfully merging a pull request may close this issue.

13 participants