@fluixi/head 0.1.0-alpha.20

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Fluixi
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,66 @@
1
+ # @fluixi/head
2
+
3
+ Built-in, **reactive, SSR-first** SEO & document-head management for Fluixi. No external library — it's a core part of the framework.
4
+
5
+ ```ts
6
+ import { seo, jsonld } from '@fluixi/head';
7
+
8
+ seo({
9
+ title: 'Home',
10
+ titleTemplate: '%s · Fluixi',
11
+ description: 'A fine-grained reactive framework.',
12
+ canonical: 'https://fluixi.com/',
13
+ og: { type: 'website', image: 'https://fluixi.com/og.png', siteName: 'Fluixi' },
14
+ twitter: { card: 'summary_large_image', site: '@fluixi' },
15
+ jsonLd: jsonld.website({ name: 'Fluixi', url: 'https://fluixi.com', searchUrl: 'https://fluixi.com/s?q=' }),
16
+ });
17
+ ```
18
+
19
+ ## What it does
20
+
21
+ - **Reactive** — pass a signal (or a function reading one) and the document updates live:
22
+ ```ts
23
+ const [title, setTitle] = createSignal('Dashboard');
24
+ seo(() => ({ title: title() })); // document.title tracks the signal — no reload
25
+ ```
26
+ - **SSR-first** — `renderHead(registry)` serializes the resolved tags into `<head>` markup plus
27
+ `<html>` attributes; the client reconciler reuses those tags **by key** → no hydration mismatch.
28
+ - **Deduplicated** — one title, one description, one canonical; repeatable tags (extra `og:image`,
29
+ hreflang alternates, JSON-LD) accumulate. Last write wins for singletons.
30
+ - **Nested inheritance** — register from a layout, then a page; child values override, arrays merge.
31
+ - **Typed** — every property is typed, including Open Graph, Twitter, robots, icons, verification.
32
+
33
+ ## Surface
34
+
35
+ `title`, `titleTemplate`, `description`, `keywords`, `author`, `robots` (string or object),
36
+ `canonical`, `lang`, `themeColor`, `viewport`, `charset`, `generator`, `applicationName`,
37
+ `referrer`, `colorScheme`, `manifest`, **Open Graph** (incl. multiple images, video, audio,
38
+ article/book/profile), **Twitter** cards, **JSON-LD**, `alternates` (hreflang), `feeds` (RSS),
39
+ `icons` (favicon/apple/mask/shortcut/ms-tile), `verification` (google/bing/yandex/pinterest),
40
+ `base`, and `meta`/`link`/`script` escape hatches.
41
+
42
+ ### JSON-LD helpers
43
+
44
+ `jsonld.organization · person · website · webPage · searchAction · article · blogPosting · faq ·
45
+ product · breadcrumb · videoObject · event · localBusiness · softwareApplication · custom`.
46
+
47
+ ### Components
48
+
49
+ `<Title>`, `<Meta>`, `<Link>`, `<Script>`, `<Base>`, `<JsonLD>` — render nothing, register metadata,
50
+ work on server + client (a signal passed in updates the document live).
51
+
52
+ ### Sitemap & robots
53
+
54
+ Pure generators: `generateSitemap(entries)` (lastmod / changefreq / priority / hreflang / images /
55
+ videos), `chunkSitemap` + `generateSitemapIndex` (50k splitting), and `generateRobots(opts)`
56
+ (rules / host / sitemaps, environment-aware disallow-all for previews).
57
+
58
+ ## Status
59
+
60
+ This is the **engine** (Phase 1): registry + resolution/dedupe + SSR serialize + reactive client
61
+ reconcile + JSON-LD + components + sitemap/robots generators, fully tested.
62
+
63
+ Planned integrations (Phase 2): `seo` route exports + nested-layout wiring in `@fluixi/router`,
64
+ automatic `<head>` injection in the `@fluixi/server` SSR/stream pipeline, `robots.txt`/`sitemap.xml`
65
+ emission from the `@fluixi/start` build, a devtools panel, an SEO validator, and an image-metadata
66
+ pipeline.
@@ -0,0 +1,26 @@
1
+ import type { Reactive } from './types.js';
2
+ export declare function Title(props: {
3
+ children?: Reactive<string>;
4
+ }): null;
5
+ export declare function Meta(props: {
6
+ name?: string;
7
+ property?: string;
8
+ httpEquiv?: string;
9
+ charset?: string;
10
+ content?: Reactive<string>;
11
+ }): null;
12
+ export declare function Link(props: Record<string, Reactive<string> | undefined>): null;
13
+ export declare function Script(props: {
14
+ src?: string;
15
+ type?: string;
16
+ children?: Reactive<string>;
17
+ [k: string]: unknown;
18
+ }): null;
19
+ export declare function Base(props: {
20
+ href: string;
21
+ target?: string;
22
+ }): null;
23
+ export declare function JsonLD(props: {
24
+ data: Reactive<object | object[]>;
25
+ }): null;
26
+ //# sourceMappingURL=components.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"components.d.ts","sourceRoot":"","sources":["../src/components.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAM3C,wBAAgB,KAAK,CAAC,KAAK,EAAE;IAAE,QAAQ,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAA;CAAE,GAAG,IAAI,CAGlE;AAED,wBAAgB,IAAI,CAAC,KAAK,EAAE;IAAE,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAA;CAAE,GAAG,IAAI,CAGxI;AAED,wBAAgB,IAAI,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,GAAG,SAAS,CAAC,GAAG,IAAI,CAO9E;AAED,wBAAgB,MAAM,CAAC,KAAK,EAAE;IAAE,GAAG,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAC;IAAC,CAAC,CAAC,EAAE,MAAM,GAAG,OAAO,CAAA;CAAE,GAAG,IAAI,CAOtH;AAED,wBAAgB,IAAI,CAAC,KAAK,EAAE;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAGnE;AAED,wBAAgB,MAAM,CAAC,KAAK,EAAE;IAAE,IAAI,EAAE,QAAQ,CAAC,MAAM,GAAG,MAAM,EAAE,CAAC,CAAA;CAAE,GAAG,IAAI,CAGzE"}
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Declarative head components. They render nothing — each registers metadata via useHead() and
3
+ * returns null, so they compose anywhere in the tree and work identically on the server and client
4
+ * (a signal passed to one updates the document live). Usage:
5
+ *
6
+ * <Title>{() => `${page()} · Fluixi`}</Title>
7
+ * <Meta name="description" content={desc} />
8
+ * <Link rel="canonical" href={url} />
9
+ * <JsonLD data={jsonld.article({ headline: title() })} />
10
+ */
11
+ import { useHead } from './core.js';
12
+ function read(v) {
13
+ return typeof v === 'function' ? v() : v;
14
+ }
15
+ export function Title(props) {
16
+ useHead(() => ({ title: read(props.children) }));
17
+ return null;
18
+ }
19
+ export function Meta(props) {
20
+ useHead(() => ({ meta: [{ ...props, content: read(props.content) }] }));
21
+ return null;
22
+ }
23
+ export function Link(props) {
24
+ useHead(() => {
25
+ const l = {};
26
+ for (const [k, v] of Object.entries(props))
27
+ l[k] = read(v);
28
+ return { link: [l] };
29
+ });
30
+ return null;
31
+ }
32
+ export function Script(props) {
33
+ useHead(() => {
34
+ const s = {};
35
+ for (const [k, v] of Object.entries(props))
36
+ s[k] = read(v);
37
+ return { script: [s] };
38
+ });
39
+ return null;
40
+ }
41
+ export function Base(props) {
42
+ useHead({ base: props.href });
43
+ return null;
44
+ }
45
+ export function JsonLD(props) {
46
+ useHead(() => ({ jsonLd: read(props.data) }));
47
+ return null;
48
+ }
package/dist/core.d.ts ADDED
@@ -0,0 +1,31 @@
1
+ import { type Accessor } from '@fluixi/reactive/signal';
2
+ import type { HeadSource, HeadTag } from './types.js';
3
+ export interface HeadRegistry {
4
+ /** Ordered sources — layouts register before pages, so later entries override. */
5
+ readonly sources: HeadSource[];
6
+ /** Bumps when a source is added/removed — read it in an effect to re-run on route changes. */
7
+ readonly version: Accessor<number>;
8
+ push(input: HeadSource): () => void;
9
+ }
10
+ export declare function createHead(): HeadRegistry;
11
+ export declare function setActiveHead(h: HeadRegistry | null): void;
12
+ export declare function getActiveHead(): HeadRegistry | null;
13
+ export declare function runWithHead<T>(h: HeadRegistry, fn: () => T): T;
14
+ /**
15
+ * Register metadata for the current page/layout. Static, async-resolved, or reactive — values
16
+ * that are signals update the document live on the client. Returns a disposer (client dynamic use).
17
+ */
18
+ export declare function useHead(input: HeadSource): () => void;
19
+ /** Ergonomic alias — `seo({ title, description })`. */
20
+ export declare const seo: typeof useHead;
21
+ export interface ResolvedHead {
22
+ tags: HeadTag[];
23
+ htmlAttrs: Record<string, string>;
24
+ }
25
+ /**
26
+ * Flatten every source (in registration order: layouts first, page last) into tags, then
27
+ * deduplicate by key — last write wins for singletons (title, description, canonical, …),
28
+ * while repeatable tags (extra og:image, hreflang alternates, json-ld) accumulate.
29
+ */
30
+ export declare function resolveHead(head: HeadRegistry): ResolvedHead;
31
+ //# sourceMappingURL=core.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"core.d.ts","sourceRoot":"","sources":["../src/core.ts"],"names":[],"mappings":"AAAA,OAAO,EAAqC,KAAK,QAAQ,EAAE,MAAM,yBAAyB,CAAC;AAC3F,OAAO,KAAK,EAEV,UAAU,EACV,OAAO,EAOR,MAAM,YAAY,CAAC;AAyBpB,MAAM,WAAW,YAAY;IAC3B,kFAAkF;IAClF,QAAQ,CAAC,OAAO,EAAE,UAAU,EAAE,CAAC;IAC/B,8FAA8F;IAC9F,QAAQ,CAAC,OAAO,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAC;IACnC,IAAI,CAAC,KAAK,EAAE,UAAU,GAAG,MAAM,IAAI,CAAC;CACrC;AAED,wBAAgB,UAAU,IAAI,YAAY,CAkBzC;AAKD,wBAAgB,aAAa,CAAC,CAAC,EAAE,YAAY,GAAG,IAAI,GAAG,IAAI,CAE1D;AACD,wBAAgB,aAAa,IAAI,YAAY,GAAG,IAAI,CAEnD;AACD,wBAAgB,WAAW,CAAC,CAAC,EAAE,CAAC,EAAE,YAAY,EAAE,EAAE,EAAE,MAAM,CAAC,GAAG,CAAC,CAQ9D;AAED;;;GAGG;AACH,wBAAgB,OAAO,CAAC,KAAK,EAAE,UAAU,GAAG,MAAM,IAAI,CAerD;AAED,uDAAuD;AACvD,eAAO,MAAM,GAAG,gBAAU,CAAC;AAmM3B,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,OAAO,EAAE,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACnC;AAED;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,YAAY,GAAG,YAAY,CAqB5D"}
package/dist/core.js ADDED
@@ -0,0 +1,349 @@
1
+ import { getOwner, onCleanup, createSignal } from '@fluixi/reactive/signal';
2
+ /** Read a possibly-reactive value (tracks the signal when called inside an effect). */
3
+ function val(v) {
4
+ return typeof v === 'function' ? v() : v;
5
+ }
6
+ function robotsToString(r) {
7
+ if (typeof r === 'string')
8
+ return r;
9
+ const out = [];
10
+ out.push(r.index === false ? 'noindex' : 'index');
11
+ out.push(r.follow === false ? 'nofollow' : 'follow');
12
+ if (r.noarchive)
13
+ out.push('noarchive');
14
+ if (r.nosnippet)
15
+ out.push('nosnippet');
16
+ if (r.noimageindex)
17
+ out.push('noimageindex');
18
+ if (r.maxSnippet != null)
19
+ out.push(`max-snippet:${r.maxSnippet}`);
20
+ if (r.maxImagePreview)
21
+ out.push(`max-image-preview:${r.maxImagePreview}`);
22
+ if (r.maxVideoPreview != null)
23
+ out.push(`max-video-preview:${r.maxVideoPreview}`);
24
+ return out.join(', ');
25
+ }
26
+ export function createHead() {
27
+ const sources = [];
28
+ const [version, bump] = createSignal(0);
29
+ return {
30
+ sources,
31
+ version,
32
+ push(input) {
33
+ sources.push(input);
34
+ bump((n) => n + 1);
35
+ return () => {
36
+ const i = sources.indexOf(input);
37
+ if (i !== -1) {
38
+ sources.splice(i, 1);
39
+ bump((n) => n + 1);
40
+ }
41
+ };
42
+ },
43
+ };
44
+ }
45
+ // The "active" registry. The server integration sets this per request (around the render);
46
+ // on the client there is a single registry for the document. Kept tiny and synchronous.
47
+ let active = null;
48
+ export function setActiveHead(h) {
49
+ active = h;
50
+ }
51
+ export function getActiveHead() {
52
+ return active;
53
+ }
54
+ export function runWithHead(h, fn) {
55
+ const prev = active;
56
+ active = h;
57
+ try {
58
+ return fn();
59
+ }
60
+ finally {
61
+ active = prev;
62
+ }
63
+ }
64
+ /**
65
+ * Register metadata for the current page/layout. Static, async-resolved, or reactive — values
66
+ * that are signals update the document live on the client. Returns a disposer (client dynamic use).
67
+ */
68
+ export function useHead(input) {
69
+ const h = getActiveHead();
70
+ if (!h) {
71
+ const proc = globalThis.process;
72
+ if (proc?.env?.NODE_ENV !== 'production') {
73
+ console.warn('[fluixi/head] useHead() called with no active head registry — ignored.');
74
+ }
75
+ return () => { };
76
+ }
77
+ const dispose = h.push(input);
78
+ // Drop the source when the owner (page/layout component) unmounts — this is what makes route
79
+ // transitions update the head on the client. NOT on the server: there it's a single render and
80
+ // we serialize the head before the root disposes, so the source must survive until then.
81
+ if (typeof window !== 'undefined' && getOwner())
82
+ onCleanup(dispose);
83
+ return dispose;
84
+ }
85
+ /** Ergonomic alias — `seo({ title, description })`. */
86
+ export const seo = useHead;
87
+ // ---------------------------------------------------------------------------
88
+ // Resolution (expand → dedupe)
89
+ // ---------------------------------------------------------------------------
90
+ function djb2(s) {
91
+ let h = 5381;
92
+ for (let i = 0; i < s.length; i++)
93
+ h = (h * 33) ^ s.charCodeAt(i);
94
+ return (h >>> 0).toString(36);
95
+ }
96
+ function metaKey(a) {
97
+ if (a.charset != null)
98
+ return 'meta:charset';
99
+ if (a.property)
100
+ return 'meta:property:' + a.property;
101
+ if (a.name)
102
+ return 'meta:name:' + a.name;
103
+ if (a['http-equiv'])
104
+ return 'meta:http-equiv:' + a['http-equiv'];
105
+ return 'meta:' + djb2(JSON.stringify(a));
106
+ }
107
+ function pushImage(out, img) {
108
+ const o = typeof img === 'string' ? { url: img } : img;
109
+ out.push({ tag: 'meta', key: 'meta:property:og:image:' + o.url, attrs: { property: 'og:image', content: o.url } });
110
+ if (o.secureUrl)
111
+ out.push({ tag: 'meta', key: 'meta:property:og:image:secure:' + o.url, attrs: { property: 'og:image:secure_url', content: o.secureUrl } });
112
+ if (o.type)
113
+ out.push({ tag: 'meta', key: 'meta:property:og:image:type:' + o.url, attrs: { property: 'og:image:type', content: o.type } });
114
+ if (o.width != null)
115
+ out.push({ tag: 'meta', key: 'meta:property:og:image:width:' + o.url, attrs: { property: 'og:image:width', content: String(o.width) } });
116
+ if (o.height != null)
117
+ out.push({ tag: 'meta', key: 'meta:property:og:image:height:' + o.url, attrs: { property: 'og:image:height', content: String(o.height) } });
118
+ if (o.alt)
119
+ out.push({ tag: 'meta', key: 'meta:property:og:image:alt:' + o.url, attrs: { property: 'og:image:alt', content: o.alt } });
120
+ }
121
+ function expandOG(og, out) {
122
+ const m = (p, c) => { if (c != null)
123
+ out.push({ tag: 'meta', key: 'meta:property:og:' + p, attrs: { property: 'og:' + p, content: c } }); };
124
+ m('title', og.title);
125
+ m('description', og.description);
126
+ m('type', og.type);
127
+ m('url', og.url);
128
+ m('site_name', og.siteName);
129
+ m('locale', og.locale);
130
+ for (const l of og.localeAlternate ?? [])
131
+ out.push({ tag: 'meta', key: 'meta:property:og:locale:alt:' + l, attrs: { property: 'og:locale:alternate', content: l } });
132
+ if (og.image != null) {
133
+ const imgs = Array.isArray(og.image) ? og.image : [og.image];
134
+ for (const i of imgs)
135
+ pushImage(out, i);
136
+ }
137
+ if (og.video) {
138
+ const v = typeof og.video === 'string' ? { url: og.video } : og.video;
139
+ m('video', v.url);
140
+ if ('type' in v && v.type)
141
+ m('video:type', v.type);
142
+ if ('width' in v && v.width != null)
143
+ m('video:width', String(v.width));
144
+ if ('height' in v && v.height != null)
145
+ m('video:height', String(v.height));
146
+ }
147
+ if (og.audio) {
148
+ const a = typeof og.audio === 'string' ? { url: og.audio } : og.audio;
149
+ m('audio', a.url);
150
+ if ('type' in a && a.type)
151
+ m('audio:type', a.type);
152
+ }
153
+ if (og.article) {
154
+ const ar = og.article;
155
+ m('article:published_time', ar.publishedTime);
156
+ m('article:modified_time', ar.modifiedTime);
157
+ m('article:expiration_time', ar.expirationTime);
158
+ m('article:section', ar.section);
159
+ for (const au of [].concat(ar.author ?? []))
160
+ out.push({ tag: 'meta', key: 'meta:property:og:article:author:' + au, attrs: { property: 'article:author', content: au } });
161
+ for (const t of [].concat(ar.tag ?? []))
162
+ out.push({ tag: 'meta', key: 'meta:property:og:article:tag:' + t, attrs: { property: 'article:tag', content: t } });
163
+ }
164
+ if (og.book) {
165
+ const b = og.book;
166
+ m('book:isbn', b.isbn);
167
+ m('book:release_date', b.releaseDate);
168
+ for (const au of [].concat(b.author ?? []))
169
+ out.push({ tag: 'meta', key: 'meta:property:og:book:author:' + au, attrs: { property: 'book:author', content: au } });
170
+ for (const t of [].concat(b.tag ?? []))
171
+ out.push({ tag: 'meta', key: 'meta:property:og:book:tag:' + t, attrs: { property: 'book:tag', content: t } });
172
+ }
173
+ if (og.profile) {
174
+ const p = og.profile;
175
+ m('profile:first_name', p.firstName);
176
+ m('profile:last_name', p.lastName);
177
+ m('profile:username', p.username);
178
+ m('profile:gender', p.gender);
179
+ }
180
+ }
181
+ function expandTwitter(t, out) {
182
+ const m = (n, c) => { if (c != null)
183
+ out.push({ tag: 'meta', key: 'meta:name:twitter:' + n, attrs: { name: 'twitter:' + n, content: c } }); };
184
+ m('card', t.card);
185
+ m('site', t.site);
186
+ m('creator', t.creator);
187
+ m('title', t.title);
188
+ m('description', t.description);
189
+ m('image', t.image);
190
+ m('image:alt', t.imageAlt);
191
+ m('player', t.player);
192
+ if (t.playerWidth != null)
193
+ m('player:width', String(t.playerWidth));
194
+ if (t.playerHeight != null)
195
+ m('player:height', String(t.playerHeight));
196
+ if (t.app) {
197
+ m('app:name:iphone', t.app.name);
198
+ m('app:id:iphone', t.app.idIphone);
199
+ m('app:id:ipad', t.app.idIpad);
200
+ m('app:id:googleplay', t.app.idGooglePlay);
201
+ }
202
+ }
203
+ function expandIcons(icons, out) {
204
+ const link = (key, attrs) => out.push({ tag: 'link', key, attrs });
205
+ if (icons.icon != null) {
206
+ const arr = typeof icons.icon === 'string' ? [{ url: icons.icon }] : icons.icon;
207
+ for (const i of arr)
208
+ link('link:icon:' + i.url, { rel: 'icon', href: i.url, ...(i.type ? { type: i.type } : {}), ...(i.sizes ? { sizes: i.sizes } : {}) });
209
+ }
210
+ if (icons.apple != null) {
211
+ const arr = typeof icons.apple === 'string' ? [{ url: icons.apple }] : icons.apple;
212
+ for (const i of arr)
213
+ link('link:apple-touch-icon:' + i.url, { rel: 'apple-touch-icon', href: i.url, ...(i.sizes ? { sizes: i.sizes } : {}) });
214
+ }
215
+ if (icons.mask)
216
+ link('link:mask-icon', { rel: 'mask-icon', href: icons.mask.url, ...(icons.mask.color ? { color: icons.mask.color } : {}) });
217
+ if (icons.shortcut)
218
+ link('link:shortcut', { rel: 'shortcut icon', href: icons.shortcut });
219
+ if (icons.msTileImage)
220
+ out.push({ tag: 'meta', key: 'meta:name:msapplication-TileImage', attrs: { name: 'msapplication-TileImage', content: icons.msTileImage } });
221
+ if (icons.msTileColor)
222
+ out.push({ tag: 'meta', key: 'meta:name:msapplication-TileColor', attrs: { name: 'msapplication-TileColor', content: icons.msTileColor } });
223
+ }
224
+ function expand(input, out, ts) {
225
+ const meta = (key, attrs) => out.push({ tag: 'meta', key, attrs });
226
+ const title = val(input.title);
227
+ if (title !== undefined)
228
+ ts.title = title;
229
+ if (input.titleTemplate !== undefined)
230
+ ts.template = input.titleTemplate;
231
+ const lang = val(input.lang);
232
+ if (lang !== undefined)
233
+ out.push({ tag: 'htmlAttr', key: 'html:lang', name: 'lang', value: lang });
234
+ if (input.charset !== undefined)
235
+ meta('meta:charset', { charset: input.charset });
236
+ if (input.viewport !== undefined)
237
+ meta('meta:name:viewport', { name: 'viewport', content: input.viewport });
238
+ const desc = val(input.description);
239
+ if (desc !== undefined)
240
+ meta('meta:name:description', { name: 'description', content: desc });
241
+ const kw = val(input.keywords);
242
+ if (kw !== undefined)
243
+ meta('meta:name:keywords', { name: 'keywords', content: Array.isArray(kw) ? kw.join(', ') : kw });
244
+ if (input.author !== undefined)
245
+ meta('meta:name:author', { name: 'author', content: input.author });
246
+ if (input.robots !== undefined)
247
+ meta('meta:name:robots', { name: 'robots', content: robotsToString(input.robots) });
248
+ if (input.generator !== undefined)
249
+ meta('meta:name:generator', { name: 'generator', content: input.generator });
250
+ if (input.applicationName !== undefined)
251
+ meta('meta:name:application-name', { name: 'application-name', content: input.applicationName });
252
+ if (input.referrer !== undefined)
253
+ meta('meta:name:referrer', { name: 'referrer', content: input.referrer });
254
+ if (input.colorScheme !== undefined)
255
+ meta('meta:name:color-scheme', { name: 'color-scheme', content: input.colorScheme });
256
+ const theme = val(input.themeColor);
257
+ if (theme !== undefined)
258
+ meta('meta:name:theme-color', { name: 'theme-color', content: theme });
259
+ const canonical = val(input.canonical);
260
+ if (canonical !== undefined)
261
+ out.push({ tag: 'link', key: 'link:rel:canonical', attrs: { rel: 'canonical', href: canonical } });
262
+ if (input.manifest !== undefined)
263
+ out.push({ tag: 'link', key: 'link:rel:manifest', attrs: { rel: 'manifest', href: input.manifest } });
264
+ if (input.base !== undefined)
265
+ out.push({ tag: 'base', key: 'base', attrs: { href: input.base } });
266
+ if (input.verification) {
267
+ const map = { google: 'google-site-verification', bing: 'msvalidate.01', yandex: 'yandex-verification', pinterest: 'p:domain_verify' };
268
+ for (const [k, v] of Object.entries(input.verification)) {
269
+ if (v == null)
270
+ continue;
271
+ const name = map[k] ?? k;
272
+ meta('meta:name:' + name, { name, content: v });
273
+ }
274
+ }
275
+ for (const a of input.alternates ?? [])
276
+ out.push({ tag: 'link', key: 'link:alternate:hreflang:' + a.hreflang, attrs: { rel: 'alternate', hreflang: a.hreflang, href: a.href } });
277
+ for (const f of input.feeds ?? [])
278
+ out.push({ tag: 'link', key: 'link:alternate:feed:' + f.href, attrs: { rel: 'alternate', type: f.type ?? 'application/rss+xml', href: f.href, ...(f.title ? { title: f.title } : {}) } });
279
+ if (input.og)
280
+ expandOG(input.og, out);
281
+ if (input.twitter)
282
+ expandTwitter(input.twitter, out);
283
+ if (input.icons)
284
+ expandIcons(input.icons, out);
285
+ const ld = val(input.jsonLd);
286
+ if (ld !== undefined) {
287
+ const arr = Array.isArray(ld) ? ld : [ld];
288
+ for (const obj of arr) {
289
+ const json = JSON.stringify(obj);
290
+ out.push({ tag: 'script', key: 'script:ldjson:' + djb2(json), attrs: { type: 'application/ld+json' }, children: json });
291
+ }
292
+ }
293
+ for (const m of input.meta ?? []) {
294
+ const attrs = {};
295
+ for (const [k, v] of Object.entries(m))
296
+ if (v != null)
297
+ attrs[k === 'httpEquiv' ? 'http-equiv' : k] = v;
298
+ out.push({ tag: 'meta', key: metaKey(attrs), attrs });
299
+ }
300
+ for (const l of input.link ?? []) {
301
+ const attrs = {};
302
+ for (const [k, v] of Object.entries(l))
303
+ if (v != null)
304
+ attrs[k] = v;
305
+ out.push({ tag: 'link', key: 'link:' + (attrs.rel ?? '') + ':' + (attrs.href ?? djb2(JSON.stringify(attrs))), attrs });
306
+ }
307
+ for (const s of input.script ?? []) {
308
+ const attrs = {};
309
+ let children;
310
+ for (const [k, v] of Object.entries(s)) {
311
+ if (v == null)
312
+ continue;
313
+ if (k === 'children')
314
+ children = v;
315
+ else
316
+ attrs[k] = v;
317
+ }
318
+ out.push({ tag: 'script', key: 'script:' + (attrs.src ?? djb2((children ?? '') + JSON.stringify(attrs))), attrs, children });
319
+ }
320
+ }
321
+ function applyTemplate(tpl, title) {
322
+ return typeof tpl === 'function' ? tpl(title) : tpl.includes('%s') ? tpl.replace('%s', title) : tpl;
323
+ }
324
+ /**
325
+ * Flatten every source (in registration order: layouts first, page last) into tags, then
326
+ * deduplicate by key — last write wins for singletons (title, description, canonical, …),
327
+ * while repeatable tags (extra og:image, hreflang alternates, json-ld) accumulate.
328
+ */
329
+ export function resolveHead(head) {
330
+ const raw = [];
331
+ const ts = {};
332
+ for (const src of head.sources) {
333
+ const input = typeof src === 'function' ? src() : src;
334
+ expand(input, raw, ts);
335
+ }
336
+ if (ts.title !== undefined) {
337
+ raw.push({ tag: 'title', key: 'title', children: ts.template ? applyTemplate(ts.template, ts.title) : ts.title });
338
+ }
339
+ const byKey = new Map();
340
+ const htmlAttrs = {};
341
+ for (const t of raw) {
342
+ if (t.tag === 'htmlAttr') {
343
+ htmlAttrs[t.name] = t.value;
344
+ continue;
345
+ }
346
+ byKey.set(t.key, t); // last wins
347
+ }
348
+ return { tags: [...byKey.values()], htmlAttrs };
349
+ }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * @fluixi/head — built-in, reactive, SSR-first SEO & document-head management.
3
+ *
4
+ * import { seo, jsonld } from '@fluixi/head';
5
+ * seo({ title: 'Home', description: '…', canonical: 'https://site.com/' });
6
+ * seo({ jsonLd: jsonld.website({ name: 'Site', url: 'https://site.com', searchUrl: '…?q=' }) });
7
+ *
8
+ * Static, async, or reactive (signals update the document live). SSR renders into <head>; the
9
+ * client reconciler reuses those tags by key (no hydration mismatch) and updates on signal change.
10
+ */
11
+ export type { HeadInput, HeadTag, HeadSource, Reactive, OpenGraph, OGImage, TwitterCard, RobotsMeta, AlternateLink, IconInput, MetaTag, LinkTag, ScriptTag, } from './types.js';
12
+ export { createHead, setActiveHead, getActiveHead, runWithHead, useHead, seo, resolveHead, type HeadRegistry, type ResolvedHead, } from './core.js';
13
+ export { renderHead, renderHtmlAttrs, MANAGED_ATTR, KEY_ATTR, type SerializedHead } from './serialize.js';
14
+ export { encodeHeadMarker, extractHeadMarker, stripHeadMarker } from './transport.js';
15
+ export { mountHead } from './reconcile.js';
16
+ export { jsonld } from './jsonld.js';
17
+ export * as components from './components.js';
18
+ export { Title, Meta, Link, Script, Base, JsonLD } from './components.js';
19
+ export { generateSitemap, generateSitemapIndex, chunkSitemap, generateRobots, type SitemapEntry, type SitemapImage, type SitemapVideo, type RobotsRule, type RobotsOptions, } from './sitemap.js';
20
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,YAAY,EACV,SAAS,EACT,OAAO,EACP,UAAU,EACV,QAAQ,EACR,SAAS,EACT,OAAO,EACP,WAAW,EACX,UAAU,EACV,aAAa,EACb,SAAS,EACT,OAAO,EACP,OAAO,EACP,SAAS,GACV,MAAM,YAAY,CAAC;AAEpB,OAAO,EACL,UAAU,EACV,aAAa,EACb,aAAa,EACb,WAAW,EACX,OAAO,EACP,GAAG,EACH,WAAW,EACX,KAAK,YAAY,EACjB,KAAK,YAAY,GAClB,MAAM,WAAW,CAAC;AAEnB,OAAO,EAAE,UAAU,EAAE,eAAe,EAAE,YAAY,EAAE,QAAQ,EAAE,KAAK,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAC1G,OAAO,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AACtF,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAC3C,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AACrC,OAAO,KAAK,UAAU,MAAM,iBAAiB,CAAC;AAC9C,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAC;AAC1E,OAAO,EACL,eAAe,EACf,oBAAoB,EACpB,YAAY,EACZ,cAAc,EACd,KAAK,YAAY,EACjB,KAAK,YAAY,EACjB,KAAK,YAAY,EACjB,KAAK,UAAU,EACf,KAAK,aAAa,GACnB,MAAM,cAAc,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,8 @@
1
+ export { createHead, setActiveHead, getActiveHead, runWithHead, useHead, seo, resolveHead, } from './core.js';
2
+ export { renderHead, renderHtmlAttrs, MANAGED_ATTR, KEY_ATTR } from './serialize.js';
3
+ export { encodeHeadMarker, extractHeadMarker, stripHeadMarker } from './transport.js';
4
+ export { mountHead } from './reconcile.js';
5
+ export { jsonld } from './jsonld.js';
6
+ export * as components from './components.js';
7
+ export { Title, Meta, Link, Script, Base, JsonLD } from './components.js';
8
+ export { generateSitemap, generateSitemapIndex, chunkSitemap, generateRobots, } from './sitemap.js';