@pyreon/head 0.13.0 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pyreon/head",
3
- "version": "0.13.0",
3
+ "version": "0.14.0",
4
4
  "description": "Head tag management for Pyreon — works in SSR and CSR",
5
5
  "homepage": "https://github.com/pyreon/pyreon/tree/main/packages/head#readme",
6
6
  "bugs": {
@@ -58,18 +58,19 @@
58
58
  "prepublishOnly": "bun run build"
59
59
  },
60
60
  "dependencies": {
61
- "@pyreon/core": "^0.13.0",
62
- "@pyreon/reactivity": "^0.13.0"
61
+ "@pyreon/core": "^0.14.0",
62
+ "@pyreon/reactivity": "^0.14.0"
63
63
  },
64
64
  "devDependencies": {
65
65
  "@happy-dom/global-registrator": "^20.8.9",
66
- "@pyreon/runtime-dom": "^0.13.0",
67
- "@pyreon/runtime-server": "^0.13.0",
68
- "@pyreon/test-utils": "^0.13.0",
66
+ "@pyreon/manifest": "0.13.1",
67
+ "@pyreon/runtime-dom": "^0.14.0",
68
+ "@pyreon/runtime-server": "^0.14.0",
69
+ "@pyreon/test-utils": "^0.13.2",
69
70
  "@vitest/browser-playwright": "^4.1.4"
70
71
  },
71
72
  "peerDependencies": {
72
- "@pyreon/runtime-server": "^0.13.0"
73
+ "@pyreon/runtime-server": "^0.14.0"
73
74
  },
74
75
  "peerDependenciesMeta": {
75
76
  "@pyreon/runtime-server": {
@@ -0,0 +1,142 @@
1
+ import { defineManifest } from '@pyreon/manifest'
2
+
3
+ export default defineManifest({
4
+ name: '@pyreon/head',
5
+ title: 'Head Management',
6
+ tagline:
7
+ 'Reactive `<head>` tag management — useHead(), HeadProvider, renderWithHead() for SSR',
8
+ description:
9
+ 'Reactive head tag management for Pyreon — `useHead()` collects title, meta, link, script, style, noscript, base, jsonLd entries from any component in the tree (static or signal-driven). `HeadProvider` collects them on the client and syncs to the live `<head>` element; `renderWithHead()` collects them on the server and returns the serialized HTML alongside the rendered app.',
10
+ category: 'browser',
11
+ features: [
12
+ 'useHead(input | () => input) — register head tags from any component',
13
+ 'Reactive: pass a function to re-register on signal change',
14
+ 'Title templates with %s placeholder or function form',
15
+ 'HeadProvider for client-side DOM sync',
16
+ 'renderWithHead() for SSR — returns html + head string',
17
+ 'Keyed deduplication — innermost component wins per key',
18
+ 'JSON-LD shorthand: `jsonLd: {...}` auto-wraps as `<script type="application/ld+json">`',
19
+ ],
20
+ longExample: `import { useHead, HeadProvider } from '@pyreon/head'
21
+ import { renderWithHead } from '@pyreon/head'
22
+ import { mount } from '@pyreon/runtime-dom'
23
+
24
+ // Static head tags from any component
25
+ function ProfilePage() {
26
+ useHead({
27
+ title: 'My Profile',
28
+ meta: [{ name: 'description', content: 'User profile page' }],
29
+ link: [{ rel: 'canonical', href: 'https://example.com/profile' }],
30
+ })
31
+ return <div>profile body</div>
32
+ }
33
+
34
+ // Reactive head — pass a function so signal reads re-register on change
35
+ function ReactiveTitle() {
36
+ useHead(() => ({
37
+ title: \`\${username()} — Profile\`,
38
+ meta: [{ property: 'og:title', content: username() }],
39
+ }))
40
+ return null
41
+ }
42
+
43
+ // Client setup
44
+ mount(
45
+ <HeadProvider>
46
+ <App />
47
+ </HeadProvider>,
48
+ document.getElementById('app')!,
49
+ )
50
+
51
+ // Server setup — collects every useHead() call and serializes the head
52
+ const { html, head, htmlAttrs, bodyAttrs } = await renderWithHead(<App />)
53
+ const document = \`<!doctype html><html\${htmlAttrs}><head>\${head}</head><body\${bodyAttrs}>\${html}</body></html>\``,
54
+ api: [
55
+ {
56
+ name: 'useHead',
57
+ kind: 'hook',
58
+ signature: 'useHead(input: UseHeadInput | (() => UseHeadInput)): void',
59
+ summary:
60
+ 'Register head tags from any component in the tree. Pass a static `UseHeadInput` object for one-shot registration, or a `() => UseHeadInput` thunk for reactive re-registration when signal reads inside the thunk change. Calling `useHead()` outside a `HeadProvider` ancestor (CSR) or `renderWithHead()` invocation (SSR) is a silent no-op — it does not throw.',
61
+ example: `// Static:
62
+ useHead({ title: "My Page", meta: [{ name: "description", content: "..." }] })
63
+
64
+ // Reactive (updates when signals change):
65
+ useHead(() => ({
66
+ title: \`\${username()} — Profile\`,
67
+ meta: [{ property: "og:title", content: username() }]
68
+ }))`,
69
+ mistakes: [
70
+ 'Using `${...}` in a `titleTemplate` string — the placeholder is `%s` (or pass a function form `(title) => …`)',
71
+ 'Calling `useHead()` outside any `HeadProvider` / `renderWithHead()` boundary — silent no-op, the entries simply go nowhere',
72
+ 'Wrapping the input in `computed()` instead of a thunk — pass a plain `() => ({...})` arrow; `useHead` registers its own effect',
73
+ 'Expecting `</script>` inside an inline script body to render verbatim — the SSR escaper rewrites it as `<\\/script>` to prevent breaking out of the inline tag',
74
+ ],
75
+ seeAlso: ['HeadProvider', 'renderWithHead'],
76
+ },
77
+ {
78
+ name: 'HeadProvider',
79
+ kind: 'component',
80
+ signature: '(props: HeadProviderProps) => VNodeChild',
81
+ summary:
82
+ 'Client-side context provider that collects every `useHead()` call from descendants and syncs the resolved tags into the live `document.head` element. Mount once near the application root. Auto-creates a `HeadContextValue` when no `context` prop is passed; nested providers each own an independent context.',
83
+ example: `<HeadProvider>{children}</HeadProvider>
84
+
85
+ // Client-side setup:
86
+ mount(
87
+ <HeadProvider>
88
+ <App />
89
+ </HeadProvider>,
90
+ document.getElementById("app")!
91
+ )`,
92
+ mistakes: [
93
+ 'Mounting two `HeadProvider` instances at sibling roots — each owns an independent context, so a `useHead()` deeper in tree A is invisible to tree B',
94
+ 'Forgetting to mount `HeadProvider` and expecting `useHead()` to still update `document.head` — silent no-op outside a provider',
95
+ ],
96
+ seeAlso: ['useHead', 'renderWithHead', 'createHeadContext'],
97
+ },
98
+ {
99
+ name: 'renderWithHead',
100
+ kind: 'function',
101
+ signature:
102
+ 'renderWithHead(app: VNode): Promise<{ html: string; head: string; htmlAttrs: string; bodyAttrs: string }>',
103
+ summary:
104
+ 'SSR companion to `HeadProvider`. Renders the app to HTML via `renderToString` while collecting every `useHead()` call from the tree, then serializes the resolved tags into a single `head` string plus separate `htmlAttrs` / `bodyAttrs` strings. Async components that call `useHead()` in their body work — the renderer awaits suspended subtrees before serialization.',
105
+ example: `import { renderWithHead } from '@pyreon/head'
106
+
107
+ const { html, head, htmlAttrs, bodyAttrs } = await renderWithHead(<App />)
108
+ const doc = \`<!doctype html><html\${htmlAttrs}><head>\${head}</head><body\${bodyAttrs}>\${html}</body></html>\``,
109
+ mistakes: [
110
+ 'Awaiting `renderWithHead` and then NOT splicing `head` into the `<head>` element — every `useHead()` call quietly disappears',
111
+ 'Forgetting to interpolate `htmlAttrs` / `bodyAttrs` (the leading space is included in each string) — `htmlAttrs.lang` and `bodyAttrs.class` set via `useHead` won\\\'t reach the DOM',
112
+ ],
113
+ seeAlso: ['useHead', 'HeadProvider'],
114
+ },
115
+ {
116
+ name: 'createHeadContext',
117
+ kind: 'function',
118
+ signature: '() => HeadContextValue',
119
+ summary:
120
+ 'Manual factory for a `HeadContextValue` — only needed when wiring up a custom SSR pipeline that bypasses `renderWithHead`, or when running multiple isolated head contexts in the same process. The value exposes `add` / `remove` / `resolve` / `resolveTitleTemplate` / `resolveHtmlAttrs` / `resolveBodyAttrs` for full programmatic control.',
121
+ example: `import { createHeadContext, HeadContext } from '@pyreon/head'
122
+
123
+ const ctx = createHeadContext()
124
+ provide(HeadContext, ctx)
125
+ // ... render tree that calls useHead() ...
126
+ const { tags, htmlAttrs, bodyAttrs } = ctx.resolve()`,
127
+ seeAlso: ['HeadProvider', 'renderWithHead'],
128
+ },
129
+ ],
130
+ gotchas: [
131
+ {
132
+ label: 'Key deduplication',
133
+ note:
134
+ 'Tags with the same key replace each other (innermost wins). Meta keys: `name` → `property` → index. Link keys: `href + rel` → `rel` → index. Script keys: `src` → index. Style and noscript are unkeyed and always accumulated.',
135
+ },
136
+ {
137
+ label: 'Inline script escaping',
138
+ note:
139
+ 'Script / style / noscript bodies are not HTML-escaped, but the SSR serializer rewrites `</script>` / `</style>` / `</noscript>` and `<!--` to prevent breaking out of the wrapping tag. Inline JSON-LD via `jsonLd: {...}` auto-wraps in `<script type="application/ld+json">` and stringifies the value.',
140
+ },
141
+ ],
142
+ })
@@ -0,0 +1,34 @@
1
+ import {
2
+ renderApiReferenceEntries,
3
+ renderLlmsFullSection,
4
+ renderLlmsTxtLine,
5
+ } from '@pyreon/manifest'
6
+ import manifest from '../manifest'
7
+
8
+ // Structural snapshot — locks in the manifest's contract with the
9
+ // gen-docs pipeline without inline-snapshotting the full prose
10
+ // (which rots on every wording change). Pairs with the en-masse
11
+ // `gen-docs --check` CI gate and the api-reference region marker.
12
+
13
+ describe('gen-docs — head snapshot', () => {
14
+ it('renders a llms.txt bullet starting with the package prefix', () => {
15
+ const line = renderLlmsTxtLine(manifest)
16
+ expect(line.startsWith('- @pyreon/head —')).toBe(true)
17
+ })
18
+
19
+ it('renders a llms-full.txt section with the right header', () => {
20
+ const section = renderLlmsFullSection(manifest)
21
+ expect(section.startsWith('## @pyreon/head —')).toBe(true)
22
+ expect(section).toContain('```typescript')
23
+ })
24
+
25
+ it('renders MCP api-reference entries for every api[] item', () => {
26
+ const record = renderApiReferenceEntries(manifest)
27
+ expect(Object.keys(record).sort()).toEqual([
28
+ 'head/HeadProvider',
29
+ 'head/createHeadContext',
30
+ 'head/renderWithHead',
31
+ 'head/useHead',
32
+ ])
33
+ })
34
+ })