next.js icon indicating copy to clipboard operation
next.js copied to clipboard

next/head <meta> tags for pages overriding default _app.js <meta> tags are rendered in the browser but not visible to facebook debugger.

Open laurencefass opened this issue 3 years ago • 46 comments

Verify canary release

  • [X] I verified that the issue exists in Next.js canary release

Provide environment information

sorry this is not working.

i am running

nextjs 12 node 16.12.1 yarn 1.22.5

What browser are you using? (if relevant)

chrome

How are you deploying your application? (if relevant)

VPS

Describe the Bug

Summary: I am trying to create fb and twitter metadata for my whole site by defining a <Head> with metadata in _app.js with overrides for specific components using the identical set of tags with some local overrides.

Implementation: I have a two page site with _app.js, one.js and two.js files. I have added a <Head> section into _app.js file and a <Head> section in one.js with some overrides. I have used the keys to ensure that duplicates are not made and when i load the pages in the browser I can see that this works correctly and component specific overrides are presented.

The relevant metadata is as follows and is contained in both _app.js and one.js (but not two.js which I expect to use _app.js metadata). Use of variables enables me to use an identical metadata and set component specific overrides.

<Head> 
      ...   other stuff here
     <meta property="og:title" content={title} key="og-title"/>
      <meta property="og:description" content={description} key="og-desc"/>
      <meta property="og:url" content={router.pathname} key="og-url"/>
      <meta property="og:image" content={image} key="og-image" />
      <meta property="og:site_name" content="mysitename" key="og-site" />
      <meta name="twitter:title" content={title} key="tw-title"/>
      <meta name="twitter:description" content={description} key="tw-desc"/>
      <meta name="twitter:image" content={image} key="tw-image"/>
      <meta name="twitter:card" content="summary_large_image" key="tw-card"/>
</Head>

Result:

Summary: things work as expected in the browser but facebook debugger is not picking up the component specific changes.

In the browser I can see that the correct metadata is loaded for one.js (presenting the component specific overrides) and two.js (presenting the metadata from _app.js). However the facebook debugger is only picking up metadata from _app.js not from one.js - so all pages have the same metadata.

Expected Behavior

Both the facebook debugger and the client browser to load the same metadata. Facebook debugger is somehow not collecting the page specific metadata overrides.

To Reproduce

I have a live site running here:

https://next18.syntapse.co.uk/two doesnt define its own metadata so loads default tags from _app.js using keys to avoid duplication

https//next18.syntapse.co.uk/one loads overrided tags using keys to avoid duplication

This works as expected in the browser. Load this into a browser and note the differences in metadata. Note the metadata changes from the default to the override.

Load this into the facebook debugger and note that /one loads the same metadata as /two and also / url.

laurencefass avatar Mar 09 '22 11:03 laurencefass

I have seen something similar to this with React Helmet. The difference to React Helmet issue is that the facebook debugger doesnt pick up any metadata from the page at all.

Could this be happening because _app.js metadata is being rendered on the server and page metadata is being rendered on the client? If so, could a potential fix therefore be to render all metadata on the server?

laurencefass avatar Mar 09 '22 13:03 laurencefass

Is the data present on the html of the page (View Page Source)? If not, then opengraph tags won't work. You most likely call that tag on a page component, which is wrapped in several few others in the _app. If any of them use a conditional render relying on the client-side logic, then they will always evaluate to false on the server and thus the entire page renders client-side only.

GabenGar avatar Mar 22 '22 16:03 GabenGar

I'm also running into this - I have some pre-rendered pages (getStaticPaths & getStaticProps) that are meant to inject some SEO data into the head using next/head. It appears that all of the page code is being ran client side instead of being fully pre-rendered. Using view source in Chrome I can see the head tags show the defaults from _app.js but inspect element shows the tags are being properly swapped out, indicating client side manipulation of the head tag.

Either this is a bug or it's very very unclear in the docs why this occurs.

The whole purpose of this for us is to get some SEO, so I would love for a solution to be found.

I did some experimenting, removing the next/head component from _app.js results in there being no head, title, meta, etc tags when the source is viewed on Chrome.

Another thing - even pages with no getStaticProps or getStaticPaths don't work.

I can see on Google when I search for <my company> login the login page comes up as the first link but the title is not replaced - it just shows the default title from _app.js

PorterK avatar Mar 22 '22 18:03 PorterK

Again, without seeing the structure of _app it's hard to tell the source of the problem.

GabenGar avatar Mar 22 '22 22:03 GabenGar

Again, without seeing the structure of _app it's hard to tell the source of the problem.

Ended up doing some deeper dives (trying to reproduce in CodeSandbox) and I think the issue may be in other ways I am using _app.js. I am loading the Google Maps API script which includes a brief loading script, preventing the child component from loading on first render (the page).

Will do some further testing but was unable to reproduce taking a simplified version of my exact setup on CodeSandbox - so I think the culprit is an implementation error.

Thanks for responding & prompting me to look into this some more!

PorterK avatar Mar 23 '22 02:03 PorterK

@PorterK when you say not able to repro on codesandbox do you suggest it is working ok with codesandbox? i.e. share works in facebook debugger?

laurencefass avatar Mar 23 '22 08:03 laurencefass

@GabenGar I dont have any conditions for rendering the overriden component. I set up the simplest possible live server to test this out and the code and URLS I am testing are below.

Quick summary I have defined default metatags on _app.tsx. /one ovverides metatags (no conditonal rendering) /two uses default metatags.

result: works as expected in browser but not for facebook debugger.

_app.tsx

function MyApp({ Component, pageProps }: AppProps) {
  let router = useRouter();
  let title = "Default metatags ";
  let description = "Metadata defined in Next js custom _app";

  return <>
    <Head>
      <link rel="shortcut icon" href={`${hostURL}/files/images/icons/syntapse_favicon.png`}/>
      <meta name="viewport" content="width=device-width, initial-scale=1" />
      <meta name="title" content="Syntapse Software" />
      <link rel="shortlink" href={`${hostURL}`} />
      <link rel="canonical" href={`${hostURL}`} />

      <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
      <meta property="og:title" content={title} key="og-title"/>
      <meta property="og:description" content={description} key="og-desc"/>
      <meta property="og:url" content={`${hostURL}${router.pathname}`} key="og-url" />
      <meta property="og:image" content={image} key="og-image" />
      <meta property="og:site_name" content="Syntapse Software" key="og-site" />
      <meta name="twitter:title" content={title} key="tw-title"/>
      <meta name="twitter:description" content={description} key="tw-desc"/>
      <meta name="twitter:image" content={image} key="tw-image"/>
      <meta name="twitter:card" content="summary_large_image" key="tw-card"/>
    </Head>  
    <Component {...pageProps} />
  </>
}

code for URL https://next18.syntapse.co.uk/one

import { useRouter } from "next/router"
import Head from "next/head"

let hostURL = "https://next18.syntapse.co.uk"

export default function Page() {
    let router = useRouter();  
    let title = "Overriden metatags ";
    let description = "Metadata defined in Next js page 'one'";
    let image = `${hostURL}/images/contact_slide.jpg`;
  
    return <>
        <Head>
            <meta property="og:title" content={title} key="og-title" />
            <meta property="og:description" content={description} key="og-desc" />
            <meta property="og:url" content={`${hostURL}${router.pathname}`} key="og-url" />
            <meta property="og:image" content={image} key="og-image" />
            <meta property="og:site_name" content="Syntapse Software" key="og-site" />
            <meta name="twitter:title" content={title} key="tw-title" />
            <meta name="twitter:description" content={description} key="tw-desc" />
            <meta name="twitter:image" content={image} key="tw-image" />
            <meta name="twitter:card" content="summary_large_image" key="tw-card" />
        </Head>
        <h1>Page One</h1>
    </>
}

code for URL https://next18.syntapse.co.uk/two (uses the default metatags)

export default function Page() {
    return <>
        <h1>Page Two</h1>
    </>
}

There are no conditionals in here and the metatags render correctly in the browser but are not recognised by facebooks debugger. Are the page actually being SSR'd, or are they rendered by the browser after initial page load?

laurencefass avatar Mar 23 '22 08:03 laurencefass

@PorterK when you say not able to repro on codesandbox do you suggest it is working ok with codesandbox? i.e. share works in facebook debugger?

I didn't check Facebook Debugger specifically, but looking at view source and inspect element show the same meta tags on codesandbox. The only way I could see it being messed up is if Facebook is doing some extra processing somewhere (definitely possible, haven't looked at it)

view source is the response the browser gets back from the server - if you look at a normal SPA it will not include any HTML other than the root (for example: view-source:https://www.facebook.com/). SSR would return atleast partial HTML (for example: view-source:https://github.com/)

That's usually a pretty good way to tell if the page you are on was rendered by the client or server on the first initial load. I think subsequent link clicks will always occur in the SPA (not 100% clear on this)

PorterK avatar Mar 23 '22 11:03 PorterK

I too have checked the page source of the links posted above and only the domain root contains the metatags they are stripped from the pages.

Using the links I posted above neither /one nor /two contain the metatags though they show up in debugger/inspector. Not sure how that is possible.

Question for maintainer: am I expecting something outside of scope of Next JS design i.e. individual pages can provide their own metadata or is there are issue with Next JS given the simple example code I have posted above? Is it reproducable?

laurencefass avatar Mar 23 '22 12:03 laurencefass

To check without a server simply run as the Facebook tool does, using curl or disabling the browser's javascript. For some reason doesn't inject the declared meta during pre-rendering, it does later with JS. So when you check finds that this supposedly works, but when checking it with the tools the changes are not found.

ahalaburda avatar Mar 23 '22 12:03 ahalaburda

There are no conditionals in here and the metatags render correctly in the browser but are not recognised by facebooks debugger.

What do you mean "render correctly in the browser"? Both of these pages are missing all head tags in the initial html. so they clearly do not render correctly.

GabenGar avatar Mar 23 '22 12:03 GabenGar

Thanks for your continued attention I am most grateful.

Please excuse my terminology here I am still trying to work out what is going wrong. By render correctly in browser: I was initially checking the page as seen by chrome developer tools which (somehow) shows all of the correct headers even though they do not appear in the page source.

If I have supplied all the code I'm using to render the pages how do I inject those headers into the page so I can see them when I "view source? I think that will surely fix the facebook issue.

Also as stated above: "For some reason doesn't inject the declared meta during pre-rendering, it does later with JS." If thats true does this imply that Next JS is not rendering page metadata on the server?

laurencefass avatar Mar 23 '22 12:03 laurencefass

So we dont get too lost in specifics: does Next JS support SSR injection of metatags at a page level to enable a default __app.ts defined set of metatags to be overriden by individual pages similar to the scheme i have used above? Is that meant to work?

laurencefass avatar Mar 23 '22 12:03 laurencefass

By render correctly in browser: I was initially checking the page as seen by chrome developer tools which (somehow) shows all of the correct headers even though they do not appear in the page source.

Dev tools shows you the state of DOM which is a parsed html with all eligible scripts in it activated. If you want to test things like opengraph tags and server-side render in general you have to turn off client javascript in your browser settings. AFAIK there is no way to do this on per-page/site basis without extensions.

If thats true does this imply that Next JS is not rendering page metadata on the server?

No, but you have to be very careful how you write the code for server-rendered pages. Specifically a lot of react hooks do not work server-side, outside of initial state set by React.useState(). Chances are useRouter() is a client-side hook and, due it being a part of nextjs, it is treated "smartly" by the framework. Try to get pathname from getServerSideProps, pass it as props to the page component and see if it fixes the issue.

GabenGar avatar Mar 23 '22 22:03 GabenGar

Possible Solution.

I have spent days looking at this now and I am not convinced that the mechanism for pages overriding _app.tsx

metadata by inclusion of a <Head>in individual pages works for facebook sharing . While the overrides show in the browser debugger they are not picked up by facebook debugger.

To workaround this limitation and solve this particular issue I have been able to expose metadata to facebook debugger by having a single <Head> block in _app.tyx and pass page specific metata as props in getServerSideProps. Guessing would also work for getStaticProps.

page.tsx


export async function getServerSideProps(context) {
    return {
      props: {
        title: "Page one! ",
        description: "Page One metadata",
        image: `${hostURL}/images/page_one_image.jpg`
      }
    }
}

export default function Page() {
    return <div>
        <h1>Page One</h1>
    </div>
}

_app.tsx

function MyApp({ Component, pageProps }: AppProps) {
  let title = pageProps.title ? pageProps.title : "Default metatags ";
  let description = pageProps.description ? pageProps.description : "Default description";
  let image = pageProps.image ? pageProps.image : `${hostURL}/image/default_image.jpg`;

  return <>
    <Head>
      <meta/>
      <other tags/>
    </Head>
  </>

laurencefass avatar Mar 24 '22 15:03 laurencefass

After messing with this for hours, scouring Google to no avail, the above solution worked for me. Thanks @laurencefass I'm doing something like this:

// Some Next JS Page

export const getStaticProps = async () => {
  return {
    props: {
      openGraphData: [
        {
          property: "og:image",
          content:
            "https://glievsbwngosqvrxtupy.supabase.co/storage/v1/object/public/event-banners/Jul%208%20Darkest%20Hour%20LONG.jpeg?t=2022-06-28T21%3A47%3A43.910Z",
          key: "ogimage",
        },
        {
          property: "og:image:width",
          content: "400",
          key: "ogimagewidth",
        },
        {
          property: "og:image:height",
          content: "300",
          key: "ogimageheight",
        },
        {
          property: "og:url",
          content: `http://foobar.com/events`,
          key: "ogurl",
        },
        {
          property: "og:image:secure_url",
          content:
            "https://glievsbwngosqvrxtupy.supabase.co/storage/v1/object/public/event-banners/Jul%208%20Darkest%20Hour%20LONG.jpeg?t=2022-06-28T21%3A47%3A43.910Z",
          key: "ogimagesecureurl",
        },
        {
          property: "og:title",
          content: "Hey hey",
          key: "ogtitle",
        },
        {
          property: "og:description",
          content: "Ima description",
          key: "ogdesc",
        },
        {
          property: "og:type",
          content: "website",
          key: "website",
        },
      ],
    },
  };
};

Then in _app.tsx:

function MyApp({ Component, pageProps }: AppProps) {
  console.log("pageProps", pageProps);
  const { openGraphData = [] } = pageProps;

  return (
    <>
      <Head>
        {openGraphData.map((og) => (
          <meta {...og} />
        ))}
      </Head>
      <Component {...pageProps} />
    </>
  );
}

export default MyApp;

adamjarling avatar Jun 28 '22 22:06 adamjarling

There's a line in the docs here: https://nextjs.org/docs/api-reference/next/head about using the key prop to properly de-duplicate meta tags. Hope it helps!

jacksteves avatar Jun 29 '22 23:06 jacksteves

The example is oversimplified and th keys dont seem to be working as expected in cases stated here.

laurencefass avatar Jun 30 '22 07:06 laurencefass

After messing with this for hours, scouring Google to no avail, the above solution worked for me. Thanks @laurencefass I'm doing something like this:

// Some Next JS Page

export const getStaticProps = async () => {
  return {
    props: {
      openGraphData: [
        {
          property: "og:image",
          content:
            "https://glievsbwngosqvrxtupy.supabase.co/storage/v1/object/public/event-banners/Jul%208%20Darkest%20Hour%20LONG.jpeg?t=2022-06-28T21%3A47%3A43.910Z",
          key: "ogimage",
        },
        {
          property: "og:image:width",
          content: "400",
          key: "ogimagewidth",
        },
        {
          property: "og:image:height",
          content: "300",
          key: "ogimageheight",
        },
        {
          property: "og:url",
          content: `http://foobar.com/events`,
          key: "ogurl",
        },
        {
          property: "og:image:secure_url",
          content:
            "https://glievsbwngosqvrxtupy.supabase.co/storage/v1/object/public/event-banners/Jul%208%20Darkest%20Hour%20LONG.jpeg?t=2022-06-28T21%3A47%3A43.910Z",
          key: "ogimagesecureurl",
        },
        {
          property: "og:title",
          content: "Hey hey",
          key: "ogtitle",
        },
        {
          property: "og:description",
          content: "Ima description",
          key: "ogdesc",
        },
        {
          property: "og:type",
          content: "website",
          key: "website",
        },
      ],
    },
  };
};

Then in _app.tsx:

function MyApp({ Component, pageProps }: AppProps) {
  console.log("pageProps", pageProps);
  const { openGraphData = [] } = pageProps;

  return (
    <>
      <Head>
        {openGraphData.map((og) => (
          <meta {...og} />
        ))}
      </Head>
      <Component {...pageProps} />
    </>
  );
}

export default MyApp;

Hey @adamjarling, so did this fix the issue where the meta of the child pages get overidden by the _app.js file? Im currently facing this issue and can't get the og tags on the child pages to render on facebook debugger as well as linkedin

lakindu2002 avatar Jul 06 '22 08:07 lakindu2002

@lakindu2002 Just use next-seo. It has all features you need including separation between default and component SEO tags, which are keyed under the hood.

GabenGar avatar Jul 06 '22 09:07 GabenGar

yeah i am using next seo. but the _app.js overiddes all child page next-seo tags

lakindu2002 avatar Jul 06 '22 09:07 lakindu2002

@GabenGar

lakindu2002 avatar Jul 06 '22 09:07 lakindu2002

@lakindu2002 did you report that in the next-seo issue queue?

laurencefass avatar Jul 06 '22 09:07 laurencefass

@laurencefass i dont think its an issue with next-seo

lakindu2002 avatar Jul 06 '22 09:07 lakindu2002

it does its thing. once you open the browser console, you can see the og tags. only issue is when used in linkedin and facebook. they fetch the tags in _app.js

lakindu2002 avatar Jul 06 '22 09:07 lakindu2002

After messing with this for hours, scouring Google to no avail, the above solution worked for me. Thanks @laurencefass I'm doing something like this:

// Some Next JS Page

export const getStaticProps = async () => {
  return {
    props: {
      openGraphData: [
        {
          property: "og:image",
          content:
            "https://glievsbwngosqvrxtupy.supabase.co/storage/v1/object/public/event-banners/Jul%208%20Darkest%20Hour%20LONG.jpeg?t=2022-06-28T21%3A47%3A43.910Z",
          key: "ogimage",
        },
        {
          property: "og:image:width",
          content: "400",
          key: "ogimagewidth",
        },
        {
          property: "og:image:height",
          content: "300",
          key: "ogimageheight",
        },
        {
          property: "og:url",
          content: `http://foobar.com/events`,
          key: "ogurl",
        },
        {
          property: "og:image:secure_url",
          content:
            "https://glievsbwngosqvrxtupy.supabase.co/storage/v1/object/public/event-banners/Jul%208%20Darkest%20Hour%20LONG.jpeg?t=2022-06-28T21%3A47%3A43.910Z",
          key: "ogimagesecureurl",
        },
        {
          property: "og:title",
          content: "Hey hey",
          key: "ogtitle",
        },
        {
          property: "og:description",
          content: "Ima description",
          key: "ogdesc",
        },
        {
          property: "og:type",
          content: "website",
          key: "website",
        },
      ],
    },
  };
};

Then in _app.tsx:

function MyApp({ Component, pageProps }: AppProps) {
  console.log("pageProps", pageProps);
  const { openGraphData = [] } = pageProps;

  return (
    <>
      <Head>
        {openGraphData.map((og) => (
          <meta {...og} />
        ))}
      </Head>
      <Component {...pageProps} />
    </>
  );
}

export default MyApp;

this is the only solution that worked for me. kudos! didn't know the props pass from child to _app.js in getStaticProps

lakindu2002 avatar Jul 06 '22 09:07 lakindu2002

Hey all, for what it's worth, I had no luck originally with next-seo, so reverted to my above solution. One way to test that your values are getting passed into _app.js (maybe mentioned above), is by opening your browser's "View Source" and inspecting the <head> for passed in values (as opposed to just viewing in the browser's dev tools DOM inspector).

adamjarling avatar Jul 07 '22 14:07 adamjarling

@adamjarling , you can use next-seo on the _app.js and then use getStaticProps on other pages and pass the required props. That way, when it renders, it'll go through _app.js so you're meta will get picked from there. If you place the meta in individual pages and have a seperate meta set in _app.js your app will override it.

lakindu2002 avatar Jul 08 '22 06:07 lakindu2002

we can actually close this issue because @adamjarling's solution is the fix for this

lakindu2002 avatar Jul 08 '22 06:07 lakindu2002

the workarounds suggested here do not fix the problem with Next code which does not work as documented for some metadata consumers like facebook. please do not close this issue based on a workaround. either the documentation or the core code is broken.

laurencefass avatar Jul 08 '22 06:07 laurencefass