React SME Cookbook
All FAQs

Search Documentation

Search across all documentation pages

react-19metadatatitlemetalinkseohead

Document Metadata - Render title, meta, and link tags anywhere in the component tree

Recipe

function BlogPost({ post }: { post: { title: string; description: string; author: string } }) {
  return (
    <article>
      {/* These are hoisted to <head> automatically */}
      <title>{post.title}</title>
      <meta name="description" content={post.description} />
      <meta name="author" content={post.author} />
      <link rel="canonical" href={`https://example.com/blog/${post.title}`} />
 
      <h1>{post.title}</h1>
      <p>{post.description}</p>
    </article>
  );
}

When to reach for this: Use built-in metadata whenever a component needs to set the page title, description, or other head tags. This eliminates the need for react-helmet or manual document.title effects.

Working Example

// A multi-page app where each route sets its own metadata
function ProductPage({ product }: {
  product: {
    name: string;
    description: string;
    price: number;
    image: string;
    category: string;
  };
}) {
  return (
    <main>
      {/* Page title */}
      <title>{product.name} | My Store</title>
 
      {/* SEO meta tags */}
      <meta name="description" content={product.description} />
      <meta name="keywords" content={`${product.category}, buy, shop`} />
 
      {/* Open Graph tags */}
      <meta property="og:title" content={product.name} />
      <meta property="og:description" content={product.description} />
      <meta property="og:image" content={product.image} />
      <meta property="og:type" content="product" />
 
      {/* Twitter card */}
      <meta name="twitter:card" content="summary_large_image" />
      <meta name="twitter:title" content={product.name} />
      <meta name="twitter:image" content={product.image} />
 
      {/* Preload the product image */}
      <link rel="preload" href={product.image} as="image" />
 
      {/* Stylesheet for this page */}
      <link rel="stylesheet" href="/styles/product.css" precedence="default" />
 
      {/* Page content */}
      <div className="max-w-4xl mx-auto p-6">
        <img src={product.image} alt={product.name} />
        <h1 className="text-3xl font-bold">{product.name}</h1>
        <p className="text-xl">${product.price.toFixed(2)}</p>
        <p>{product.description}</p>
      </div>
    </main>
  );
}
 
function SettingsPage() {
  return (
    <div>
      <title>Settings | My Store</title>
      <meta name="robots" content="noindex" />
      <h1>Account Settings</h1>
      {/* ... */}
    </div>
  );
}
 
export { ProductPage, SettingsPage };

What this demonstrates:

  • <title> rendered inline, automatically hoisted to <head>
  • SEO meta tags (description, keywords) set per-page
  • Open Graph and Twitter Card tags for social sharing
  • <link rel="preload"> for performance optimization
  • <link rel="stylesheet"> with precedence for CSS ordering
  • Different pages setting completely different metadata

Deep Dive

How It Works

  • React 19 recognizes <title>, <meta>, and <link> elements when rendered inside a component and hoists them to the document <head>.
  • This works in all rendering modes: client-only SPAs, streaming SSR, and Server Components.
  • During SSR, metadata tags are included in the initial HTML <head>. During client-side navigation, React updates the <head> by adding, removing, or updating the hoisted elements.
  • <title> tags are deduplicated -- only the last rendered <title> wins if multiple components render one.
  • <meta> tags are deduplicated by the name or property attribute. Two <meta name="description"> tags from different components will result in only the most recent one.
  • <link rel="stylesheet"> tags support a precedence attribute that controls insertion order. Stylesheets with the same precedence are grouped together. React also deduplicates stylesheet links by href.
  • Other <link> types (preload, icon, canonical, etc.) are hoisted but not deduplicated by default.

Variations

Dynamic title with live data:

function ChatRoom({ roomName, unreadCount }: { roomName: string; unreadCount: number }) {
  const titlePrefix = unreadCount > 0 ? `(${unreadCount}) ` : "";
  return (
    <div>
      <title>{titlePrefix}{roomName} | Chat</title>
      <h1>{roomName}</h1>
      {/* chat UI */}
    </div>
  );
}

Nested components adding metadata:

function Layout({ children }: { children: React.ReactNode }) {
  return (
    <html>
      <body>
        {/* Base metadata */}
        <meta charSet="utf-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1" />
        <link rel="icon" href="/favicon.ico" />
        {children}
      </body>
    </html>
  );
}
 
function AboutPage() {
  return (
    <Layout>
      {/* Page-specific metadata merges with layout metadata */}
      <title>About Us</title>
      <meta name="description" content="Learn about our company" />
      <h1>About Us</h1>
    </Layout>
  );
}

Stylesheet precedence ordering:

function App() {
  return (
    <>
      {/* "default" precedence loads first */}
      <link rel="stylesheet" href="/base.css" precedence="default" />
      {/* "high" precedence loads after "default" */}
      <link rel="stylesheet" href="/theme.css" precedence="high" />
    </>
  );
}
 
function Widget() {
  return (
    <>
      {/* Same precedence groups together with other "default" stylesheets */}
      <link rel="stylesheet" href="/widget.css" precedence="default" />
      <div>Widget content</div>
    </>
  );
}

TypeScript Notes

  • The <title>, <meta>, and <link> JSX elements use their standard HTML attribute types from React.JSX.IntrinsicElements.
  • The precedence attribute on <link> is a React-specific extension typed as string | undefined.
  • No special imports are needed -- these are standard JSX elements.

Gotchas

  • Multiple <title> tags -- Only the last rendered <title> applies. If two sibling components both render <title>, the result depends on render order. Fix: Set <title> in only one component per route, typically the page-level component.
  • Framework metadata APIs may conflict -- Next.js has its own metadata export and generateMetadata. Using both that and inline <title> tags can cause duplicates. Fix: Choose one approach per project. For Next.js apps, prefer the framework's metadata API.
  • Meta tags outside <head> in SSR HTML -- React hoists tags during rendering, but if you inspect the raw HTML response, they appear in <head>. If hydration mismatches occur, check for server/client differences. Fix: Ensure metadata values are deterministic (no Date.now() or Math.random()).
  • <link rel="stylesheet"> without precedence -- If you omit precedence, the stylesheet link is treated as a regular link and is not deduplicated or ordered by React. Fix: Always add precedence to stylesheet links for React-managed ordering.
  • No support for <script> hoisting -- React 19 does not hoist <script> tags to <head>. Fix: Use preinit from react-dom for scripts, or place script tags in your HTML template.

Alternatives

ApproachWhen to choose
Built-in metadata (React 19)Default choice for new React 19 projects
Next.js metadata exportNext.js App Router projects with static or dynamic metadata
react-helmet / react-helmet-asyncReact 18 or older projects
Manual document.title in useEffectQuick hack for client-only title changes
HTML <head> in templateStatic metadata that never changes

FAQs

How does React 19 handle title, meta, and link tags rendered inside components?
  • React recognizes these elements and hoists them to the document <head> automatically
  • This works in all rendering modes: client-only SPAs, streaming SSR, and Server Components
  • During client-side navigation, React updates the <head> by adding, removing, or updating hoisted elements
What happens if multiple components render a title tag?
  • Only the last rendered <title> applies -- React deduplicates them
  • If two sibling components both render <title>, the result depends on render order
  • Set <title> in only one component per route, typically the page-level component
How are meta tags deduplicated?
  • <meta> tags are deduplicated by the name or property attribute
  • Two <meta name="description"> tags from different components result in only the most recent one
  • Other <link> types (preload, icon, canonical) are hoisted but not deduplicated by default
What does the precedence attribute do on stylesheet link tags?
  • precedence controls insertion order of stylesheets in the <head>
  • Stylesheets with the same precedence value are grouped together
  • React also deduplicates stylesheet links by href and suspends rendering until the stylesheet loads
How do you set Open Graph and Twitter Card tags with React 19 metadata?
function Page({ title, description, image }) {
  return (
    <>
      <meta property="og:title" content={title} />
      <meta property="og:description" content={description} />
      <meta property="og:image" content={image} />
      <meta name="twitter:card" content="summary_large_image" />
      <meta name="twitter:title" content={title} />
      {/* page content */}
    </>
  );
}
Can you dynamically update the title with live data (e.g., unread count)?
function ChatRoom({ roomName, unreadCount }) {
  const prefix = unreadCount > 0 ? `(${unreadCount}) ` : "";
  return (
    <>
      <title>{prefix}{roomName} | Chat</title>
      {/* chat UI */}
    </>
  );
}

The title updates reactively as unreadCount changes.

Can nested components from layouts and pages both contribute metadata?
  • Yes. A layout can set base metadata (<meta charSet>, <link rel="icon">) and child pages add page-specific tags
  • Tags are merged -- page-specific <title> overrides the layout's if both render one
  • <meta> deduplication by name/property ensures no duplicates
Gotcha: Should I use React 19 inline metadata or Next.js metadata export?
  • Next.js has its own metadata export and generateMetadata which may conflict with inline tags
  • Using both can cause duplicate tags in <head>
  • Choose one approach per project; for Next.js apps, prefer the framework's metadata API
Gotcha: Does React 19 hoist script tags to the head?
  • No. React 19 does not hoist <script> tags to <head>
  • Use preinit from react-dom for scripts, or place script tags in your HTML template
  • This only applies to <title>, <meta>, and <link> elements
What happens if a stylesheet link tag is missing the precedence attribute?
  • Without precedence, the stylesheet is treated as a regular link and is not deduplicated or ordered by React
  • React also will not suspend rendering for it, potentially causing a flash of unstyled content
  • Always add precedence to stylesheet links you want React to manage
How is the precedence attribute typed in TypeScript?
  • The precedence attribute on <link> is a React-specific extension typed as string | undefined
  • <title>, <meta>, and <link> use their standard HTML attribute types from React.JSX.IntrinsicElements
  • No special imports are needed -- these are standard JSX elements
How do you ensure metadata values do not cause hydration mismatches?
  • Ensure metadata values are deterministic -- avoid Date.now(), Math.random(), or other non-deterministic values
  • Server and client must produce the same metadata content during hydration
  • Use stable values from props, params, or database queries