Head Management

Davaux provides a simple system for controlling the content of the HTML <head> element from anywhere in the request cycle — page handlers, middleware, and client-side islands.

Server-side: ctx.head

In any server handler (pages, layouts, middleware), ctx.head is a mutable object you write to. Your root _layout.tsx reads these values and renders the actual HTML:

// In a page handler:
ctx.head.title        = 'My Page — My App'
ctx.head.description  = 'A brief description for search engines and social cards.'
ctx.head.meta['og:image'] = 'https://example.com/og/my-page.png'
ctx.head.meta['og:type']  = 'article'

ctx.head.title

A string used as the document's <title>. Your layout typically formats it:

// In _layout.tsx:
<title>{ctx.head.title ? `${ctx.head.title} | My App` : 'My App'}</title>

ctx.head.description

Rendered as <meta name="description"> by your layout:

{ctx.head.description && (
  <meta name="description" content={ctx.head.description} />
)}

ctx.head.meta

A plain object (Record<string, string>) for arbitrary meta tags. The key becomes the name attribute:

ctx.head.meta['twitter:card']  = 'summary_large_image'
ctx.head.meta['twitter:title'] = ctx.head.title ?? ''
ctx.head.meta['robots']        = 'noindex'

Render them in your layout:

{Object.entries(ctx.head.meta ?? {}).map(([name, content]) => (
  <meta name={name} content={content} />
))}

ctx.head.stylesheets

An array of stylesheet URLs to inject into <head>. Push extra CSS files that apply only to specific pages:

ctx.head.stylesheets.push('/css/syntax-highlight.css')
ctx.head.stylesheets.push('https://fonts.googleapis.com/css2?family=Inter')

Layout:

{ctx.head.stylesheets?.map((href) => (
  <link rel="stylesheet" href={href} />
))}

ctx.head.scripts

An array of script URLs rendered at the bottom of <body>:

ctx.head.scripts.push('/js/analytics.js')
ctx.head.scripts.push('https://platform.twitter.com/widgets.js')

Layout:

{ctx.head.scripts?.map((src) => (
  <script src={src} defer />
))}

Setting head in middleware

Middleware runs before page handlers, so you can set default head values and let pages override them:

// src/routes/_middleware.ts
import { defineMiddleware } from 'davaux'

export default defineMiddleware(async (ctx, next) => {
  // Set defaults — pages can override these
  ctx.head.title = 'My App'
  ctx.head.description = 'The default app description.'
  ctx.head.meta['og:site_name'] = 'My App'

  return next()
})

Client-side: useHead from davaux/client

For islands that need to update the document head after hydration, import useHead from davaux/client:

import { useHead } from 'davaux/client'
import { createSignal } from 'davaux/client'

function DocumentManager() {
  const [page, setPage] = createSignal(1)

  useHead({
    title: () => `Results — Page ${page()}`,
    meta: [
      { name: 'robots', content: 'noindex' },
    ],
  })

  // Changing `page` automatically updates document.title
  return <button onClick={() => setPage(n => n + 1)}>Next</button>
}

useHead options

OptionTypeDescription
titlestring | (() => string)Sets document.title. Pass a signal getter for reactive updates.
meta{ name: string; content: string }[]Injects or updates <meta> tags in document.head.

When title is a function, useHead wraps it in a createEffect internally. The document title updates automatically whenever the signal changes:

const [productName, setProductName] = createSignal('Loading...')

useHead({ title: () => `${productName()} — My Shop` })

// Later:
setProductName('Running Shoes')
// document.title → "Running Shoes — My Shop"

Server no-op

On the server (during SSR or SSG), useHead is a no-op. It does not throw, so you can call it unconditionally in island code without needing a guard. The server-rendered head comes from ctx.head instead; useHead only activates in the browser after hydration.