Head / SEO

@sometic/head is a portable document-head controller: nestable title/meta/link patches, SSR serialization, and thin React/Vue adapters. It is not a React-only Helmet fork. The same engine works across stacks.

System standout: SEO recipes + theme bind

Use @sometic/head/seo for page/OG/Twitter/canonical/hreflang/JSON-LD patches, createRouteHeadStack, and detectHeadConflicts. Pair with bindThemeToHead so color-scheme and theme-color stay in sync with @sometic/theme.

Overview

ConcernAPI
CreatecreateHeadController(options?)
Patchset(id, patch) / remove(id)
Readget()HeadSnapshot
Subscribesubscribe(listener) → unsubscribe
Browser applyapplyHead(document, snapshot)
SSRserializeHead(snapshot)
MergemergePatches(patches)
ReactHeadProvider, Head, useHead, useHeadController from @sometic/react/head
VueprovideHead, useHead, useHeadController from @sometic/vue/head

Installation

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

React adapter (peer):

pnpm
pnpm add @sometic/react @sometic/head
npm
npm install @sometic/react @sometic/head
yarn
yarn add @sometic/react @sometic/head
bun
bun add @sometic/react @sometic/head

Vue adapter (peer):

pnpm
pnpm add @sometic/vue @sometic/head
npm
npm install @sometic/vue @sometic/head
yarn
yarn add @sometic/vue @sometic/head
bun
bun add @sometic/vue @sometic/head

Usage

js
import { applyHead, createHeadController, serializeHead } from "@sometic/head";

const head = createHeadController({
    initial: { title: "Sometic", titleTemplate: "%s | Docs" },
});

head.set("page", {
    title: "Architecture",
    meta: [{ name: "description", content: "Controllers and adapters" }],
});

applyHead(document, head.get());
const ssrTags = serializeHead(head.get());
ts
import { applyHead, createHeadController, serializeHead, type HeadPatch } from "@sometic/head";

const head = createHeadController({
    initial: { title: "Sometic", titleTemplate: "%s | Docs" },
});

const patch: HeadPatch = {
    title: "Architecture",
    meta: [{ name: "description", content: "Controllers and adapters" }],
};
head.set("page", patch);

applyHead(document, head.get());
const ssrTags: string = serializeHead(head.get());
js
import { applyHead, createHeadController, serializeHead } from "@sometic/head";

const head = createHeadController({
    initial: { title: "Sometic", titleTemplate: "%s | Docs" },
});

head.set("page", {
    title: "Architecture",
    meta: [{ name: "description", content: "Controllers and adapters" }],
    link: [{ rel: "canonical", href: "https://sometic.aitistack.com/architecture" }],
    htmlAttrs: { lang: "en" },
    bodyAttrs: { class: "docs" },
    jsonLd: [{ type: "application/ld+json", data: { "@type": "WebSite", name: "Sometic" } }],
});

const stop = head.subscribe((snapshot) => {
    applyHead(document, snapshot);
});
applyHead(document, head.get());

// SSR string for <head> injection:
const tags = serializeHead(head.get());

React

tsx
import { Head, HeadProvider } from "@sometic/react/head";

export function App() {
    return (
        <HeadProvider options={{ initial: { titleTemplate: "%s | Sometic" } }}>
            <Head title="Home" meta={[{ name: "description", content: "…" }]} />
            {/* routes */}
        </HeadProvider>
    );
}
tsx
import { useHead } from "@sometic/react/head";

export function ProductPage({ name }: { name: string }) {
    useHead({
        title: name,
        meta: [{ property: "og:title", content: name }],
    });
    return <h1>{name}</h1>;
}

Vue

ts
import { provideHead, useHead } from "@sometic/vue/head";

provideHead({ initial: { titleTemplate: "%s | Sometic" } });
useHead({ title: "Home" });

HeadPatch fields

FieldTypeDescription
titlestringDocument title segment. Combined with titleTemplate when present.
titleTemplatestringTemplate for the final title. If it contains %s, that token is replaced with title. Otherwise the engine concatenates title + space + template when both are set.
metaHeadMeta[]Meta tags to merge (see below).
linkHeadLink[]Link tags to merge (see below).
htmlAttrsHeadAttrsAttributes applied to <html> (Record<string, string | undefined>).
bodyAttrsHeadAttrsAttributes applied to <body>.
jsonLdHeadJsonLd[]JSON-LD script payloads (see below).

Patches are layered by set(id, patch) order. Later patches override earlier ones for scalar fields; meta / link / jsonLd arrays are concatenated across layers; attr maps are merged key-by-key.

HeadMeta

FieldTypeDescription
namestringStandard meta name
propertystringOpen Graph–style property
contentstringMeta content
charsetstringCharset meta
httpEquivstringhttp-equiv meta
FieldTypeDescription
relstringRequired. Link relation
hrefstringRequired. Target URL
asstringResource hint as
typestringMIME type
crossOriginstringCORS mode
mediastringMedia query

HeadJsonLd

FieldTypeDescription
typestringOptional script type (defaults to JSON-LD handling in serialize/apply)
dataRecord<string, unknown> | Record<string, unknown>[]Structured data object(s)

HeadSnapshot (from get())

FieldTypeDescription
titlestringResolved title after template merge
metaHeadMeta[]Merged meta list
linkHeadLink[]Merged link list
htmlAttrsRecord<string, string>Merged html attrs (undefined keys dropped)
bodyAttrsRecord<string, string>Merged body attrs
jsonLdHeadJsonLd[]Merged JSON-LD entries

Controller API

createHeadController(options?)

OptionTypeDescription
initialHeadPatchSeed patch registered as id "root"

Returns HeadController:

MethodSignatureDescription
get() => HeadSnapshotCurrent merged snapshot
set(id: string, patch: HeadPatch) => voidUpsert a named patch layer
remove(id: string) => voidRemove a patch layer
subscribe(listener: (snapshot: HeadSnapshot) => void) => () => voidListen for changes
dispose() => voidClear listeners and patches

applyHead(document, snapshot)

Writes the snapshot into a live Document: title, meta/link tags managed by the engine, html/body attributes, and JSON-LD script nodes. Call only in the browser (or any environment with a real Document).

serializeHead(snapshot)

Returns an HTML string fragment suitable for SSR <head> injection (title, meta, link, JSON-LD scripts). Does not include htmlAttrs / bodyAttrs as tags. Apply those on the server template’s <html> / <body> from the snapshot fields.

mergePatches(patches)

Pure merge of an ordered HeadPatch[] into a HeadSnapshot (same rules as the controller).

SEO helpers (@sometic/head/seo)

HelperRole
createPageSeoPatchTitle / description / robots / keywords
createOpenGraphPatchog:* meta
createTwitterPatchTwitter card meta
createCanonicalLinkCanonical <link>
createHreflangLinksAlternate language links
createJsonLdPatchArticle / Product / Organization / SoftwareApplication JSON-LD
createRouteHeadStackenter(routeId, patch) / leave(routeId) over controller ids
detectHeadConflictsNon-throwing warnings (duplicate names, OG without canonical)

React adapters (@sometic/react/head)

HeadProvider

PropTypeDefaultDescription
controllerHeadController-Optional external controller
optionsCreateHeadControllerOptions-Used when controller is omitted
applybooleantrueWhen true, subscribe and applyHead in the browser
childrenReactNode-Tree that can call useHead / render Head

Accepts all HeadPatch fields as props, plus optional children (rendered through; head side effects only). Registers a patch via useHead with a stable React useId.

useHead(patch)

Registers patch for the component lifetime; removes on unmount. Requires HeadProvider.

useHeadController()

Returns the context HeadController. Throws if no provider.

Also re-exports createHeadController, applyHead, serializeHead, and head types.

Vue adapters (@sometic/vue/head)

provideHead(optionsOrController?, apply = true)

Provides a controller (creates one from CreateHeadControllerOptions unless you pass an existing controller). When apply is true, subscribes and calls applyHead. Disposes a created controller on scope dispose.

useHead(patch, id?)

Registers patch under id (random id by default). Requires provideHead. Updates reactively via watchEffect.

useHeadController()

Injects the provided controller. Throws if missing.

Also re-exports core head APIs and types.

SSR

  1. Create a controller (or merge patches) on the server.
  2. Call serializeHead(controller.get()) and inject into HTML <head>.
  3. Apply htmlAttrs / bodyAttrs from the snapshot onto your document shell.
  4. Do not call applyHead during SSR module evaluation, only after you have a Document, or skip apply and serialize only.
  5. On the client, hydrate with the same initial patch (or apply: false on the provider until ready) so the first client apply does not thrash.

When to use / When not

Use when you need portable head management across React, Vue, and Vanilla with one merge model and SSR serialization.

Do not use if you only need a one-off static <title> in a single HTML file with no nesting or SSR; plain markup is enough.

FAQ

How is this different from react-helmet-async? Helmet is React-tree oriented. @sometic/head is a framework-agnostic controller: the same HeadPatch / snapshot / serializeHead path works in Vanilla and Vue. React/Vue adapters are thin. You are not locked into a React-only head library.

Does it replace react-helmet-async in a React app? It can for title/meta/link/json-ld/html/body attrs. If you depend on Helmet-specific APIs or ecosystem plugins, migrate patch-by-patch.

Can I nest patches? Yes. Each set(id, patch) / useHead / <Head> is a layer. Remove or unmount to drop a layer.

Title template with %s? titleTemplate: "%s | Docs" and title: "Home""Home | Docs".

Who owns meta deduplication? Arrays concatenate across layers. Prefer stable ids and replace whole patches when updating a page so you do not accumulate duplicate metas.

SSR + client apply? Serialize on the server; HeadProvider / provideHead apply in the browser by default. Pass apply={false} / provideHead(…, false) when you need manual control.

Bundle tip? Import from @sometic/head (or @sometic/react/head / @sometic/vue/head); keep peers external in adapters.