next.js
Public

The React Framework

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

Closed
Opened 4/29/20251 commentsby bschmeisser-stytch
bschmeisser-stytch

### πŸ“¦ 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 ```bash 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: ```ts 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. ```js // ==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 }); })(); ```

AI Analysis

This issue appears to be discussing a feature request or bug report related to the repository. Based on the content, it seems to be resolved. The issue was opened by bschmeisser-stytch and has received 1 comments.

Add a comment
Comment form would go here