<head>

App

We define the <head> tags (e.g. <title> or <meta name="description">) of our app in our render() hook.

// _default.page.server.js
// Environment: Node.js

import { escapeInject, dangerouslySkipEscape } from 'vite-plugin-ssr'
import { renderToHtml } from 'some-view-framework'

export { render }

async function render(pageContext) {
  return escapeInject`<html>
    <head>
      <title>SpaceX</title>
      <meta name="description" content="We deliver payload to space.">
    </head>
    <body>
      <div id="root">
        ${dangerouslySkipEscape(await renderToHtml(pageContext.Page))}
      </div>
    </body>
  </html>`
}

Page — static

To define <head> tags (e.g. <title> or <meta name="description">) for a specific page, we can use pageContext.pageExports.

// about.page.js

// We statically export/determine `<head>` tags
export const documentProps = {
  // This title and description will override the defaults
  title: 'About SpaceX',
  description: 'Our mission is to explore the galaxy.'
}
// _default.page.server.js

import { escapeInject, dangerouslySkipEscape } from 'vite-plugin-ssr'
import { renderToHtml } from 'some-view-framework'

export { render }

async function render(pageContext) {
  // We use `pageContext.pageExports.documentProps` which pages can statically define.
  const documentProps = pageContext.pageExports.documentProps

  // Defaults
  const title = documentProps.title || 'SpaceX'
  const description = documentProps.description || 'We deliver payload to space.'

  return escapeInject`<html>
    <head>
      <title>${title}</title>
      <meta name="description" content="${description}">
    </head>
    <body>
      <div id="root">
        ${dangerouslySkipEscape(await renderToHtml(pageContext.Page))}
      </div>
    </body>
  </html>`
}

Page — dynamic

To define <head> tags (e.g. <title> or <meta name="description">) that are dynamic (i.e. determined at run-time), we can use the onBeforeRender() hook.

// rocket.page.route.js

export const '/rocket/:rocketSlug'
// rocket.page.server.js

export { onBeforeRender }

function onBeforeRender(pageContext) {
  const documentProps = (() => {
    const shipName = pageContext.routeParams.rocketSlug
    if (shipName==='starship') {
      return {
        title: 'Starship',
        description: 'Starship: deliver payload to Mars'
      }
    }
    if (shipName==='falcon') {
      return {
        title: 'Falcon 9',
        description: 'Falcon 9: deliver payload to Low Earth Orbit'
      }
    }
  })()

  return {
    pageContext: {
      documentProps
    }
  }
}
// _default.page.server.js

import { escapeInject, dangerouslySkipEscape } from 'vite-plugin-ssr'
import { renderToHtml } from 'some-view-framework'

export { render }

async function render(pageContext) {
  // We use `pageContext.documentProps` which pages can dynamically
  // define in their `onBeforeRender()` hook.
  const documentProps = pageContext.documentProps

  // Defaults
  const title = documentProps.title || 'SpaceX'
  const description = documentProps.description || 'We deliver payload to space.'

  return escapeInject`<html>
    <head>
      <title>${title}</title>
      <meta name="description" content="${description}">
    </head>
    <body>
      <div id="root">
        ${dangerouslySkipEscape(await renderToHtml(pageContext.Page))}
      </div>
    </body>
  </html>`
}

View Component

To define <head> tags by some deeply nested view (React/Vue/...) component:

  1. We add documentProps to passToClient.
  2. We pass pageContext.documentProps to all components, see Guides > Access pageContext anywhere.
  3. We modify pageContext.documentProps in the deeply nested component.

For example:

// _default.page.server.js
// Environment: Node.js

import { escapeInject, dangerouslySkipEscape } from 'vite-plugin-ssr'
import renderToHtml from 'some-view-framework'

export async function render(pageContext) {
  // We use our UI framework to pass `pageContext.documentProps` to all components
  // of our component tree. (E.g. React Context or Vue's `app.config.globalProperties`.)
  const pageHtml = await renderToHtml(
    <ContextProvider documentProps={pageContext.documentProps} >
      <Page />
    </ContextProvider>
  )

  // What happens here is:
  // 1. Our UI framework passed `documentProps` to all our components
  // 2. One of our (deeply nested) component modified `documentProps`
  // 3. We now render `documentProps` to HTML meta tags
  return escapeInject`<html>
    <head>
      <title>${pageContext.documentProps.title}</title>
      <meta name="description" content="${pageContext.documentProps.description}">
    </head>
    <body>
      <div id="app">
        ${dangerouslySkipEscape(pageHtml)}
      </div>
    </body>
  </html>`
}
// Somewhere in a component deep inside our component tree

// Thanks to our previous steps, `documentProps` is available here.
documentProps.title = 'I was set by some deep component.'
documentProps.description = 'Me too.'

Client Routing

If we use Client Routing, we need to make sure to update document.title upon page change:

// _default.page.server.js

// We make `pageContext.documentProps` available in the browser.
export const passToClient = ['documentProps', 'pageProps']
// _default.page.client.js

import { useClientRouter } from 'vite-plugin-ssr/client/router'

useClientRouter({
  render(pageContext) {
    if( ! pageContext.isHydration ) {
      document.title = pageContext.documentProps.title
    }
    // ...
  }
})

Libraries

We can also use libraries such as @vueuse/head or react-helmet.

We should use such library only if we have a rationale: the aforementioned solutions are simpler and work for most use cases.

Head libraries already sanitize the HTML <head>, this means we can skip escapeInject and wrap the overall result with dangerouslySkipEscape().

// _default.page.server.js
// Environment: Node.js

import { dangerouslySkipEscape } from 'vite-plugin-ssr'
import { renderToHtml } from 'some-view-framework'

export async function render(pageContext) {
  return dangerouslySkipEscape(await renderToHtml(pageContext.Page))
}

Markdown

For pages defined with markdown, see Integration > Markdown > <head> (pageContext.pageExports).