Head & Metadata

Catmint provides multiple ways to manage the document <head>: the <Head> component for declarative tags, the useHead() hook for dynamic client-side updates, and the generateMetadata export for async server-side metadata generation.

The Head Component

The <Head> component from catmint/head lets you declaratively add tags to the document head from any component in your tree. During SSR, head tags are collected and rendered into the HTML response. On the client, they are applied to the live document.

import { Head } from 'catmint/head'

export default function AboutPage() {
  return (
    <>
      <Head>
        <title>About Us - My App</title>
        <meta name="description" content="Learn more about our company." />
        <link rel="canonical" href="https://example.com/about" />
      </Head>
      <div>
        <h1>About Us</h1>
        <p>Welcome to our company page.</p>
      </div>
    </>
  )
}

Supported tags

The <Head> component supports the following child elements:

  • <title> -- sets the document title
  • <meta> -- meta tags (description, og:, twitter:, etc.)
  • <link> -- stylesheets, canonical URLs, favicons, preloads
  • <script> -- external scripts or JSON-LD structured data
  • <style> -- inline styles

Deduplication

When multiple <Head> components render the same tag, Catmint deduplicates them. For <title>, the last rendered value wins. For <meta> tags, deduplication is based on the name or property attribute.

// app/layout.tsx -- sets a default title
<Head>
  <title>My App</title>
  <meta name="description" content="Default description." />
</Head>

// app/about/page.tsx -- overrides for this page
<Head>
  <title>About - My App</title>
  <meta name="description" content="About page description." />
</Head>

// Result: title is "About - My App", description is "About page description."

Open Graph and social tags

import { Head } from 'catmint/head'

export default function BlogPostPage({ post }: { post: Post }) {
  return (
    <>
      <Head>
        <title>{post.title} - My Blog</title>
        <meta name="description" content={post.excerpt} />
        <meta property="og:title" content={post.title} />
        <meta property="og:description" content={post.excerpt} />
        <meta property="og:image" content={post.coverImage} />
        <meta property="og:type" content="article" />
        <meta name="twitter:card" content="summary_large_image" />
        <meta name="twitter:title" content={post.title} />
        <meta name="twitter:description" content={post.excerpt} />
        <meta name="twitter:image" content={post.coverImage} />
      </Head>
      <article>
        <h1>{post.title}</h1>
        <div dangerouslySetInnerHTML={{ __html: post.content }} />
      </article>
    </>
  )
}

useHead Hook

The useHead() hook from catmint/head provides imperative, dynamic control over head tags on the client. Use it when head content depends on client-side state that changes after the initial render.

// app/components/chat.client.tsx
import { useHead } from 'catmint/head'
import { useState } from 'react'

export function ChatPage() {
  const [unread, setUnread] = useState(0)

  useHead({
    title: unread > 0 ? `(${unread}) Chat - My App` : 'Chat - My App',
  })

  return (
    <div>
      <p>Unread messages: {unread}</p>
      <button onClick={() => setUnread((n) => n + 1)}>Simulate message</button>
    </div>
  )
}

The useHead() hook accepts an object with the following optional fields:

FieldTypeDescription
titlestringSets the document title
metaArray<{ name?: string; property?: string; content: string }>Sets or updates meta tags
linkArray<{ rel: string; href: string; [key: string]: string }>Adds link tags

Tags managed by useHead() are cleaned up when the component unmounts.

generateMetadata

For server-side metadata that depends on async data (like database queries), export a generateMetadata async function from your page.tsx or layout.tsx. Catmint calls this function during SSR and injects the returned metadata into the document head.

// app/blog/[slug]/page.tsx
import { useParams } from 'catmint/hooks'

export async function generateMetadata({
  params,
}: {
  params: { slug: string }
}) {
  const post = await getPost(params.slug)

  if (!post) {
    return { title: 'Post Not Found' }
  }

  return {
    title: `${post.title} - My Blog`,
    description: post.excerpt,
    openGraph: {
      title: post.title,
      description: post.excerpt,
      images: [{ url: post.coverImage, width: 1200, height: 630 }],
      type: 'article',
    },
    twitter: {
      card: 'summary_large_image',
      title: post.title,
      description: post.excerpt,
      images: [post.coverImage],
    },
  }
}

export default function BlogPostPage() {
  const { slug } = useParams<{ slug: string }>()
  // Component rendering...
  return <article>...</article>
}

Metadata object shape

The object returned by generateMetadata supports the following fields:

FieldTypeDescription
titlestringDocument title
descriptionstringMeta description tag
openGraphobjectOpen Graph metadata (title, description, images, type, url)
twitterobjectTwitter card metadata (card, title, description, images)
robotsstring | objectRobots meta tag (e.g. "noindex, nofollow")
alternatesobjectAlternate language/canonical URLs

Layout metadata

Layouts can also export generateMetadata. Page metadata takes precedence over layout metadata for fields that overlap, while non-overlapping fields are merged:

// app/layout.tsx -- provides defaults
export async function generateMetadata() {
  return {
    title: 'My App',
    description: 'A Catmint application.',
    openGraph: {
      siteName: 'My App',
    },
  }
}

// app/about/page.tsx -- overrides title, inherits siteName
export async function generateMetadata() {
  return {
    title: 'About - My App',
    description: 'Learn more about My App.',
  }
}

HeadContext (SSR)

During server-side rendering, Catmint uses a HeadContext to collect all head tags rendered by <Head> components and generateMetadata across the component tree. These tags are then serialized into the HTML response before it is sent to the client.

You do not need to interact with HeadContext directly. It is used internally by Catmint's rendering pipeline. However, if you are building a custom renderer or testing head output, you can access it:

import { HeadContext, renderHeadToString } from 'catmint/head'

// Used internally during SSR
const headContext = new HeadContext()

// After rendering, extract the collected tags as an HTML string
const headHtml = renderHeadToString(headContext)
// <title>About - My App</title><meta name="description" content="...">...

Structured Data (JSON-LD)

Use the <Head> component to inject JSON-LD structured data for search engines:

import { Head } from 'catmint/head'

export default function ProductPage({ product }: { product: Product }) {
  const jsonLd = {
    '@context': 'https://schema.org',
    '@type': 'Product',
    name: product.name,
    description: product.description,
    image: product.image,
    offers: {
      '@type': 'Offer',
      price: product.price,
      priceCurrency: 'USD',
    },
  }

  return (
    <>
      <Head>
        <title>{product.name} - Store</title>
        <script
          type="application/ld+json"
          dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
        />
      </Head>
      <div>
        <h1>{product.name}</h1>
        <p>{product.description}</p>
      </div>
    </>
  )
}

Summary

APIUse Case
<Head>Declarative head tags from any component (SSR + client)
useHead()Dynamic client-side head updates based on state
generateMetadataAsync server-side metadata from page.tsx or layout.tsx
HeadContextInternal SSR context for collecting head tags

Next: Hooks →