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
| Option | Type | Description |
|---|---|---|
title | string | (() => 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.