You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Next.js 15动态页面刷新后元数据(Metadata)错误渲染至<body>而非<head>的问题求助

Next.js 15动态页面刷新后元数据(Metadata)错误渲染至而非的问题求助

各位Next.js开发者们,我最近在用Next.js 15做项目,采用app目录下export const metadata = { ... }这种官方推荐的最新元数据处理方式,结果遇到了一个非常诡异的问题:

  • 首页的标题、关键词这些元数据,都能老老实实渲染在<head>标签里,完全正常;
  • 从其他页面通过客户端路由(比如点击Link组件)进入动态页面时,元数据也显示没问题;
  • 但一旦刷新这个动态页面,或者直接在新标签页打开它的URL,元数据居然跑到<body>标签里去了!这直接导致SEO完全失效,太头疼了!

下面是我的相关代码,麻烦大家帮忙看看哪里出问题了:

根布局文件(app/layout.jsx)

import ScrollTop from "../components/common/ScrollTop";
import Header from "@/components/header";
import Footer from "@/components/footer";
import WhatsApp from "@/components/common/WhatsApp";
import CallNow from "@/components/common/CallNow";
import "swiper/css";
import "swiper/css/pagination";
import "swiper/css/navigation";
import "swiper/css/scrollbar";
import "swiper/css/effect-cards";
import "aos/dist/aos.css";
import "../styles/index.scss";
import "../styles/font-awesome/css/all.css";
import { fetchCompanyData } from "@/utils/api/companyAndSeoData";
import { baseURL, username, password } from "@/utils/helper/constants";

export const metadata = {
  title: "Abhijeet",
  keywords: "JHjn",
  verification: {
    google: "",
  },
};

export const viewport = {
  themeColor: 'width=device-width, initial-scale=1',
}

export default async function RootLayout({ children }) {
  const web = await fetchCompanyData(baseURL, username, password).catch(() => ({}));

  return (
    <html lang="en">
      <head />
      <body>
        <Header web={web}/>
        {children}
        <WhatsApp />
        <CallNow />
        <ScrollTop />
        <Footer web={web}/>
      </body>
    </html>
  );
}

动态页面的核心组件(app/[alias]/[slug]/PostServer.jsx)

import React from "react";
import { fetchIndividualPost, fetchRelatedPosts } from "@/utils/api/fetchPost";
import { baseURL, username, password, ImageUrl } from "@/utils/helper/constants";
import Banner from "@/components/common/Banner";
import GalleryTwo from "@/components/hotel-single/GalleryTwo";
import RelatedPostSkeleton from "@/components/skeletons/RelatedPostSkeleton";
import Link from "next/link";

export default async function PostServer({ alias, slug }) {
  let postData = null;
  let relatedPost = null;
  let relatedPostError = null;

  try {
    postData = await fetchIndividualPost(baseURL, username, password, alias, slug);
    if (!postData) throw new Error("Post not found");
  } catch (err) {
    return <p className="text-center text-red-500">{err.message}</p>;
  }

  // 单独请求相关文章,避免主请求失败影响页面展示
  try {
    relatedPost = await fetchRelatedPosts(baseURL, username, password, slug);
  } catch (err) {
    relatedPostError = err.message;
  }

  return (
    <>
      <Banner data={postData} />
      <GalleryTwo data={postData} slug={slug} />
      {Array.isArray(relatedPost) && relatedPost.length > 0 ? (
        <section className="layout-pt-md layout-pb-md mt-10">
          <div className="container">
            <div className="row mb-2">
              <div className="col-12">
                <div className="sectionTitle -md text-start">
                  <h2 className="text-24 font-bold">You may also visit</h2>
                </div>
              </div>
            </div>
            <div className="row y-gap-30 justify-start items-center mt-2">
              {relatedPost.map((post) => (
                <div className="col-lg-3" key={post.id}>
                  <div className="card border rounded-4 shadow-sm p-3 card-container" style={{ minHeight: "400px" }}>
                    {post.post_images?.length > 0 && (
                      <Link href={`/${alias}/${post.slug}`}>
                        <img 
                          src={`${ImageUrl}/uploads/post/${post.post_images[0].post_img}`} 
                          alt={post.post_images[0].img_alt || post.title} 
                          className="rounded-4 mb-3 w-100 object-cover" 
                          style={{ height: 150 }} 
                        />
                      </Link>
                    )}
                    <div className="d-flex flex-column h-100">
                      <div>
                        <Link href={`/${alias}/${post.slug}`}>
                          <h5 className="text-18 font-semibold mb-2 hover:text-[#0451a4] transition-colors">
                            {post.title}
                          </h5>
                        </Link>
                        <p className="text-14 text-dark-3 mb-3 text-justify">
                          {post.content?.replace(/<[^>]+>/g, "").slice(0, 100)}...
                        </p>
                      </div>
                      <div className="mt-auto text-center">
                        <Link href={`/${alias}/${post.slug}`} className="btn btn-primary text-14 font-medium">
                          Read More →
                        </Link>
                      </div>
                    </div>
                  </div>
                </div>
              ))}
            </div>
          </div>
        </section>
      ) : (
        relatedPostError ? null : <RelatedPostSkeleton />
      )}
    </>
  );
}

动态页面的page.jsx(app/[alias]/[slug]/page.jsx)

我这里的page.jsx很简单,就是把PostServer组件包了一下,代码大概是这样:

import PostServer from './PostServer';

export default async function Page({ params }) {
  const { alias, slug } = params;
  return <PostServer alias={alias} slug={slug} />;
}

我自己排查过的点:

  1. 根布局里的<head>标签是空的,首页能正常注入元数据,所以这块应该没问题吧?
  2. 动态页面目前没有单独导出metadata,是不是需要在page.jsx里根据postData动态生成?但客户端导航时是正常的,刷新就不行,这点我实在搞不懂。

有没有大佬遇到过类似的情况?到底是什么原因导致元数据在刷新动态页时跑到<body>里?应该怎么解决这个问题?万分感谢!


内容来源于stack exchange

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.07 12:18:09