Facebook debugger does not pick up Next js next-seo meta tags

0 votes

It feels like this question has been asked a hundred times. So, it's probably a hard question. I'll provide a bit more detail for my case. Maybe it'll help.

I'm using Next.js ^10.0.9 and next-seo ^4.22.0. I can see the meta tags in devtools but FB and Twitter and a lot of other meta tag validators can't pick them up.

From several other questions on here and elsewhere it seems there is some agreement that as long as it's not in the source, i.e., if we "inspect source" and can't see it, then it's not available to scrapers. As I understand it, this is because these bots don't run the JavaScript needed to render the meta tags.

Source

This page source should contain opengraph meta tags for description, title, images, and several other things, but it doesn't:

<!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>

Pages generated at build time?

It puzzles me that Next.js isn't rendering them. According to the docs:

By default, Next.js pre-renders every page. This means that Next.js generates HTML for each page in advance, instead of having it all done by client-side JavaScript. Pre-rendering can result in better performance and SEO.

Unless I'm misreading this, it means that as long I'm using the default setup, pages will be rendered either through SSR or Static Generation. Right?

Moreover, the source above mentions "nextExport":true,"autoExport":true, which I think indicates that the page should be created at build time.

My code may have gone through some changes but I'm pretty sure they've not deviated from SSR or Static Generation.

High level view of code

At some point I added _document.tsx and _app.tsx.

I've not changed _document.tsx much. After some experimentation I gather it's vital to have Head from next/document here:

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 has more changes, but nothing I think would affect the meta tags. If I manually place meta tags manually in Head here, they'd show up in source. Works well for defaults, but I'd like to be able to do it programmatically for every page.

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>
    </>
  );
}

I created a Head component using next-seo. It does quite a bit of work. Perhaps just note that it directly returns NextSeo, without putting it into a React fragment or other components:

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;

Here's how it might be used in a component/page. This page has dynamic content coming from a third-party API, and the URL depends on that contant. I'm not sure, but I think this page is created through Static Generation:

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,
  };
}

A simpler page without need for external content looks like the code below. I believe it's also made with Static Generation since (as I understand it) I've not used getServerSideProps with it:

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;

I believe both pages are being generated through Static Generation, right? In which case, they're both generated at build time. Which means the meta tags should be in the source. So, why aren't they?

Several attempts made

I've tried quite a number of things including:

  • using getServerSideProps in an attempt to somehow be more explicit in making sure the page is generated server-side
  • removing Head from _app.tsx and _document.tsx
  • using just the html tag head in the pages to manually set meta tags
  • using next/head rather than next-seo directly in the pages to manually set meta tags:
<Head>
        ...
        <meta name="twitter:card" content="summary_large_image" />
        <meta name="twitter:site" content={twitter} />
        <meta name="twitter:creator" content={twitter} />
        ...
</Head>

None of them worked.

One last thing to note is, I read here that:

title, meta or any other elements (e.g. script) need to be contained as direct children of the Head element, or wrapped into maximum one level of <React.Fragment> or arrays—otherwise the tags won't be correctly picked up on client-side navigations.

I think I've made sure of that.

A few of the many questions I've read through:

Have I missed anything?

Feb 25 in Others by Kichu
• 17,220 points
806 views

1 answer to this question.

0 votes

It's a common pattern to have a loading screen before data loads. If we have a loading screen, that's what's created at build time.

My components are nested like this:

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

The loading screen is in <Layout /> but my <NextSeo /> implementations are in <Main />. Here's my loading code in Layout:

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

So, even though the page got correctly rendered at runtime by the browser, Next.js built only the Loading component at build time. Probably because it's the first thing it received. And that's what the various meta tag validators scraped.

Unfortunately, I didn't know this when I wrote my question, so probably nobody would have been able to figure this out since I didn't know to give an idea of how all the components and implementations were nested. My bad.

In my case, I kind of deprecated the user loading thing and was able to replace it with 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]);

If I do need to have user data again, I might have to employ getServerSideProps in addition to useRouter. I don't know yet. This is just to say your use case might require something a little different from mine.

getServerSideProps or getStaticProps?

My second issue was a misuse/misconception of getStaticPath and getStaticProps. According to the docs:

getStaticPaths (Static Generation): Specify dynamic routes to pre-render pages based on data.

My use case involves getting a dynamic id from a third-party API and then using that to build a URL and fetch content for it. So I used getStaticPath and getStaticProps together to do it. These create pages at build time. So, the pages never received the data required for the meta tags.

Because the docs are explicit about using dynamic paths with those two methods, but not so explicit when it comes to getServerSideProps, I got the impression that this shouldn't be done with getServerSideProps.

It turns out, I was wrong. We can use getServerSideProps with dynamic paths. So instead of this:

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,
  };
}

I now do this:

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

  return {
    props: { item },
  };
}

Because getServerSideProps generates the page on request, it provides the meta tags anew every time.

answered Feb 26 by narikkadan
• 7,860 points

Related Questions In Others

0 votes
0 answers

SEO META Tags - HTML

i have registered my sited with google ...READ MORE

Feb 14 in Others by Kichu
• 17,220 points
15 views
0 votes
0 answers

Yoast SEO/Facebook OG: Image not displaying after fb url change

i recently changed the name of my ...READ MORE

Feb 14 in Others by Kichu
• 17,220 points
37 views
0 votes
1 answer
0 votes
1 answer
0 votes
1 answer

Next JS Seo with static pages, SSR Pages and Client Side rendering

Use getStaticProps()  with a revalidate property because sites ...READ MORE

answered Feb 20 in Others by narikkadan
• 7,860 points
158 views
0 votes
1 answer

How to use next-seo for setting nextjs meta tag with multiple OGP images?

https://github.com/garmeeh/next-seo use this git repo that contains ...READ MORE

answered Feb 24 in Others by narikkadan
• 7,860 points
93 views
0 votes
1 answer

Next JS Seo with static pages, SSR Pages and Client Side rendering

use getStaticProps() with the revalidate property that ...READ MORE

answered Feb 25 in Others by narikkadan
• 7,860 points
27 views
0 votes
0 answers

Reactjs fetch data from API and google SEO problem

My website create with ReactJS and all ...READ MORE

Mar 4 in Digital Marketing by Kichu
• 17,220 points
26 views
0 votes
1 answer

How to set meta tags using Angular universal SSR and ngx-seo plug-in?

first Install the plug-in with npm i ngx-seo ...READ MORE

answered Feb 11 in Others by narikkadan
• 7,860 points
355 views
webinar REGISTER FOR FREE WEBINAR X
Send OTP
REGISTER NOW
webinar_success Thank you for registering Join Edureka Meetup community for 100+ Free Webinars each month JOIN MEETUP GROUP