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.