npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@pyreon/head

v0.45.0

Published

Head tag management for Pyreon — works in SSR and CSR

Downloads

3,979

Readme

@pyreon/head

Reactive <head> tag management — useHead() + HeadProvider + SSR renderWithHead().

Register <title> / <meta> / <link> / <script> / <style> / <noscript> / <base> / JSON-LD / Speculation Rules entries from ANY component (static or signal-driven via a thunk). <HeadProvider> collects them on the client and syncs to the live document.head. renderWithHead() (subpath /ssr) collects them server-side and returns a serialized head string plus htmlAttrs / bodyAttrs. Innermost component wins per key; the inheritance contract makes <HeadProvider> mounted inside renderWithHead() compose without manual context plumbing.

Install

bun add @pyreon/head

For SSR you'll also need @pyreon/runtime-server (peer).

Quick start

import { HeadProvider, useHead } from '@pyreon/head'
import { renderWithHead } from '@pyreon/head/ssr'
import { mount } from '@pyreon/runtime-dom'

function ProfilePage() {
  useHead({
    title: 'My Profile',
    meta: [
      { name: 'description', content: 'User profile page' },
      { property: 'og:title', content: 'My Profile' },
      { property: 'og:image', content: 'https://example.com/og.jpg' },
    ],
    link: [{ rel: 'canonical', href: 'https://example.com/profile' }],
  })
  return <div>profile body</div>
}

// CSR
mount(
  <HeadProvider>
    <ProfilePage />
  </HeadProvider>,
  document.getElementById('app')!,
)

// SSR — note the /ssr subpath
const { html, head, htmlAttrs, bodyAttrs } = await renderWithHead(<ProfilePage />)

Reactive head — thunk form

Pass a function so signal reads re-register on change:

function ReactiveTitle({ username }: { username: () => string }) {
  useHead(() => ({
    title: `${username()} — Profile`,
    meta: [{ property: 'og:title', content: username() }],
  }))
  return null
}

Static-object form registers ONCE; thunk form runs an effect that re-registers whenever a tracked signal changes.

All supported tags

useHead({
  title: 'Page',
  titleTemplate: '%s | My App',                    // or: (title) => `${title} | My App`
  meta: [{ name: 'description', content: '...' }],
  link: [{ rel: 'stylesheet', href: '/app.css' }],
  script: [{ src: '/analytics.js', defer: 'true' }],
  style: [{ children: 'body { font-family: sans-serif }' }],
  noscript: [{ children: 'Please enable JavaScript' }],
  base: { href: 'https://example.com/' },
  jsonLd: { '@context': 'https://schema.org', '@type': 'Article', headline: 'Hi' },
  speculationRules: { prefetch: [{ urls: ['/next-page'], eagerness: 'moderate' }] },
  htmlAttrs: { lang: 'en' },
  bodyAttrs: { class: 'dark' },
})

jsonLd: {...} is shorthand for a <script type="application/ld+json"> tag with JSON.stringify applied. speculationRules: {...} is shorthand for <script type="speculationrules"> — supported browsers prefetch/prerender at their own discretion; inert in non-supporting browsers.

Title templates

useHead({ titleTemplate: '%s | My App' })
// Elsewhere:
useHead({ title: 'About' }) // → <title>About | My App</title>

// Function form for full control:
useHead({ titleTemplate: (t) => (t === 'Home' ? 'My App' : `${t} | My App`) })

%s is the placeholder. Mismatched: ${...} does NOT interpolate (the pyreon/lint rule i18n-prefer-trans-for-rich-jsx is a different concern).

Deduplication

Tags with the same key replace each other — innermost component wins.

| Tag | Key generation | | -------------------- | ------------------------------------------------ | | title | always 'title' | | meta | namepropertyhttp-equiv → array index | | link | href + relrel → index | | script | src → index | | style / noscript | unkeyed — always accumulated |

HeadProvider — context resolution

HeadProvider resolves its context in this order, first non-null wins:

  1. props.context — explicit context (for isolation / custom SSR pipelines)
  2. An outer HeadContext already in scope — inherited transparently
  3. A fresh HeadContext — root-level fallback (pure CSR)

This means renderWithHead(<HeadProvider><App /></HeadProvider>) composes correctly without manual plumbing — the outer ctx that renderWithHead pushed is inherited by the inner provider. A nested <HeadProvider> (e.g. inside another <HeadProvider>, or inside a meta-framework's App that mounts one unconditionally) inherits, not isolates. Pass context={createHeadContext()} explicitly when you genuinely want isolation (iframe / micro-frontend boundary).

// Default: nested HeadProvider inherits
<HeadProvider>
  <HeadProvider>  {/* same ctx as parent */}
    <App />
  </HeadProvider>
</HeadProvider>

// Opt-out: explicit isolated context
<HeadProvider>
  <HeadProvider context={createHeadContext()}>  {/* isolated ctx */}
    <IsolatedSubApp />
  </HeadProvider>
</HeadProvider>

SSR

import { renderWithHead } from '@pyreon/head/ssr'

const { html, head, htmlAttrs, bodyAttrs } = await renderWithHead(<App />)

const page = `<!DOCTYPE html>
<html ${htmlAttrs}>
  <head>
    <meta charset="UTF-8" />
    ${head}
  </head>
  <body ${bodyAttrs}>
    <div id="app">${html}</div>
  </body>
</html>`

renderWithHead (subpath @pyreon/head/ssr) creates its own HeadContext, runs renderToString from @pyreon/runtime-server, then serializes the resolved tags. htmlAttrs / bodyAttrs are space-prefixed strings ready to splice into the opening tags.

XSS hardening: inline script/style/noscript bodies are not HTML-escaped (would break the content), but </script> / </style> / </noscript> and <!-- are rewritten (<\/script> etc.) so user content cannot break out of the wrapping tag.

Subpath exports

import { useHead } from '@pyreon/head/use-head'         // tree-shake fine-grained
import { HeadProvider } from '@pyreon/head/provider'    // tree-shake fine-grained
import { renderWithHead } from '@pyreon/head/ssr'       // SSR-only
import { HeadContext, createHeadContext } from '@pyreon/head/context'  // sub-bundle-stable

The main entry re-exports everything from /use-head + /provider for ergonomics. The /ssr entry is intentionally separate so client bundles don't pull in renderToString from @pyreon/runtime-server.

@pyreon/head/context is the canonical address for HeadContext across every sub-bundle. The build pipeline runs rolldown once per sub-entry (no cross-entry shared chunks), so without externalizing HeadContext each sub-bundle minted its own Symbol ID and useContext lookups silently missed provide calls from sibling bundles. Externalizing /context gives HeadContext a stable runtime address every sub-bundle resolves to. Consumers should rarely need to import directly — but if you wire a custom SSR pipeline that crosses sub-bundles (rare), use @pyreon/head/context for the symbol.

Caveats

  • useHead() called outside any HeadProvider / renderWithHead boundary is a silent no-op (does not throw).
  • useHead() inside a <Suspense> child does NOT reach the document <head> during SSR — the head is flushed in the shell before any boundary resolves. Hoist the call into the layout / shell-level component.
  • Speculation Rules are a declarative HINT. Supported browsers prefetch/prerender at their discretion; non-supporting browsers ignore the tag. It is NOT a replacement for RouterLink prefetch, which warms loader data.

Documentation

Full docs: pyreon.dev/docs/head (or docs/src/content/docs/head.md in this repo).

License

MIT