Thanks to visit codestin.com
Credit goes to github.com

Skip to content

🧁 When generateMetadata is async, nextjs puts metadata in the body on first render. #78688

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

Closed
bschmeisser-stytch opened this issue Apr 29, 2025 · 1 comment
Labels
Headers Related to the async headers() function.

Comments

@bschmeisser-stytch
Copy link

bschmeisser-stytch commented Apr 29, 2025

πŸ“¦ Link to the code that reproduces this issue

https://github.com/bschmeisser-stytch/cupcake-auth-nextjs

πŸ—οΈ To Reproduce

  1. Install the tamper monkey monitoring script (below)
  2. Clone the repo provided
  3. npm run dev
  4. Open chrome, devtools and filter the console for META CHECK βœ…
  5. Load the nextjs app
  6. See the meta tags are placed in the body not the head

πŸ€” Current vs. Expected behavior

  • Meta tags in the body create a lower SEO score in Lighthouse
  • Expected: All meta tags should be placed in the head tag
  • Actual: Async generateMetadata are placed in the body tag

πŸ’» Provide environment information

Node.js v22.13.0

Operating System:
  Platform: darwin
  Arch: arm64
  Version: Darwin Kernel Version 24.4.0: Fri Apr 11 18:33:40 PDT 2025; root:xnu-11417.101.15~117/RELEASE_ARM64_T6031
  Available memory (MB): 36864
  Available CPU cores: 14
Binaries:
  Node: 22.13.0
  npm: 10.9.2
  Yarn: 1.22.22
  pnpm: N/A
Relevant Packages:
  next: 15.3.1 // Latest available version is detected (15.3.1).
  eslint-config-next: 15.3.1
  react: 19.1.0
  react-dom: 19.1.0
  typescript: 5.8.3
Next.js Config:
  output: N/A

🐈 Which area(s) are affected? (Select all that apply)

Headers

🦌 Which stage(s) are affected? (Select all that apply)

next dev (local), next build (local), next start (local)

🐡 Additional context

🍌 Local Fix

I was able to fix this internally by removing all async in the method. I was able to make an async call in the background and cache the values. For example:

let cachedResult: SEOMetaData | undefined = undefined;
let isLoading = false;

const fetchSeoMetaData = (): SEOMetaData | undefined => {
  // Return cached result if available
  if (cachedResult) return cachedResult;

  // If not already loading, start fetching
  if (!isLoading) {
    isLoading = true;
    executeQuery(someQuery).then(
      (result) => {
        cachedResult = result ?? undefined;
        isLoading = false;
      },
      () => {
        isLoading = false;
      },
    );
  }

  // Return undefined until data is available
  return undefined;
};

🐡 Here's the tamper monkey script.

// ==UserScript==
// @name         Meta Tag Monitor
// @namespace    http://tampermonkey.net/
// @version      1.0
// @description  Monitors meta tags for changes, location, and value
// @author       You
// @match        *://*/*
// @grant        none
// ==/UserScript==

(function () {
  'use strict';

  let previousMetaData = new Map();

  const internalLog = (message) => {
    console.log(`META CHECK βœ…: ${message}`);
  };

  function getMetaKey(meta) {
    return meta.getAttribute('name') || meta.getAttribute('property') || meta.getAttribute('charset') || meta.getAttribute('http-equiv') || 'unknown';
  }

  function getMetaValue(meta) {
    return meta.getAttribute('content') || meta.getAttribute('charset') || '';
  }

  function getLocation(meta) {
    return (document.head.contains(meta)) ? '🐡head🐡' : '🦾body🦾';
  }

  function scanMetaTags() {
    const metaTags = document.querySelectorAll('meta');
    const currentMetaData = new Map();

    metaTags.forEach(meta => {
      const key = getMetaKey(meta);
      const value = getMetaValue(meta);
      const location = getLocation(meta);
      const identifier = key + '|' + location;

      currentMetaData.set(identifier, value);

      if (!previousMetaData.has(identifier)) {
        internalLog(`[${key}](${location}) πŸ“¦ NEW πŸ“¦`);
      } else if (previousMetaData.get(identifier) !== value) {
        internalLog(`[${key}](${location}) πŸ‘· UPDATED πŸ‘·`);
      }
    });

    // Check for deletions or movements
    previousMetaData.forEach((value, identifier) => {
      if (!currentMetaData.has(identifier)) {
        const [key, location] = identifier.split('|');
        const otherLocation = location === 'head' ? 'body' : 'head';
        const movedIdentifier = key + '|' + otherLocation;

        if (currentMetaData.has(movedIdentifier)) {
          internalLog(`[${key}](${location}) -> (${otherLocation}) πŸ›Ί MOVED πŸ›Ί`);
        } else {
          internalLog(`[${key}](${location}) ❌ DELETED ❌`);
        }
      }
    });

    previousMetaData = currentMetaData;
  }

  function initialScan() {
    const metaTags = document.querySelectorAll('meta');
    metaTags.forEach(meta => {
      const key = getMetaKey(meta);
      const value = getMetaValue(meta);
      const location = getLocation(meta);
      const identifier = key + '|' + location;
      previousMetaData.set(identifier, value);
      internalLog(`[${key}](${location}) πŸ“¦ INITIAL πŸ“¦`);
    });
  }

  window.addEventListener('load', () => {
    initialScan();
    setInterval(scanMetaTags, 3000); // every 3 seconds
  });
})();
@github-actions github-actions bot added the Headers Related to the async headers() function. label Apr 29, 2025
@ztanner
Copy link
Member

ztanner commented Apr 29, 2025

Hi -- please see my response here for an explanation on this behavior.

@ztanner ztanner closed this as not planned Won't fix, can't repro, duplicate, stale Apr 29, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Headers Related to the async headers() function.
Projects
None yet
Development

No branches or pull requests

2 participants