Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

LinkedIn Post Inspector fails to detect Open Graph metadata using Next.js 15.2.0 #76746

Open
susimsek opened this issue Mar 3, 2025 · 7 comments

Comments

@susimsek
Copy link

susimsek commented Mar 3, 2025

Link to the code that reproduces this issue

https://github.com/susimsek/blog

To Reproduce

  1. Clone the repository:
    git clone https://github.com/susimsek/blog.git
    cd blog
  2. Install dependencies:
    npm install
  3. Start the application:
    npm run dev
  4. Deploy the app (e.g., Vercel, Netlify, or any hosting).
  5. Paste a post URL (e.g., https://suaybsimsek.com/en/posts/spring-boot-eureka-server) into LinkedIn Post Inspector.
  6. Observe that LinkedIn reports "No title found / No description found," etc.

Code for the post page can be found here: Post Page Code. Open Graph meta tags are defined inside the <Head> component in this file.

To check the meta tags locally, visit: http://localhost:3000/en/posts/spring-boot-eureka-server after running the application.

Current vs. Expected behavior

  • Current: LinkedIn Post Inspector doesn’t detect or display any Open Graph/meta tags (title, description, og:image, etc.).
  • Expected: LinkedIn should recognize the <meta> tags defined in <Head> and display the correct preview (title, description, image) when sharing the link.

Provide environment information

Operating System:
  Platform: darwin
  Arch: arm64
  Version: Darwin Kernel Version 24.3.0: Thu Jan  2 20:24:23 PST 2025; root:xnu-11215.81.4~3/RELEASE_ARM64_T6020
  Available memory (MB): 32768
  Available CPU cores: 12
Binaries:
  Node: 22.14.0
  npm: 10.9.2
  Yarn: N/A
  pnpm: N/A
Relevant Packages:
  next: 15.2.0 // Latest available version is detected (15.2.0).
  eslint-config-next: 15.2.0
  react: 19.0.0
  react-dom: 19.0.0
  typescript: 5.7.3
Next.js Config:
  output: export

Which area(s) are affected? (Select all that apply)

Metadata

Which stage(s) are affected? (Select all that apply)

Other (Deployed), next dev (local), next build (local)

Additional context

The issue occurs in a dynamic Next.js route (pages/[locale]/posts/[id].tsx). The metadata is correctly set inside the <Head> component, but LinkedIn Post Inspector fails to recognize it.

Code for the post page can be found here: Post Page Code

The application is hosted on GitHub Pages and deployed using static export (next export).


Code Block

<Layout posts={posts} searchEnabled={true}>
      <Head>
        <title>{post.title}</title>
        <meta name="description" content={post.summary} />
        <link rel="canonical" href={url} />
        <meta name="keywords" content={keywords} />
        <meta name="author" content={AUTHOR_NAME} />
        <meta name="robots" content="index, follow, max-image-preview:large, max-snippet:-1" />

        {/* Open Graph meta tags for social media sharing */}
        <meta property="og:title" content={post.title} />
        <meta property="og:description" content={post.summary} />
        <meta property="og:type" content="article" />
        <meta property="og:url" content={localizedUrl} />
        <meta property="og:site_name" content={t('common:common.siteName')} />
        <meta property="og:image" content={image} />
        <meta property="og:image:width" content="800" />
        <meta property="og:image:height" content="600" />
        <meta property="og:image:type" content="image/jpeg" />
        <meta property="og:locale" content={LOCALES[currentLocale]?.ogLocale} />

        <meta property="article:published_time" content={post.date} />
        <meta property="article:modified_time" content={post.date} />
        <meta property="article:author" content={AUTHOR_NAME} />
        {(post.topics ?? []).map(topic => (
          <meta key={topic.name} property="article:tag" content={topic.name} />
        ))}

        {/* Twitter Card meta tags for sharing on Twitter */}
        <meta name="twitter:card" content="summary_large_image" />
        <meta name="twitter:title" content={post.title} />
        <meta name="twitter:description" content={post.summary} />
        <meta name="twitter:image" content={image} />
        <meta name="twitter:creator" content={TWITTER_USERNAME} />
        <meta name="twitter:site" content={TWITTER_USERNAME} />

        {/* JSON-LD structured data for enhanced search result features */}
        <script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLdData) }} />
      </Head>
      <PostDetail post={post} />
    </Layout>
@icyJoseph
Copy link
Contributor

icyJoseph commented Mar 3, 2025

Hi,

The problem is in your ThemeProvider, it does:

export default function ThemeProvider({ children }: Readonly<ThemeProviderProps>) {
  const theme = useAppSelector(state => state.theme.theme);
  const [isClient, setIsClient] = useState(false);

  useEffect(() => {
    setIsClient(true);
    document.body.className = theme;
  }, [theme]);

  if (!isClient) {
    return <Loading />;
  }

  return <>{children}</>;
}

Which means, that when bots and such, hit your page, they see the Loading UI, and then since most of those won't execute JavaScript, they never see the children, which contains the metadata you want them to see.

@susimsek
Copy link
Author

susimsek commented Mar 3, 2025

Hi,

I removed the isClient check, but now I constantly get this error locally:

Unhandled Runtime Error

Hydration failed because the server rendered HTML didn't match the client. As a result this tree will be regenerated on the client. This can happen if a SSR-ed Client Component used:

- A server/client branch `if (typeof window !== 'undefined')`.
- Variable input such as `Date.now()` or `Math.random()` which changes each time it's called.
- Date formatting in a user's locale which doesn't match the server.
- External changing data without sending a snapshot of it along with the HTML.
- Invalid HTML tag nesting.

How can I fix this issue without affecting Open Graph metadata?

Here is the updated code:

import { ReactNode, useEffect } from 'react';
import { useAppSelector } from '@/config/store';

type ThemeProviderProps = {
  children: ReactNode;
};

export default function ThemeProvider({ children }: Readonly<ThemeProviderProps>) {
  const theme = useAppSelector(state => state.theme.theme);

  useEffect(() => {
    document.body.className = theme;
  }, [theme]);

  return <>{children}</>;
}

@icyJoseph
Copy link
Contributor

This is another issue entirely now. What HTML element is having the issue? It should be visible in the error overlay?

@susimsek
Copy link
Author

susimsek commented Mar 3, 2025

The hydration issue is likely occurring on the <body> element because document.body.className = theme; is being modified inside useEffect. Since useEffect runs only on the client, the server-rendered HTML does not match what the client renders, causing the hydration error.


Console Error Log:

Uncaught Error: Hydration failed because the server rendered HTML didn't match the client. As a result, this tree will be regenerated on the client. This can happen if a SSR-ed Client Component used:

- A server/client branch `if (typeof window !== 'undefined')`.
- Variable input such as `Date.now()` or `Math.random()` which changes each time it's called.
- Date formatting in a user's locale which doesn't match the server.
- External changing data without sending a snapshot of it along with the HTML.
- Invalid HTML tag nesting.

It can also happen if the client has a browser extension installed which messes with the HTML before React loaded.

https://react.dev/link/hydration-mismatch

    at throwOnHydrationMismatch (react-dom-client.development.js:4129:11)
    at beginWork (react-dom-client.development.js:9930:17)
    at runWithFiberInDEV (react-dom-client.development.js:544:16)
    at performUnitOfWork (react-dom-client.development.js:15045:22)
    at workLoopSync (react-dom-client.development.js:14871:41)
    at renderRootSync (react-dom-client.development.js:14851:11)
    at performWorkOnRoot (react-dom-client.development.js:14335:13)
    at performWorkOnRootViaSchedulerTask (react-dom-client.development.js:15932:7)
    at MessagePort.performWorkUntilDeadline (scheduler.development.js:44:48)

Current ThemeProvider Code:

import { ReactNode, useEffect } from 'react';
import { useAppSelector } from '@/config/store';

type ThemeProviderProps = {
  children: ReactNode;
};

export default function ThemeProvider({ children }: Readonly<ThemeProviderProps>) {
  const theme = useAppSelector(state => state.theme.theme);

  useEffect(() => {
    document.body.className = theme;
  }, [theme]);

  return <>{children}</>;
}

Problem Analysis:

  • The issue occurs because useEffect only runs on the client, which means document.body.className is applied after hydration.
  • This results in a mismatch between the server-rendered HTML and the client-rendered DOM, causing the hydration error.
  • When isClient is added, hydration is fixed, but bots and scrapers no longer see Open Graph metadata because they often don't execute JavaScript.

Question:

I added the isClient check back, which resolves the hydration error, but now Open Graph metadata is not being fetched. What do you think is the correct solution to fix the hydration issue in ThemeProvider without affecting Open Graph metadata?

ThemeProvider Code: [ThemeProvider.tsx]

@icyJoseph
Copy link
Contributor

icyJoseph commented Mar 3, 2025

Doing the change within useEffect is the correct way. Hydration errors are found/triggered before the effects are run.

The isClient check you are doing, is not good for SEO and such as it defers to client side execution to show any content in the Router tree.

I'll take a look at your source code, but the error overlay usually points at the problematic node. Many things can trigger hydration errors.

@icyJoseph
Copy link
Contributor

icyJoseph commented Mar 3, 2025

I have bad news, somewhat... the hydration error, is happening because of FontAwesomeIcon... somehow in the server, it fails to load the icon, or something like that, and then in the client, it does find the icon, and as it hydrates, and that causes the hydration mismatch...

I see in the server side logs:

Could not find icon { prefix: 'fas', iconName: 'clock' }
Could not find icon { prefix: 'fas', iconName: 'calendar-alt' }
Could not find icon { prefix: 'fas', iconName: 'clock' }

I am not sure how to fix that for FontAwesomeIcon though, I have found some topics online, but most seem to suggest to import the icons directly, FortAwesome/Font-Awesome#19348 (comment)

@susimsek
Copy link
Author

susimsek commented Mar 3, 2025

Thanks for the clarification! I’ll review the FontAwesome solutions and see which works best for my case.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants