afaqrashid.com  ·  portfolio · 2026 ed.

"Viewport Meta Element Was Not Specified" in Next.js: Tested

"A 'viewport' meta element was not specified." is a webhint message. Microsoft Edge runs webhint in DevTools, so it shows up in Edge's Issues panel, and in VS Code if you have the Edge Tools or webhint extension. It means the HTML document it checked has no <meta name="viewport"> in the head.

A normal Next.js page can't trigger it: Next.js adds a viewport tag to every App Router page by itself. So when you see it on a Next.js site, the response almost always came from somewhere other than a page. I built a Next.js 16.4.4 app, ran next build and next start, and counted the viewport tags in each kind of response, fetched as Edge and as Googlebot.

What each response contains

response                                  viewport tags in the HTML
app/plain/page.tsx (static metadata)      1  width=device-width, initial-scale=1
app/slow/page.tsx (slow generateMetadata) 1  same, in <head>
app/not-found (a 404)                     1  same
pages/legacy.tsx (Pages Router)           1  width=device-width   (no initial-scale)
app/raw/route.ts returning HTML           0
public/static.html                        0
page rendering its own <meta viewport>    2

Edge and Googlebot got the same viewport tags on every route. On the slow page the title was streamed after the head, but the viewport stayed in the head: Next.js doesn't stream it.

Cause 1: HTML from a route handler or the public folder

A route.ts that returns HTML and a file in public/ are sent exactly as you wrote them. Next.js doesn't touch them, so there is no default viewport tag. These are the two places I got "was not specified": a hand-written HTML response, and a static landing page dropped into public/.

// app/raw/route.ts
export function GET() {
  return new Response(
    `<!doctype html><html><head>
      <meta name="viewport" content="width=device-width, initial-scale=1">
      <title>raw</title>
    </head><body>raw</body></html>`,
    { headers: { "content-type": "text/html" } },
  );
}

Write the tag into the head yourself, as above. Do the same in any .html file in public/. If the HTML is a full page, it is usually simpler to make it a page.tsx and let Next.js add the tag.

Cause 2: Pages Router has no initial-scale

The Pages Router's default tag is width=device-width only. That isn't the "not specified" message, but webhint flags it with a sibling one: "The 'viewport' meta element 'content' attribute value should contain 'initial-scale=1'." (webhint only asks for initial-scale when you target old iOS Safari.) Set it once in _app with next/head:

// pages/_app.tsx
import type { AppProps } from "next/app";
import Head from "next/head";

export default function App({ Component, pageProps }: AppProps) {
  return (
    <>
      <Head>
        <meta name="viewport" content="width=device-width, initial-scale=1" />
      </Head>
      <Component {...pageProps} />
    </>
  );
}

In my build this replaced the default: the page had one tag, width=device-width, initial-scale=1. Put it in _app, not _document: Next.js warns against viewport tags in _document's Head.

Cause 3: your own tag on top of the default

In the App Router, rendering <meta name="viewport"> in a page or layout doesn't replace anything. React 19 hoists the tag into the head and Next.js still adds its own, so my test page had two identical tags. webhint reports that as "A 'viewport' meta element is not needed as one was already specified."

Delete the hand-written tag and use the viewport export instead. Anything you set there is merged into the single tag Next.js writes:

// app/fixed/page.tsx (or app/layout.tsx for every page)
import type { Viewport } from "next";

export const viewport: Viewport = {
  width: "device-width",
  initialScale: 1,
  viewportFit: "cover",
};
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover"/>

The other viewport messages

  • "…should not contain 'maximum-scale'" (or user-scalable, minimum-scale): remove them. They stop people zooming, and Lighthouse's accessibility audit fails a maximum-scale below 5 too.
  • "…should be specified in the '<head>', not '<body>'": the tag is in the body, usually from HTML injected by a CMS or a third-party script.
  • "…should contain 'width=device-width'": a fixed width such as width=1024 is set somewhere.
  • "Unsupported metadata viewport" in the Next.js build log is a different warning: viewport settings inside the metadata object. See the related post below.

Check what the browser actually got

DevTools' Elements panel shows the DOM after scripts run, so check the raw response instead. This prints every viewport tag in the HTML a URL returns:

curl -s https://example.com/some-page | grep -o '<meta name="viewport"[^>]*>'

No output means cause 1. Two lines mean cause 3. One line without initial-scale on a Pages Router site means cause 2.

Related

Sources