@pyreon/head 0.21.0 → 0.23.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/lib/index.js CHANGED
@@ -1,318 +1,5 @@
1
- import { createContext, nativeCompat, onMount, onUnmount, provide, useContext } from "@pyreon/core";
2
- import { effect } from "@pyreon/reactivity";
1
+ import { HeadContext, createHeadContext } from "./context.js";
2
+ import { HeadProvider } from "./provider.js";
3
+ import { t as useHead } from "./_chunks/use-head-B8n30QMl.js";
3
4
 
4
- //#region src/context.ts
5
- function createHeadContext() {
6
- const map = /* @__PURE__ */ new Map();
7
- let dirty = true;
8
- let cachedTags = [];
9
- let cachedTitleTemplate;
10
- let cachedHtmlAttrs = {};
11
- let cachedBodyAttrs = {};
12
- function rebuild() {
13
- if (!dirty) return;
14
- dirty = false;
15
- const keyed = /* @__PURE__ */ new Map();
16
- const unkeyed = [];
17
- let titleTemplate;
18
- const htmlAttrs = {};
19
- const bodyAttrs = {};
20
- for (const entry of map.values()) {
21
- for (const tag of entry.tags) if (tag.key) keyed.set(tag.key, tag);
22
- else unkeyed.push(tag);
23
- if (entry.titleTemplate !== void 0) titleTemplate = entry.titleTemplate;
24
- if (entry.htmlAttrs) Object.assign(htmlAttrs, entry.htmlAttrs);
25
- if (entry.bodyAttrs) Object.assign(bodyAttrs, entry.bodyAttrs);
26
- }
27
- cachedTags = [...keyed.values(), ...unkeyed];
28
- cachedTitleTemplate = titleTemplate;
29
- cachedHtmlAttrs = htmlAttrs;
30
- cachedBodyAttrs = bodyAttrs;
31
- }
32
- return {
33
- add(id, entry) {
34
- map.set(id, entry);
35
- dirty = true;
36
- },
37
- remove(id) {
38
- map.delete(id);
39
- dirty = true;
40
- },
41
- resolve() {
42
- rebuild();
43
- return cachedTags;
44
- },
45
- resolveTitleTemplate() {
46
- rebuild();
47
- return cachedTitleTemplate;
48
- },
49
- resolveHtmlAttrs() {
50
- rebuild();
51
- return cachedHtmlAttrs;
52
- },
53
- resolveBodyAttrs() {
54
- rebuild();
55
- return cachedBodyAttrs;
56
- }
57
- };
58
- }
59
- const HeadContext = createContext(null);
60
-
61
- //#endregion
62
- //#region src/provider.ts
63
- /**
64
- * Provides a HeadContextValue to all descendant components.
65
- * Wrap your app root with this to enable useHead() throughout the tree.
66
- *
67
- * Resolution order (first non-null wins):
68
- * 1. `props.context` — explicit context (documented SSR pattern).
69
- * 2. An outer `HeadContext` already in scope — inherited transparently.
70
- * This is what makes `renderWithHead(h(HeadProvider, null, h(App)))`
71
- * work without manual context plumbing: `renderWithHead` pushes its
72
- * own `HeadContext` onto the per-request stack, and a nested
73
- * `HeadProvider` (e.g. one zero's `App` renders unconditionally)
74
- * inherits it instead of silently shadowing it with a fresh,
75
- * write-only registry.
76
- * 3. A freshly-created `HeadContext` — root-level fallback (pure CSR).
77
- *
78
- * The inheritance step is load-bearing for any consumer wrapping
79
- * `<HeadProvider>` inside `renderWithHead()` (the documented JSDoc
80
- * pattern below) AND for the SSG / runtime-SSR pipeline in `@pyreon/zero`,
81
- * whose `createApp` always mounts `h(HeadProvider, null, …)` with no
82
- * `context` prop. Without inheritance, all `useHead()` calls in the
83
- * subtree wrote tags into the inner ctx while `renderWithHead` resolved
84
- * the outer ctx — producing an empty `<head>` for the whole app.
85
- *
86
- * Apps that genuinely need an isolated registry (e.g. iframe / micro-
87
- * frontend boundaries) can still opt out by passing
88
- * `context={createHeadContext()}` explicitly — `props.context` always wins.
89
- *
90
- * @example
91
- * // Auto-create context (root of a CSR app):
92
- * <HeadProvider><App /></HeadProvider>
93
- *
94
- * // Explicit context (e.g. for SSR):
95
- * const headCtx = createHeadContext()
96
- * mount(h(HeadProvider, { context: headCtx }, h(App, null)), root)
97
- *
98
- * // Composes with `renderWithHead` out of the box — no plumbing needed:
99
- * const { html, head } = await renderWithHead(h(HeadProvider, null, h(App, null)))
100
- */
101
- const HeadProvider = (props) => {
102
- provide(HeadContext, props.context ?? useContext(HeadContext) ?? createHeadContext());
103
- const ch = props.children;
104
- return typeof ch === "function" ? ch() : ch;
105
- };
106
- nativeCompat(HeadProvider);
107
-
108
- //#endregion
109
- //#region src/dom.ts
110
- const ATTR = "data-pyreon-head";
111
- /** Tracks managed elements by key — avoids querySelectorAll on every sync */
112
- const managedElements = /* @__PURE__ */ new Map();
113
- /**
114
- * Sync the resolved head tags to the real DOM <head>.
115
- * Uses incremental diffing: matches existing elements by key, patches attributes
116
- * in-place, adds new elements, and removes stale ones.
117
- * Also syncs htmlAttrs, bodyAttrs, and applies titleTemplate.
118
- * No-op on the server (typeof document === "undefined").
119
- */
120
- function patchExistingTag(found, tag, kept) {
121
- kept.add(found.getAttribute(ATTR));
122
- patchAttrs(found, tag.props);
123
- const content = String(tag.children);
124
- if (found.textContent !== content) found.textContent = content;
125
- }
126
- function createNewTag(tag) {
127
- if (typeof document === "undefined") return;
128
- const el = document.createElement(tag.tag);
129
- const key = tag.key;
130
- el.setAttribute(ATTR, key);
131
- for (const [k, v] of Object.entries(tag.props)) el.setAttribute(k, v);
132
- if (tag.children) el.textContent = tag.children;
133
- document.head.appendChild(el);
134
- managedElements.set(key, el);
135
- }
136
- function syncDom(ctx) {
137
- if (typeof document === "undefined") return;
138
- const tags = ctx.resolve();
139
- const titleTemplate = ctx.resolveTitleTemplate();
140
- let needsSeed = managedElements.size === 0;
141
- if (!needsSeed) {
142
- const sample = managedElements.values().next().value;
143
- if (sample && !sample.isConnected) {
144
- managedElements.clear();
145
- needsSeed = true;
146
- }
147
- }
148
- if (needsSeed) {
149
- const existing = document.head.querySelectorAll(`[${ATTR}]`);
150
- for (const el of existing) managedElements.set(el.getAttribute(ATTR), el);
151
- }
152
- const kept = /* @__PURE__ */ new Set();
153
- for (const tag of tags) {
154
- if (tag.tag === "title") {
155
- document.title = applyTitleTemplate(String(tag.children), titleTemplate);
156
- continue;
157
- }
158
- const key = tag.key;
159
- const found = managedElements.get(key);
160
- if (found && found.tagName.toLowerCase() === tag.tag) patchExistingTag(found, tag, kept);
161
- else {
162
- if (found) {
163
- found.remove();
164
- managedElements.delete(key);
165
- }
166
- createNewTag(tag);
167
- kept.add(key);
168
- }
169
- }
170
- for (const [key, el] of managedElements) if (!kept.has(key)) {
171
- el.remove();
172
- managedElements.delete(key);
173
- }
174
- syncElementAttrs(document.documentElement, ctx.resolveHtmlAttrs());
175
- syncElementAttrs(document.body, ctx.resolveBodyAttrs());
176
- }
177
- /** Patch an element's attributes to match the desired props. */
178
- function patchAttrs(el, props) {
179
- for (let i = el.attributes.length - 1; i >= 0; i--) {
180
- const attr = el.attributes[i];
181
- if (!attr || attr.name === ATTR) continue;
182
- if (!(attr.name in props)) el.removeAttribute(attr.name);
183
- }
184
- for (const [k, v] of Object.entries(props)) if (el.getAttribute(k) !== v) el.setAttribute(k, v);
185
- }
186
- function applyTitleTemplate(title, template) {
187
- if (!template) return title;
188
- if (typeof template === "function") return template(title);
189
- return template.replace(/%s/g, title);
190
- }
191
- /** Sync pyreon-managed attributes on <html> or <body>. */
192
- function syncElementAttrs(el, attrs) {
193
- const managed = el.getAttribute(`${ATTR}-attrs`);
194
- if (managed) {
195
- for (const name of managed.split(",")) if (name && !(name in attrs)) el.removeAttribute(name);
196
- }
197
- const keys = [];
198
- for (const [k, v] of Object.entries(attrs)) {
199
- keys.push(k);
200
- if (el.getAttribute(k) !== v) el.setAttribute(k, v);
201
- }
202
- if (keys.length > 0) el.setAttribute(`${ATTR}-attrs`, keys.join(","));
203
- else if (managed) el.removeAttribute(`${ATTR}-attrs`);
204
- }
205
-
206
- //#endregion
207
- //#region src/use-head.ts
208
- /** Cast a strict tag interface to the internal props format, stripping undefined values */
209
- function toProps(obj) {
210
- const result = {};
211
- for (const [k, v] of Object.entries(obj)) if (v !== void 0) result[k] = v;
212
- return result;
213
- }
214
- function buildEntry(o) {
215
- const tags = [];
216
- if (o.title != null) tags.push({
217
- tag: "title",
218
- key: "title",
219
- children: o.title
220
- });
221
- o.meta?.forEach((m, i) => {
222
- tags.push({
223
- tag: "meta",
224
- key: m.name ?? m.property ?? `meta-${i}`,
225
- props: toProps(m)
226
- });
227
- });
228
- o.link?.forEach((l, i) => {
229
- tags.push({
230
- tag: "link",
231
- key: l.href ? `link-${l.rel || ""}-${l.href}` : l.rel ? `link-${l.rel}` : `link-${i}`,
232
- props: toProps(l)
233
- });
234
- });
235
- o.script?.forEach((s, i) => {
236
- const { children, ...rest } = s;
237
- tags.push({
238
- tag: "script",
239
- key: s.src ?? `script-${i}`,
240
- props: toProps(rest),
241
- ...children != null ? { children } : {}
242
- });
243
- });
244
- o.style?.forEach((s, i) => {
245
- const { children, ...rest } = s;
246
- tags.push({
247
- tag: "style",
248
- key: `style-${i}`,
249
- props: toProps(rest),
250
- children
251
- });
252
- });
253
- o.noscript?.forEach((ns, i) => {
254
- tags.push({
255
- tag: "noscript",
256
- key: `noscript-${i}`,
257
- children: ns.children
258
- });
259
- });
260
- if (o.jsonLd) tags.push({
261
- tag: "script",
262
- key: "jsonld",
263
- props: { type: "application/ld+json" },
264
- children: JSON.stringify(o.jsonLd)
265
- });
266
- if (o.speculationRules) tags.push({
267
- tag: "script",
268
- key: "speculationrules",
269
- props: { type: "speculationrules" },
270
- children: JSON.stringify(o.speculationRules)
271
- });
272
- if (o.base) tags.push({
273
- tag: "base",
274
- key: "base",
275
- props: toProps(o.base)
276
- });
277
- return {
278
- tags,
279
- titleTemplate: o.titleTemplate,
280
- htmlAttrs: o.htmlAttrs,
281
- bodyAttrs: o.bodyAttrs
282
- };
283
- }
284
- /**
285
- * Register head tags (title, meta, link, script, style, noscript, base, jsonLd)
286
- * for the current component.
287
- *
288
- * Accepts a static object or a reactive getter:
289
- * useHead({ title: "My Page", meta: [{ name: "description", content: "..." }] })
290
- * useHead(() => ({ title: `${count()} items` })) // updates when signal changes
291
- *
292
- * Tags are deduplicated by key — innermost component wins.
293
- * Requires a <HeadProvider> (CSR) or renderWithHead() (SSR) ancestor.
294
- */
295
- function useHead(input) {
296
- const ctx = useContext(HeadContext);
297
- if (!ctx) return;
298
- const id = Symbol();
299
- if (typeof input === "function") if (typeof document !== "undefined") effect(() => {
300
- ctx.add(id, buildEntry(input()));
301
- syncDom(ctx);
302
- });
303
- else ctx.add(id, buildEntry(input()));
304
- else {
305
- ctx.add(id, buildEntry(input));
306
- onMount(() => {
307
- syncDom(ctx);
308
- });
309
- }
310
- onUnmount(() => {
311
- ctx.remove(id);
312
- syncDom(ctx);
313
- });
314
- }
315
-
316
- //#endregion
317
- export { HeadContext, HeadProvider, createHeadContext, useHead };
318
- //# sourceMappingURL=index.js.map
5
+ export { HeadContext, HeadProvider, createHeadContext, useHead };
package/lib/provider.js CHANGED
@@ -1,63 +1,6 @@
1
- import { createContext, nativeCompat, provide, useContext } from "@pyreon/core";
1
+ import { HeadContext, createHeadContext } from "./context.js";
2
+ import { nativeCompat, provide, useContext } from "@pyreon/core";
2
3
 
3
- //#region src/context.ts
4
- function createHeadContext() {
5
- const map = /* @__PURE__ */ new Map();
6
- let dirty = true;
7
- let cachedTags = [];
8
- let cachedTitleTemplate;
9
- let cachedHtmlAttrs = {};
10
- let cachedBodyAttrs = {};
11
- function rebuild() {
12
- if (!dirty) return;
13
- dirty = false;
14
- const keyed = /* @__PURE__ */ new Map();
15
- const unkeyed = [];
16
- let titleTemplate;
17
- const htmlAttrs = {};
18
- const bodyAttrs = {};
19
- for (const entry of map.values()) {
20
- for (const tag of entry.tags) if (tag.key) keyed.set(tag.key, tag);
21
- else unkeyed.push(tag);
22
- if (entry.titleTemplate !== void 0) titleTemplate = entry.titleTemplate;
23
- if (entry.htmlAttrs) Object.assign(htmlAttrs, entry.htmlAttrs);
24
- if (entry.bodyAttrs) Object.assign(bodyAttrs, entry.bodyAttrs);
25
- }
26
- cachedTags = [...keyed.values(), ...unkeyed];
27
- cachedTitleTemplate = titleTemplate;
28
- cachedHtmlAttrs = htmlAttrs;
29
- cachedBodyAttrs = bodyAttrs;
30
- }
31
- return {
32
- add(id, entry) {
33
- map.set(id, entry);
34
- dirty = true;
35
- },
36
- remove(id) {
37
- map.delete(id);
38
- dirty = true;
39
- },
40
- resolve() {
41
- rebuild();
42
- return cachedTags;
43
- },
44
- resolveTitleTemplate() {
45
- rebuild();
46
- return cachedTitleTemplate;
47
- },
48
- resolveHtmlAttrs() {
49
- rebuild();
50
- return cachedHtmlAttrs;
51
- },
52
- resolveBodyAttrs() {
53
- rebuild();
54
- return cachedBodyAttrs;
55
- }
56
- };
57
- }
58
- const HeadContext = createContext(null);
59
-
60
- //#endregion
61
4
  //#region src/provider.ts
62
5
  /**
63
6
  * Provides a HeadContextValue to all descendant components.
package/lib/ssr.js CHANGED
@@ -1,64 +1,7 @@
1
- import { createContext, h, pushContext } from "@pyreon/core";
1
+ import { HeadContext, createHeadContext } from "./context.js";
2
+ import { h, pushContext } from "@pyreon/core";
2
3
  import { renderToString } from "@pyreon/runtime-server";
3
4
 
4
- //#region src/context.ts
5
- function createHeadContext() {
6
- const map = /* @__PURE__ */ new Map();
7
- let dirty = true;
8
- let cachedTags = [];
9
- let cachedTitleTemplate;
10
- let cachedHtmlAttrs = {};
11
- let cachedBodyAttrs = {};
12
- function rebuild() {
13
- if (!dirty) return;
14
- dirty = false;
15
- const keyed = /* @__PURE__ */ new Map();
16
- const unkeyed = [];
17
- let titleTemplate;
18
- const htmlAttrs = {};
19
- const bodyAttrs = {};
20
- for (const entry of map.values()) {
21
- for (const tag of entry.tags) if (tag.key) keyed.set(tag.key, tag);
22
- else unkeyed.push(tag);
23
- if (entry.titleTemplate !== void 0) titleTemplate = entry.titleTemplate;
24
- if (entry.htmlAttrs) Object.assign(htmlAttrs, entry.htmlAttrs);
25
- if (entry.bodyAttrs) Object.assign(bodyAttrs, entry.bodyAttrs);
26
- }
27
- cachedTags = [...keyed.values(), ...unkeyed];
28
- cachedTitleTemplate = titleTemplate;
29
- cachedHtmlAttrs = htmlAttrs;
30
- cachedBodyAttrs = bodyAttrs;
31
- }
32
- return {
33
- add(id, entry) {
34
- map.set(id, entry);
35
- dirty = true;
36
- },
37
- remove(id) {
38
- map.delete(id);
39
- dirty = true;
40
- },
41
- resolve() {
42
- rebuild();
43
- return cachedTags;
44
- },
45
- resolveTitleTemplate() {
46
- rebuild();
47
- return cachedTitleTemplate;
48
- },
49
- resolveHtmlAttrs() {
50
- rebuild();
51
- return cachedHtmlAttrs;
52
- },
53
- resolveBodyAttrs() {
54
- rebuild();
55
- return cachedBodyAttrs;
56
- }
57
- };
58
- }
59
- const HeadContext = createContext(null);
60
-
61
- //#endregion
62
5
  //#region src/ssr.ts
63
6
  const VOID_TAGS = new Set([
64
7
  "meta",
@@ -0,0 +1,205 @@
1
+ //#region src/context.d.ts
2
+ interface HeadTag {
3
+ /** HTML tag name */
4
+ tag: 'title' | 'meta' | 'link' | 'script' | 'style' | 'base' | 'noscript';
5
+ /**
6
+ * Deduplication key. Tags with the same key replace each other;
7
+ * innermost component (last added) wins.
8
+ * Example: all components setting the page title use key "title".
9
+ */
10
+ key?: string;
11
+ /** HTML attributes for the tag */
12
+ props?: Record<string, string>;
13
+ /** Text content — for <title>, <script>, <style>, <noscript> */
14
+ children?: string;
15
+ }
16
+ /** Standard `<meta>` tag attributes. Catches typos like `{ naem: "description" }`. */
17
+ interface MetaTag {
18
+ /** Standard meta name (e.g. "description", "viewport", "robots") */
19
+ name?: string;
20
+ /** Open Graph / social property (e.g. "og:title", "twitter:card") */
21
+ property?: string;
22
+ /** HTTP equivalent header (e.g. "refresh", "content-type") */
23
+ 'http-equiv'?: string;
24
+ /** Value associated with name, property, or http-equiv */
25
+ content?: string;
26
+ /** Document character encoding (e.g. "utf-8") */
27
+ charset?: string;
28
+ /** Schema.org itemprop */
29
+ itemprop?: string;
30
+ /** Media condition for applicability (e.g. "(prefers-color-scheme: dark)") */
31
+ media?: string;
32
+ }
33
+ /** Standard `<link>` tag attributes. */
34
+ interface LinkTag {
35
+ /** Relationship to the current document (e.g. "stylesheet", "icon", "canonical") */
36
+ rel?: string;
37
+ /** URL of the linked resource */
38
+ href?: string;
39
+ /** Resource type hint for preloading (e.g. "style", "script", "font") */
40
+ as?: string;
41
+ /** MIME type (e.g. "text/css", "image/png") */
42
+ type?: string;
43
+ /** Media query for conditional loading */
44
+ media?: string;
45
+ /** CORS mode */
46
+ crossorigin?: string;
47
+ /** Subresource integrity hash */
48
+ integrity?: string;
49
+ /** Icon sizes (e.g. "32x32", "any") */
50
+ sizes?: string;
51
+ /** Language of the linked resource */
52
+ hreflang?: string;
53
+ /** Title for the link (used for alternate stylesheets) */
54
+ title?: string;
55
+ /** Fetch priority hint */
56
+ fetchpriority?: 'high' | 'low' | 'auto';
57
+ /** Referrer policy */
58
+ referrerpolicy?: string;
59
+ /** Image source set for preloading responsive images */
60
+ imagesrcset?: string;
61
+ /** Image sizes for preloading responsive images */
62
+ imagesizes?: string;
63
+ /** Disable the resource (for stylesheets) */
64
+ disabled?: string;
65
+ /** Color for mask-icon */
66
+ color?: string;
67
+ }
68
+ /** Standard `<script>` tag attributes. */
69
+ interface ScriptTag {
70
+ /** External script URL */
71
+ src?: string;
72
+ /** Script MIME type or module type (e.g. "module", "importmap") */
73
+ type?: string;
74
+ /** Load asynchronously */
75
+ async?: string;
76
+ /** Defer execution until document is parsed */
77
+ defer?: string;
78
+ /** CORS mode */
79
+ crossorigin?: string;
80
+ /** Subresource integrity hash */
81
+ integrity?: string;
82
+ /** Exclude from module-supporting browsers */
83
+ nomodule?: string;
84
+ /** Referrer policy */
85
+ referrerpolicy?: string;
86
+ /** Fetch priority hint */
87
+ fetchpriority?: string;
88
+ /** Inline script content */
89
+ children?: string;
90
+ }
91
+ /** Standard `<style>` tag attributes. */
92
+ interface StyleTag {
93
+ /** Inline CSS content (required) */
94
+ children: string;
95
+ /** Media query for conditional styles */
96
+ media?: string;
97
+ /** Nonce for CSP */
98
+ nonce?: string;
99
+ /** Title for alternate stylesheets */
100
+ title?: string;
101
+ /** Render-blocking behavior */
102
+ blocking?: string;
103
+ }
104
+ /**
105
+ * How eagerly the browser should act on a speculation rule.
106
+ * Per the W3C Speculation Rules spec.
107
+ */
108
+ type SpeculationEagerness = 'immediate' | 'eager' | 'moderate' | 'conservative';
109
+ /**
110
+ * A single speculation rule (one entry in a `prefetch` / `prerender` list).
111
+ *
112
+ * - `source: 'list'` + `urls` — prefetch/prerender these explicit URLs.
113
+ * - `source: 'document'` + `where` — let the browser pick links from the
114
+ * current document that match the predicate (e.g. a CSS selector via
115
+ * `{ selector_matches: '.router-link' }`).
116
+ */
117
+ interface SpeculationRule {
118
+ /** `'list'` (explicit `urls`) or `'document'` (predicate-driven). */
119
+ source?: 'list' | 'document';
120
+ /** Same-origin URLs to prefetch/prerender (for `source: 'list'`). */
121
+ urls?: string[];
122
+ /** Document predicate (for `source: 'document'`) — e.g. `{ selector_matches: 'a.next' }`. */
123
+ where?: Record<string, unknown>;
124
+ /** When the browser should fetch — defaults to the browser's per-source default. */
125
+ eagerness?: SpeculationEagerness;
126
+ /** Capability requirements, e.g. `['anonymous-client-ip-when-cross-origin']`. */
127
+ requires?: string[];
128
+ /** Referrer policy for the speculative request. */
129
+ referrer_policy?: string;
130
+ }
131
+ /**
132
+ * Declarative Speculation Rules — emitted as a single
133
+ * `<script type="speculationrules">` tag. Supported browsers prefetch or
134
+ * fully prerender the next document(s) so navigation is instant. Inert in
135
+ * non-supporting browsers (no polyfill needed). Opt-in: only emitted when
136
+ * `useHead({ speculationRules })` is called.
137
+ *
138
+ * @see https://developer.mozilla.org/docs/Web/API/Speculation_Rules_API
139
+ */
140
+ interface SpeculationRules {
141
+ /** Lightweight: fetch the response, no rendering. */
142
+ prefetch?: SpeculationRule[];
143
+ /** Heavy: fully render the next document in the background. */
144
+ prerender?: SpeculationRule[];
145
+ }
146
+ /** Standard `<base>` tag attributes. */
147
+ interface BaseTag {
148
+ /** Base URL for relative URLs in the document */
149
+ href?: string;
150
+ /** Default target for links and forms */
151
+ target?: '_blank' | '_self' | '_parent' | '_top';
152
+ }
153
+ interface UseHeadInput {
154
+ title?: string;
155
+ /**
156
+ * Title template — use `%s` as a placeholder for the page title.
157
+ * Applied to the resolved title after deduplication.
158
+ * @example useHead({ titleTemplate: "%s | My App" })
159
+ */
160
+ titleTemplate?: string | ((title: string) => string);
161
+ meta?: MetaTag[];
162
+ link?: LinkTag[];
163
+ script?: ScriptTag[];
164
+ style?: StyleTag[];
165
+ noscript?: {
166
+ children: string;
167
+ }[];
168
+ /** Convenience: emits a <script type="application/ld+json"> tag with JSON.stringify'd content */
169
+ jsonLd?: Record<string, unknown> | Record<string, unknown>[];
170
+ /**
171
+ * Convenience: emits a `<script type="speculationrules">` tag with the
172
+ * JSON.stringify'd rules. Supported browsers prefetch/prerender the next
173
+ * document(s) for near-instant navigation; inert elsewhere. Opt-in.
174
+ * @example useHead({ speculationRules: { prerender: [{ source: 'list', urls: ['/about'], eagerness: 'moderate' }] } })
175
+ */
176
+ speculationRules?: SpeculationRules;
177
+ base?: BaseTag;
178
+ /** Attributes to set on the <html> element (e.g. { lang: "en", dir: "ltr" }) */
179
+ htmlAttrs?: Record<string, string>;
180
+ /** Attributes to set on the <body> element (e.g. { class: "dark" }) */
181
+ bodyAttrs?: Record<string, string>;
182
+ }
183
+ interface HeadEntry {
184
+ tags: HeadTag[];
185
+ titleTemplate?: string | ((title: string) => string) | undefined;
186
+ htmlAttrs?: Record<string, string> | undefined;
187
+ bodyAttrs?: Record<string, string> | undefined;
188
+ }
189
+ interface HeadContextValue {
190
+ add(id: symbol, entry: HeadEntry): void;
191
+ remove(id: symbol): void;
192
+ /** Returns deduplicated tags — last-added entry wins per key */
193
+ resolve(): HeadTag[];
194
+ /** Returns the merged titleTemplate (last-added wins) */
195
+ resolveTitleTemplate(): (string | ((title: string) => string)) | undefined;
196
+ /** Returns merged htmlAttrs (later entries override earlier) */
197
+ resolveHtmlAttrs(): Record<string, string>;
198
+ /** Returns merged bodyAttrs (later entries override earlier) */
199
+ resolveBodyAttrs(): Record<string, string>;
200
+ }
201
+ declare function createHeadContext(): HeadContextValue;
202
+ declare const HeadContext: import("@pyreon/core").Context<HeadContextValue | null>;
203
+ //#endregion
204
+ export { BaseTag, HeadContext, HeadContextValue, HeadEntry, HeadTag, LinkTag, MetaTag, ScriptTag, SpeculationEagerness, SpeculationRule, SpeculationRules, StyleTag, UseHeadInput, createHeadContext };
205
+ //# sourceMappingURL=context2.d.ts.map