【问题标题】:Facebook debugger does not pick up Next.js next-seo meta tagsFacebook 调试器不拾取 Next.js 的 next-seo 元标记
【发布时间】:2021-06-17 09:15:02
【问题描述】:

感觉这个问题已经被问了一百遍了。所以,这可能是一个很难的问题。我将为我的案例提供更多细节。也许会有所帮助。

我正在使用 Next.js ^10.0.9 和 next-seo ^4.22.0。我可以在 devtools 中看到元标记,但 FB 和 Twitter 以及许多其他元标记验证器无法获取它们。

从这里和其他地方的其他几个问题来看,似乎有一些共识,只要它不在源中,即,如果我们“检查源”并且看不到它,那么爬虫就无法使用它。据我了解,这是因为这些机器人不运行呈现元标记所需的 JavaScript。

来源

此页面源应包含描述、标题、图像和其他一些内容的 opengraph 元标记,但它没有:

<!DOCTYPE html>
<html lang="en">
    <head>
        <style data-next-hide-fouc="true">
        body{display:none}
    </style>
    <noscript data-next-hide-fouc="true">
        <style>
        body{display:block}
    </style>
</noscript>

<meta charSet="utf-8"/>
<meta name="viewport" content="minimum-scale=1, initial-scale=1, width=device-width"/>
<meta name="next-head-count" content="2"/>

<noscript data-n-css=""></noscript>

<link rel="preload" href="/_next/static/chunks/main.js?ts=1616232654196" as="script"/>
<link rel="preload" href="/_next/static/chunks/webpack.js?ts=1616232654196" as="script"/>
<link rel="preload" href="/_next/static/chunks/pages/_app.js?ts=1616232654196" as="script"/>
<link rel="preload" href="/_next/static/chunks/pages/index.js?ts=1616232654196" as="script"/>

<noscript id="__next_css__DO_NOT_USE__"></noscript>

<style id="jss-server-side">html {
  box-sizing: border-box;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}
*, *::before, *::after {
  box-sizing: inherit;
}
strong, b {
  font-weight: 700;
}
body {
  color: rgba(0, 0, 0, 0.87);
  margin: 0;
  font-size: 0.875rem;
  font-family: "Roboto", "Helvetica", "Arial", sans-serif;
  font-weight: 400;
  line-height: 1.43;
  letter-spacing: 0.01071em;
  background-color: #c3d0f5;
}
@media print {
  body {
    background-color: #fff;
  }
}
body::backdrop {
  background-color: #c3d0f5;
}</style>

</head>

<body>
    <div id="__next">
        <div class="Loading__Center-sc-9gpo7v-0 dctQei">
            <div style="width:100%;height:100%;overflow:hidden;margin:0 auto;outline:none" title="" role="button" aria-label="animation" tabindex="0">
            </div>
            <h2>This is you. This is how you wait.</h2>
        </div>
    </div>
    
    <script src="/_next/static/chunks/react-refresh.js?ts=1616232654196"></script>
    
    <script id="__NEXT_DATA__" type="application/json">{"props":{"pageProps":{}},"page":"/","query":{},"buildId":"development","runtimeConfig":{"APP_NAME":"example","APP_DESCRIPTION":"Discover examples of examples of examples.","APP_URL":"https://example.com","nextExport":true,"autoExport":true,"isFallback":false}</script>
    
    <script nomodule="" src="/_next/static/chunks/polyfills.js?ts=1616232654196"></script>
    <script src="/_next/static/chunks/main.js?ts=1616232654196"></script>
    <script src="/_next/static/chunks/webpack.js?ts=1616232654196"></script>
    <script src="/_next/static/chunks/pages/_app.js?ts=1616232654196"></script>
    <script src="/_next/static/chunks/pages/index.js?ts=1616232654196"></script>
    <script src="/_next/static/development/_buildManifest.js?ts=1616232654196"></script>
    <script src="/_next/static/development/_ssgManifest.js?ts=1616232654196"></script>
</body>
</html>

构建时生成的页面?

让我感到困惑的是 Next.js 没有渲染它们。根据docs

默认情况下,Next.js 会预渲染每个页面。这意味着 Next.js 预先为每个页面生成 HTML,而不是全部完成 通过客户端 JavaScript。预渲染可以带来更好的效果 性能和 SEO。

除非我看错了,这意味着只要我使用默认设置,页面将通过 SSR 或静态生成呈现。对吧?

另外,上面的源码中提到了"nextExport":true,"autoExport":true,我认为这表明页面应该在构建时创建。

我的代码可能发生了一些变化,但我很确定它们没有偏离 SSR 或静态生成。

代码的高级视图

在某个时候,我添加了 _document.tsx 和 _app.tsx。

我没有对 _document.tsx 进行太多更改。经过一些实验后,我认为从next/document 获得Head 至关重要:

import Document, { Head, Html, Main, NextScript } from "next/document";

import React from "react";
import { ServerStyleSheets } from "@material-ui/core/styles";
import theme from "../styles/theme";

export default class MyDocument extends Document {
  render() {
    return (
      <Html lang="en">
        <Head />
        <body>
          <Main />
          <NextScript />
        </body>
      </Html>
    );
  }
}

MyDocument.getInitialProps = async (ctx) => {
  const sheets = new ServerStyleSheets();
  const originalRenderPage = ctx.renderPage;

  ctx.renderPage = () =>
    originalRenderPage({
      enhanceApp: (App) => (props) => sheets.collect(<App {...props} />),
    });

  const initialProps = await Document.getInitialProps(ctx);

  return {
    ...initialProps,
    styles: [
      ...React.Children.toArray(initialProps.styles),
      sheets.getStyleElement(),
    ],
  };
};

_app.tsx 有更多变化,但我认为不会影响元标记。如果我在这里手动将元标记手动放置在Head 中,它们会显示在源代码中。默认情况下效果很好,但我希望能够以编程方式为每个页面执行此操作。

import { useEffect, useState } from "react";

import { AppProps } from "next/app";
import CssBaseline from "@material-ui/core/CssBaseline";
import Layout from "../components/Layout";
import { ThemeProvider } from "@material-ui/core/styles";
import { User } from "../context/user";
import theme from "../styles/theme";

export default function App({ Component, pageProps }: AppProps) {
  const [user, setUser] = useState({
    auth: null,
    loading: true,
  });

  useEffect(() => {
    const getUser = async () => {
      const res = await fetch("/api/auth/me");
      const auth = res.ok ? await res.json() : null;
      setUser({ auth, loading: false });
    };
    getUser();

    const jssStyles = document.querySelector("#jss-server-side");
    if (jssStyles) {
      jssStyles &&
        jssStyles.parentElement &&
        jssStyles.parentElement.removeChild(jssStyles);
    }
  }, []);

  return (
    <>
      <User.Provider value={{ user, setUser }}>
        <ThemeProvider theme={theme}>
          <CssBaseline />
          <Layout>
            <Component {...pageProps} />
          </Layout>
        </ThemeProvider>
      </User.Provider>
    </>
  );
}

我使用 next-seo 创建了一个Head 组件。它做了很多工作。也许只是注意它直接返回NextSeo,而不是把它放入React Fragment或其他组件中:

import { NextPage } from "next";
import { NextSeo } from "next-seo";
import React from "react";
import getConfig from "next/config";
const { publicRuntimeConfig } = getConfig();
interface Props {
  page?: string;
  description?: string;
  image?: {
    url: string;
    alt: string;
  };
}

const appDescription = publicRuntimeConfig.APP_DESCRIPTION;
const appName = publicRuntimeConfig.APP_NAME;
const appUrl = publicRuntimeConfig.APP_URL;
const twitter = publicRuntimeConfig.TWITTER;

const PageHead: NextPage<Props> = ({ page, description, image }: Props) => {
  const pageTitle = page ? `${page} | ${appName}` : appName;
  const pageDescription = description ?? appDescription;

  let pageUrl;
  let isItemPage;

  if (typeof window !== "undefined") {
    pageUrl = window.location.href ?? appUrl;
    isItemPage = window.location.pathname.includes("item");
  }

  const disallowRobot = isItemPage ? true : false;

  const pageImage = image ?? {
    url: `${appUrl}/logo.png`,
    width: 400,
    height: 400,
    alt: `${appName} logo`,
  };

  return (
    <NextSeo
      title={pageTitle}
      description={pageDescription}
      canonical={pageUrl}
      openGraph={{
        url: pageUrl,
        title: pageTitle,
        description: pageDescription,
        images: [pageImage],
        site_name: appName,
      }}
      twitter={{
        handle: twitter,
        site: twitter,
        cardType: "summary_large_image",
      }}
      noindex={disallowRobot}
      nofollow={disallowRobot}
    />
  );
};

export default PageHead;

这是它在组件/页面中的使用方式。此页面包含来自第三方 API 的动态内容,并且 URL 取决于该内容。我不确定,但我认为这个页面是通过静态生成创建的:

import Head from "../../components/Head";

interface Props {
  item: ContentProps;
}

const MostLikedThings: NextPage<Props> = ({ item }: Props) => {
  ...

  return (
    <>
      <Head
        page={item.title}
        description={item.synopsis}
        image={...}
      />
      <MainWrapper>
          ...
      </MainWrapper>
    </>
  );
};

export default MostLikedThings;

export async function getStaticPaths() {
  return {
    paths: [],
    fallback: true,
  };
}

export async function getStaticProps({ params }: { params: { id: string } }) {
  const item = await (
    await fetch(`/api/content`)
  ).json();
  return {
    props: { item },
    revalidate: 86400,
  };
}

不需要外部内容的更简单的页面如下面的代码所示。我相信它也是使用静态生成制作的,因为(据我所知)我没有使用 getServerSideProps

import Head from "../components/Head";
import Main from "../apps/main/index";
import { NextPage } from "next";

const Home: NextPage = () => {

  return (
    <>
      <Head page="Home" />
      <Main />
    </>
  );
};

export default Home;

我相信这两个页面都是通过静态生成生成的,对吧?在这种情况下,它们都是在构建时生成的。这意味着元标记应该在源代码中。那么,为什么不呢?

做了几次尝试

我尝试了很多方法,包括:

  • 使用getServerSideProps 试图以某种方式更明确地确保页面是在服务器端生成的
  • 从 _app.tsx 和 _document.tsx 中删除 Head
  • 仅使用页面中的html标签head手动设置元标签
  • 直接在页面中使用next/head而不是next-seo手动设置元标签:
<Head>
        ...
        <meta name="twitter:card" content="summary_large_image" />
        <meta name="twitter:site" content={twitter} />
        <meta name="twitter:creator" content={twitter} />
        ...
</Head>

它们都不起作用。

最后要注意的是,我读到here

需要包含标题、元数据或任何其他元素(例如脚本) 作为 Head 元素的直接子元素,或包裹到最多一个 或数组的级别——否则标签将不会 正确选择客户端导航。

我想我已经确定了这一点。

我读过的许多问题中的几个:

我错过了什么吗?

谢谢。

【问题讨论】:

  • 能否包含页面源代码(至少通过元标记,仅作为检查)?有时,服务器提供的页面源与 devtools 中显示的代码不同。
  • @RichDeBourke 已添加,问题顶部的第一块代码。谢谢你,我完全忘了添加它。
  • 您网页的 HTML 代码包含您的 OG 元数据。由于您可以使用 devtools 查看 OG 数据,因此这些标签可能是由客户端 JavaScript 生成的。 Facebook 不会运行任何 JavaScript,因此他们将无法看到您的标签。我没有在我的任何网站上使用 React 或 next.js,所以我不确定它是如何工作的,但似乎你想做static generation

标签: javascript reactjs seo next.js server-side-rendering


【解决方案1】:

在无数次根据其他人未回答的问题进行实验后,我发现我有两个问题。

首先要注意的一点:主页面肯定是在构建时创建的(静态生成)。在我的问题中,我不确定。现在我确定了。

解决问题。

加载画面问题

在加载数据之前有一个加载屏幕是一种常见的模式。如果我们有一个加载屏幕,那就是在构建时创建的。

我的组件是这样嵌套的:

    <Layout> // loading screen here
      <Navbar />
      <Main>{children!}</Main> // NextSeo here
    </Layout>

加载屏幕位于&lt;Layout /&gt;,但我的&lt;NextSeo /&gt; 实现位于&lt;Main /&gt;。这是我在 Layout 中的加载代码:

  if (user && user.loading)
    return (
      <Loading />
    );

因此,即使页面在运行时被浏览器正确渲染,Next.js 在构建时也只构建了 Loading 组件。可能是因为它是它收到的第一件事。这就是各种元标记验证器所抓取的内容。

不幸的是,我在写问题时并不知道这一点,所以可能没有人能够弄清楚这一点,因为我不知道所有组件和实现是如何嵌套的。我的错。

在我的情况下,我有点不赞成用户加载东西,并且能够用this solution替换它:

  const router = useRouter();

  const [pageLoading, setPageLoading] = useState<boolean>(false);

  useEffect(() => {
    const handleStart = () => {
      setPageLoading(true);
    };
    const handleComplete = () => {
      setPageLoading(false);
    };

    router.events.on("routeChangeStart", handleStart);
    router.events.on("routeChangeComplete", handleComplete);
    router.events.on("routeChangeError", handleComplete);
  }, [router]);

如果我确实需要再次获取用户数据,除了useRouter 之外,我可能还需要使用getServerSideProps。我还不知道。这只是说您的用例可能需要一些与我的不同的东西。

getServerSideProps 还是 getStaticProps?

我的第二个问题是对getStaticPathgetStaticProps 的误用/误解。根据docs

getStaticPaths(静态生成):指定动态路由到 根据数据预渲染页面。

我的用例涉及从第三方 API 获取动态 id,然后使用它来构建 URL 并为其获取内容。所以我用 getStaticPath 和 getStaticProps 一起做。这些在构建时创建页面。因此,页面从未收到元标记所需的数据。

由于文档明确说明了使用这两种方法的动态路径,但在 getServerSideProps 方面没有那么明确,我的印象是不应该使用 getServerSideProps。

事实证明,我错了。我们可以将 getServerSideProps 与动态路径一起使用。所以不要这样:

export async function getStaticPaths() {
  return {
    paths: [],
    fallback: true,
  };
}

export async function getStaticProps({ params }: { params: { id: string } }) {
  let item = await (
    await fetch(
      `/api/content/?id=${params.id}`
    )
  ).json();

  return {
    props: { item },
    revalidate: 86400,
  };
}

我现在这样做:

export async function getServerSideProps({
  params,
}: {
  params: { id: string };
}) {
  let item = await (
    await fetch(
      `/api/content/?id=${params.id}`
    )
  ).json();

  return {
    props: { item },
  };
}

因为 getServerSideProps 会根据请求生成页面,所以每次都重新提供元标记。

【讨论】:

  • 感谢您的解释。我也有类似的问题和误解。
【解决方案2】:

感谢您发布您的解决方案。我实现了您概述的 NextJS 路由器加载器策略,现在 Facebook 的调试器正在检测元数据——以及我测试过的所有其他地方。

也就是说,我仍在使用 getStaticProps 为动态路由生成静态页面。即使页面源不包含开放图元标记,它们仍会被检测到。

【讨论】:

    猜你喜欢
    • 2014-06-16
    • 1970-01-01
    • 2021-07-13
    • 1970-01-01
    • 1970-01-01
    • 2021-12-05
    • 2020-09-14
    • 2015-07-19
    • 1970-01-01
    相关资源
    最近更新 更多