Metadata

How title, description, and Open Graph tags resolve across layouts and pages.


Waku renders metadata as ordinary JSX. React hoists title, meta, and link elements into the document head, so a page declares them wherever it likes:

export default function AboutPage() {
  return (
    <div>
      <title>About</title>
      <meta name="description" content="Who we are." />
      <h1>About</h1>
    </div>
  );
}

Pages override layouts

React does not deduplicate hoisted tags, so a layout and a page that both declare a title would otherwise emit two of them. Waku resolves this while streaming the HTML: when the same key is declared more than once in the head, the last declaration wins.

A layout that declares its metadata before it renders {children} comes first, so the page's declaration comes last. Defaults in a layout, overridden per page, work as expected:

// src/pages/blog/_layout.tsx
export default function BlogLayout({ children }: { children: ReactNode }) {
  return (
    <div>
      <title>Blog</title>
      <meta property="og:site_name" content="My Site" />
      {children}
    </div>
  );
}

// src/pages/blog/hello.tsx: this page's title wins, og:site_name is inherited
export default function HelloPage() {
  return (
    <div>
      <title>Hello</title>
      <h1>Hello</h1>
    </div>
  );
}

The keys that are merged are title, meta[name="description"], and the Open Graph properties that hold a single value: og:title, og:type, og:url, og:description, og:determiner, og:site_name and og:locale. Each is merged on its own, so a layout can set og:site_name while a page sets og:title. A name is matched in any ASCII case, and a property exactly as written.

This matters most for consumers that do not run JavaScript. Search engine and social-card crawlers read the served HTML, where without merging the layout's tags would come first and win.

Everything else is your responsibility

Tags outside that set are emitted as written, and declaring one twice produces two tags. Most notably:

  • viewport and charset are not merged. React re-adds on hydration any tag missing from the served HTML, appending it after the survivor, which would flip which viewport applies; charset is resolved by its first occurrence, so a later one never takes effect anyway. To change either, replace the root component rather than declaring a second tag, as shown below.
  • Repeated Open Graph properties such as og:image, og:video and og:locale:alternate are arrays in OGP, and their order carries meaning, so they are emitted as written. A layout that declares og:image is not replaced by a page that declares one.
  • Slices declare their metadata wherever the <Slice> is rendered, so whether a slice or the page wins depends on which comes first. Keep metadata out of reusable slices.
  • Metadata inside a <Suspense> boundary is emitted after the head has been flushed and cannot be merged into it. An async page component is fine. Only an explicit boundary between the page and its metadata defers it.
  • A <meta> that names itself twice, such as with both name and property, turns the merge off for every page it is on. Give each name a tag of its own.
  • A head holding anything but metadata is served as rendered. A <template> or an inline <svg> has a <title> of its own that is not the document's, so a root component that puts one in <head> turns the merge off for that page.

Overriding charset and viewport

Waku's defaults live in the root component. Adding src/pages/_root.tsx replaces it, and with it the entire document head:

// src/pages/_root.tsx
export default function Root({ children }: { children: ReactNode }) {
  return (
    <html>
      <head>
        <meta charSet="utf-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1" />
      </head>
      <body>{children}</body>
    </html>
  );
}

An app that defines its own root, or that renders the document itself through the minimal API, owns these tags and must declare the ones it wants.

designed bycandycode alternative graphic design web development agency San Diego