PraxisJS

Document Head

@Head manages document title, meta tags, og:*, and twitter:* reactively per component — the active route's component always wins, and everything is cleaned up on unmount.

Document Head

@Head binds a component to the document <head>. When the component mounts, it sets the title and meta tags. When it unmounts (or the route changes), everything is cleaned up and the previous entry is restored.

npm install @praxisjs/head
pnpm add @praxisjs/head
yarn add @praxisjs/head
bun add @praxisjs/head

Basic usage

import { Head } from '@praxisjs/head'
import { Component } from '@praxisjs/decorators'
import { StatefulComponent } from '@praxisjs/core'

@Head({ title: 'Home — My Site', description: 'Welcome to my site' })
@Component()
class HomePage extends StatefulComponent {
  render() {
    return <main>…</main>
  }
}

Reactive config

Pass a getter function to read signals. Any signal read inside re-runs the update automatically:

@Head((self: PostPage) => ({
  title: `${self.post.data()?.title ?? 'Loading…'} — My Blog`,
  description: self.post.data()?.summary ?? '',
  canonical: `https://myblog.com/posts/${self.slug}`,
  og: {
    title: self.post.data()?.title,
    description: self.post.data()?.summary,
    image: self.post.data()?.coverImage,
    url: `https://myblog.com/posts/${self.slug}`,
    type: 'article',
  },
  twitter: {
    card: 'summary_large_image',
    title: self.post.data()?.title,
    image: self.post.data()?.coverImage,
  },
}))
@Component()
class PostPage extends StatefulComponent {
  @State() slug = ''

  @Resource((self: PostPage) => api.getPost(self.slug))
  post!: ResourceInstance<Post>

  render() { … }
}

The getter receives the component instance as self — identical to the pattern used by @Resource. When self.post.data() resolves, the title and og tags update in the same tick.


Observing head changes

headVersion is a signal that increments every time the document head is updated. Read it inside a reactive expression to subscribe to head changes — useful for inspectors, devtools, or any component that needs to re-render when the head changes:

import { headVersion } from '@praxisjs/head'

@Component()
class HeadDisplay extends StatefulComponent {
  render() {
    return (
      <p>
        {() => {
          void headVersion()          // subscribe — re-runs when head updates
          return `Title: ${document.title}`
        }}
      </p>
    )
  }
}

HeadConfig reference

import type { HeadConfig, MetaTag } from '@praxisjs/head'
FieldTypeDescription
titlestringSets document.title
descriptionstring<meta name="description">
canonicalstring<link rel="canonical" href="...">
metaMetaTag[]Arbitrary <meta> tags by name or property
og.titlestring<meta property="og:title">
og.descriptionstring<meta property="og:description">
og.imagestring<meta property="og:image">
og.urlstring<meta property="og:url">
og.typestring<meta property="og:type">
og.siteNamestring<meta property="og:site_name">
twitter.cardstring<meta name="twitter:card">
twitter.titlestring<meta name="twitter:title">
twitter.descriptionstring<meta name="twitter:description">
twitter.imagestring<meta name="twitter:image">

MetaTag

{ name?: string; property?: string; content: string }

Router integration

When used with @praxisjs/router, each route component declares its own @Head. As the user navigates, the outgoing route unmounts (removing its head entry) and the incoming route mounts (applying its own). The last mounted entry always wins:

@Head({ title: 'Home', description: 'Landing page' })
@Route('/')
@Component()
class Home extends StatefulComponent { … }

@Head((self: About) => ({ title: `About — ${self.siteName}` }))
@Route('/about')
@Component()
class About extends StatefulComponent {
  siteName = 'My Site'
  render() { … }
}

Stack behaviour

@Head uses a stack internally. Each component instance owns one entry identified by a unique symbol. The topmost entry (last mounted) is applied to the DOM. Removing an entry re-applies the one below it, so nested layouts work correctly:

[Layout @Head: site name]
  [PostPage @Head: post title]   ← active — wins

When PostPage unmounts, Layout's entry takes over automatically.

@Head only manages tags it creates (marked with data-praxis-head). Pre-existing tags in the HTML template are not modified or removed. For a fully JS-managed head (SPA), omit default meta tags from the HTML and let @Head manage them entirely.

Storybook
Live demo — reactive title, og, twitter

On this page