Why the preview is blank
When someone shares a link, Facebook, X, LinkedIn, WhatsApp, Discord and Slack fetch the page’s HTML and read the tags in its <head>. They don’t run JavaScript. In a single-page app, the server sends an almost empty page, and tags set with React Helmet, a Vue head manager or Angular’s Meta service appear only in the browser. The crawler never sees them.
Google renders JavaScript for search, so search results can look fine while link previews are blank. That makes this problem easy to miss.
How to tell
Use the command above, or open the page and choose View page source (Ctrl+U, or Cmd+Option+U on a Mac). Don’t use the developer tools’ Elements panel: it shows the page after JavaScript has run, so the tags look present.
The checker reads only the server’s HTML, like a crawler. When the head has no social or description tags but loads app scripts, it reports the page as likely client-side rendered, next to the missing-tag findings it causes.
Next.js
Set tags with the metadata export or generateMetadata in a server component (a page.tsx or layout.tsx without "use client"). Next.js then puts them in the HTML. Setting document.title or meta tags in a client component doesn’t reach crawlers.
Since Next.js 15.2, metadata from generateMetadata can be streamed after </head> to crawlers Next.js doesn’t recognize, such as ours. Facebook, X, LinkedIn, Slack, Discord and WhatsApp are on its list and get the tags in the head, so if the checker finds a Next.js site with no tags, confirm in the Facebook Sharing Debugger (opens in a new tab) before changing anything. The htmlLimitedBots option in next.config controls which crawlers get the tags in the head.
React, Vue and Angular apps
- React single-page apps (Vite, Create React App)
- Move to a framework that renders on the server or at build time, such as Next.js or React Router in framework mode, or prerender your routes to static HTML when you build. React Helmet alone doesn’t fix previews.
- Vue
- Nuxt renders
useSeoMetaon the server by default. Withssr: false, the tags are set in the browser only, so turn server rendering or prerendering back on for pages people share. - Angular
- Add server-side rendering with
@angular/ssr. TheMetaandTitleservices then write their tags into the HTML the server sends. - Can’t change the rendering?
- Put one site-wide set of tags in the static
index.html, so links at least get your site’s title and image. A server or edge function can also insert per-page tags into the HTML before sending it.
Code for your framework
These all render on the server, so crawlers see the tags. Replace the example values with your page’s own.
HTML inside <head>
<title>How we cut our build time in half</title>
<meta name="description" content="What we changed in our CI pipeline, and what it saved.">
<link rel="canonical" href="https://example.com/blog/faster-builds">
<!-- Open Graph -->
<meta property="og:title" content="How we cut our build time in half">
<meta property="og:description" content="What we changed in our CI pipeline, and what it saved.">
<meta property="og:url" content="https://example.com/blog/faster-builds">
<meta property="og:type" content="article">
<meta property="og:site_name" content="Example Blog">
<meta property="og:locale" content="en_US">
<meta property="og:image" content="https://example.com/images/faster-builds.jpg">
<meta property="og:image:width" content="1200">
<meta property="og:image:height" content="630">
<meta property="og:image:alt" content="A chart of build times dropping from 12 to 6 minutes">
<!-- X (Twitter). X falls back to the og: tags for title, description and image. -->
<meta name="twitter:card" content="summary_large_image">
Next.js (App Router) in page.tsx or layout.tsx
import type { Metadata } from "next";
// app/<route>/page.tsx (or layout.tsx)
export async function generateMetadata(): Promise<Metadata> {
return {
title: "How we cut our build time in half",
description: "What we changed in our CI pipeline, and what it saved.",
alternates: { canonical: "https://example.com/blog/faster-builds" },
openGraph: {
title: "How we cut our build time in half",
description: "What we changed in our CI pipeline, and what it saved.",
url: "https://example.com/blog/faster-builds",
type: "article",
siteName: "Example Blog",
locale: "en_US",
images: [
{
url: "https://example.com/images/faster-builds.jpg",
width: 1200,
height: 630,
alt: "A chart of build times dropping from 12 to 6 minutes",
},
],
},
twitter: {
card: "summary_large_image",
},
};
}
Nuxt in the page component
<script setup lang="ts">
useSeoMeta({
title: "How we cut our build time in half",
description: "What we changed in our CI pipeline, and what it saved.",
ogTitle: "How we cut our build time in half",
ogDescription: "What we changed in our CI pipeline, and what it saved.",
ogUrl: "https://example.com/blog/faster-builds",
ogType: "article",
ogSiteName: "Example Blog",
ogLocale: "en_US",
ogImage: "https://example.com/images/faster-builds.jpg",
ogImageWidth: 1200,
ogImageHeight: 630,
ogImageAlt: "A chart of build times dropping from 12 to 6 minutes",
twitterCard: "summary_large_image",
});
useHead({
link: [{ rel: "canonical", href: "https://example.com/blog/faster-builds" }],
});
</script>
React Router / Remix in the route module
// In a route module (React Router v7 framework mode or Remix v2).
export function meta() {
return [
{ title: "How we cut our build time in half" },
{ name: "description", content: "What we changed in our CI pipeline, and what it saved." },
{ tagName: "link", rel: "canonical", href: "https://example.com/blog/faster-builds" },
{ property: "og:title", content: "How we cut our build time in half" },
{ property: "og:description", content: "What we changed in our CI pipeline, and what it saved." },
{ property: "og:url", content: "https://example.com/blog/faster-builds" },
{ property: "og:type", content: "article" },
{ property: "og:site_name", content: "Example Blog" },
{ property: "og:locale", content: "en_US" },
{ property: "og:image", content: "https://example.com/images/faster-builds.jpg" },
{ property: "og:image:width", content: "1200" },
{ property: "og:image:height", content: "630" },
{ property: "og:image:alt", content: "A chart of build times dropping from 12 to 6 minutes" },
{ name: "twitter:card", content: "summary_large_image" },
];
}
After the change, run the check again, then ask Facebook to re-fetch the page in the Facebook Sharing Debugger (opens in a new tab), because platforms cache the blank preview.