@fluixi/head 0.1.0-alpha.22 → 1.0.0-alpha.101
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 +1 -1
- package/README.md +8 -8
- package/dist/components.cjs +91 -0
- package/dist/components.js +1 -1
- package/dist/components.mjs +68 -0
- package/dist/core.cjs +314 -0
- package/dist/core.d.ts +10 -5
- package/dist/core.d.ts.map +1 -1
- package/dist/core.js +35 -17
- package/dist/core.mjs +293 -0
- package/dist/index.cjs +672 -0
- package/dist/index.d.ts +5 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.mjs +655 -0
- package/dist/jsonld.cjs +94 -0
- package/dist/jsonld.js +1 -1
- package/dist/jsonld.mjs +73 -0
- package/dist/reconcile.cjs +323 -0
- package/dist/reconcile.d.ts +1 -1
- package/dist/reconcile.js +1 -1
- package/dist/reconcile.mjs +302 -0
- package/dist/serialize.cjs +306 -0
- package/dist/serialize.mjs +283 -0
- package/dist/sitemap.cjs +115 -0
- package/dist/sitemap.d.ts +1 -1
- package/dist/sitemap.d.ts.map +1 -1
- package/dist/sitemap.js +1 -1
- package/dist/sitemap.mjs +94 -0
- package/dist/transport.cjs +61 -0
- package/dist/transport.d.ts +3 -3
- package/dist/transport.js +4 -4
- package/dist/transport.mjs +40 -0
- package/dist/tsconfig.lib.tsbuildinfo +1 -1
- package/dist/types.cjs +18 -0
- package/dist/types.d.ts +21 -5
- package/dist/types.d.ts.map +1 -1
- package/dist/types.mjs +0 -0
- package/package.json +12 -6
package/LICENSE
CHANGED
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @fluixi/head
|
|
2
2
|
|
|
3
|
-
Built-in, **reactive, SSR-first** SEO & document-head management for Fluixi. No external library
|
|
3
|
+
Built-in, **reactive, SSR-first** SEO & document-head management for Fluixi. No external library: it's a core part of the framework.
|
|
4
4
|
|
|
5
5
|
```ts
|
|
6
6
|
import { seo, jsonld } from '@fluixi/head';
|
|
@@ -18,17 +18,17 @@ seo({
|
|
|
18
18
|
|
|
19
19
|
## What it does
|
|
20
20
|
|
|
21
|
-
- **Reactive
|
|
21
|
+
- **Reactive**: pass a signal (or a function reading one) and the document updates live:
|
|
22
22
|
```ts
|
|
23
|
-
const
|
|
23
|
+
const title = $signal('Dashboard');
|
|
24
24
|
seo(() => ({ title: title() })); // document.title tracks the signal — no reload
|
|
25
25
|
```
|
|
26
|
-
- **SSR-first
|
|
26
|
+
- **SSR-first**: `renderHead(registry)` serializes the resolved tags into `<head>` markup plus
|
|
27
27
|
`<html>` attributes; the client reconciler reuses those tags **by key** → no hydration mismatch.
|
|
28
|
-
- **Deduplicated
|
|
28
|
+
- **Deduplicated**: one title, one description, one canonical; repeatable tags (extra `og:image`,
|
|
29
29
|
hreflang alternates, JSON-LD) accumulate. Last write wins for singletons.
|
|
30
|
-
- **Nested inheritance
|
|
31
|
-
- **Typed
|
|
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
32
|
|
|
33
33
|
## Surface
|
|
34
34
|
|
|
@@ -46,7 +46,7 @@ product · breadcrumb · videoObject · event · localBusiness · softwareApplic
|
|
|
46
46
|
|
|
47
47
|
### Components
|
|
48
48
|
|
|
49
|
-
`<Title>`, `<Meta>`, `<Link>`, `<Script>`, `<Base>`, `<JsonLD
|
|
49
|
+
`<Title>`, `<Meta>`, `<Link>`, `<Script>`, `<Base>`, `<JsonLD>`: render nothing, register metadata,
|
|
50
50
|
work on server + client (a signal passed in updates the document live).
|
|
51
51
|
|
|
52
52
|
### Sitemap & robots
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/components.ts
|
|
21
|
+
var components_exports = {};
|
|
22
|
+
__export(components_exports, {
|
|
23
|
+
Base: () => Base,
|
|
24
|
+
JsonLD: () => JsonLD,
|
|
25
|
+
Link: () => Link,
|
|
26
|
+
Meta: () => Meta,
|
|
27
|
+
Script: () => Script,
|
|
28
|
+
Title: () => Title
|
|
29
|
+
});
|
|
30
|
+
module.exports = __toCommonJS(components_exports);
|
|
31
|
+
|
|
32
|
+
// src/core.ts
|
|
33
|
+
var import_signal = require("@fluixi/reactive/signal");
|
|
34
|
+
var slot = globalThis[/* @__PURE__ */ Symbol.for("fluixi.head")] ??= {
|
|
35
|
+
active: null,
|
|
36
|
+
scope: null
|
|
37
|
+
};
|
|
38
|
+
function getActiveHead() {
|
|
39
|
+
return slot.scope?.() ?? slot.active;
|
|
40
|
+
}
|
|
41
|
+
function useHead(input) {
|
|
42
|
+
const h = getActiveHead();
|
|
43
|
+
if (!h) {
|
|
44
|
+
const proc = globalThis.process;
|
|
45
|
+
if (proc?.env?.NODE_ENV !== "production") {
|
|
46
|
+
console.warn("[fluixi/head] useHead() called with no active head registry — ignored.");
|
|
47
|
+
}
|
|
48
|
+
return () => {
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
const dispose = h.push(input);
|
|
52
|
+
if (typeof window !== "undefined" && (0, import_signal.getOwner)()) (0, import_signal.onCleanup)(dispose);
|
|
53
|
+
return dispose;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// src/components.ts
|
|
57
|
+
function read(v) {
|
|
58
|
+
return typeof v === "function" ? v() : v;
|
|
59
|
+
}
|
|
60
|
+
function Title(props) {
|
|
61
|
+
useHead(() => ({ title: read(props.children) }));
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
function Meta(props) {
|
|
65
|
+
useHead(() => ({ meta: [{ ...props, content: read(props.content) }] }));
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
function Link(props) {
|
|
69
|
+
useHead(() => {
|
|
70
|
+
const l = {};
|
|
71
|
+
for (const [k, v] of Object.entries(props)) l[k] = read(v);
|
|
72
|
+
return { link: [l] };
|
|
73
|
+
});
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
function Script(props) {
|
|
77
|
+
useHead(() => {
|
|
78
|
+
const s = {};
|
|
79
|
+
for (const [k, v] of Object.entries(props)) s[k] = read(v);
|
|
80
|
+
return { script: [s] };
|
|
81
|
+
});
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
function Base(props) {
|
|
85
|
+
useHead({ base: props.href });
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
function JsonLD(props) {
|
|
89
|
+
useHead(() => ({ jsonLd: read(props.data) }));
|
|
90
|
+
return null;
|
|
91
|
+
}
|
package/dist/components.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Declarative head components. They render nothing
|
|
2
|
+
* Declarative head components. They render nothing, each registers metadata via useHead() and
|
|
3
3
|
* returns null, so they compose anywhere in the tree and work identically on the server and client
|
|
4
4
|
* (a signal passed to one updates the document live). Usage:
|
|
5
5
|
*
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// src/core.ts
|
|
2
|
+
import { getOwner, onCleanup, createSignal } from "@fluixi/reactive/signal";
|
|
3
|
+
var slot = globalThis[/* @__PURE__ */ Symbol.for("fluixi.head")] ??= {
|
|
4
|
+
active: null,
|
|
5
|
+
scope: null
|
|
6
|
+
};
|
|
7
|
+
function getActiveHead() {
|
|
8
|
+
return slot.scope?.() ?? slot.active;
|
|
9
|
+
}
|
|
10
|
+
function useHead(input) {
|
|
11
|
+
const h = getActiveHead();
|
|
12
|
+
if (!h) {
|
|
13
|
+
const proc = globalThis.process;
|
|
14
|
+
if (proc?.env?.NODE_ENV !== "production") {
|
|
15
|
+
console.warn("[fluixi/head] useHead() called with no active head registry — ignored.");
|
|
16
|
+
}
|
|
17
|
+
return () => {
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
const dispose = h.push(input);
|
|
21
|
+
if (typeof window !== "undefined" && getOwner()) onCleanup(dispose);
|
|
22
|
+
return dispose;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// src/components.ts
|
|
26
|
+
function read(v) {
|
|
27
|
+
return typeof v === "function" ? v() : v;
|
|
28
|
+
}
|
|
29
|
+
function Title(props) {
|
|
30
|
+
useHead(() => ({ title: read(props.children) }));
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
function Meta(props) {
|
|
34
|
+
useHead(() => ({ meta: [{ ...props, content: read(props.content) }] }));
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
function Link(props) {
|
|
38
|
+
useHead(() => {
|
|
39
|
+
const l = {};
|
|
40
|
+
for (const [k, v] of Object.entries(props)) l[k] = read(v);
|
|
41
|
+
return { link: [l] };
|
|
42
|
+
});
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
function Script(props) {
|
|
46
|
+
useHead(() => {
|
|
47
|
+
const s = {};
|
|
48
|
+
for (const [k, v] of Object.entries(props)) s[k] = read(v);
|
|
49
|
+
return { script: [s] };
|
|
50
|
+
});
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
function Base(props) {
|
|
54
|
+
useHead({ base: props.href });
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
function JsonLD(props) {
|
|
58
|
+
useHead(() => ({ jsonLd: read(props.data) }));
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
export {
|
|
62
|
+
Base,
|
|
63
|
+
JsonLD,
|
|
64
|
+
Link,
|
|
65
|
+
Meta,
|
|
66
|
+
Script,
|
|
67
|
+
Title
|
|
68
|
+
};
|
package/dist/core.cjs
ADDED
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/core.ts
|
|
21
|
+
var core_exports = {};
|
|
22
|
+
__export(core_exports, {
|
|
23
|
+
createHead: () => createHead,
|
|
24
|
+
getActiveHead: () => getActiveHead,
|
|
25
|
+
resolveHead: () => resolveHead,
|
|
26
|
+
runWithHead: () => runWithHead,
|
|
27
|
+
seo: () => seo,
|
|
28
|
+
setActiveHead: () => setActiveHead,
|
|
29
|
+
setHeadScope: () => setHeadScope,
|
|
30
|
+
useHead: () => useHead
|
|
31
|
+
});
|
|
32
|
+
module.exports = __toCommonJS(core_exports);
|
|
33
|
+
var import_signal = require("@fluixi/reactive/signal");
|
|
34
|
+
function val(v) {
|
|
35
|
+
return typeof v === "function" ? v() : v;
|
|
36
|
+
}
|
|
37
|
+
function robotsToString(r) {
|
|
38
|
+
if (typeof r === "string") return r;
|
|
39
|
+
const out = [];
|
|
40
|
+
out.push(r.index === false ? "noindex" : "index");
|
|
41
|
+
out.push(r.follow === false ? "nofollow" : "follow");
|
|
42
|
+
if (r.noarchive) out.push("noarchive");
|
|
43
|
+
if (r.nosnippet) out.push("nosnippet");
|
|
44
|
+
if (r.noimageindex) out.push("noimageindex");
|
|
45
|
+
if (r.maxSnippet != null) out.push(`max-snippet:${r.maxSnippet}`);
|
|
46
|
+
if (r.maxImagePreview) out.push(`max-image-preview:${r.maxImagePreview}`);
|
|
47
|
+
if (r.maxVideoPreview != null) out.push(`max-video-preview:${r.maxVideoPreview}`);
|
|
48
|
+
return out.join(", ");
|
|
49
|
+
}
|
|
50
|
+
function createHead() {
|
|
51
|
+
const sources = [];
|
|
52
|
+
const [version, bump] = (0, import_signal.createSignal)(0);
|
|
53
|
+
return {
|
|
54
|
+
sources,
|
|
55
|
+
version,
|
|
56
|
+
push(input) {
|
|
57
|
+
sources.push(input);
|
|
58
|
+
bump((n) => n + 1);
|
|
59
|
+
return () => {
|
|
60
|
+
const i = sources.indexOf(input);
|
|
61
|
+
if (i !== -1) {
|
|
62
|
+
sources.splice(i, 1);
|
|
63
|
+
bump((n) => n + 1);
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
var slot = globalThis[/* @__PURE__ */ Symbol.for("fluixi.head")] ??= {
|
|
70
|
+
active: null,
|
|
71
|
+
scope: null
|
|
72
|
+
};
|
|
73
|
+
function setActiveHead(h) {
|
|
74
|
+
slot.active = h;
|
|
75
|
+
}
|
|
76
|
+
function getActiveHead() {
|
|
77
|
+
return slot.scope?.() ?? slot.active;
|
|
78
|
+
}
|
|
79
|
+
function runWithHead(h, fn) {
|
|
80
|
+
const prev = slot.active;
|
|
81
|
+
slot.active = h;
|
|
82
|
+
try {
|
|
83
|
+
return fn();
|
|
84
|
+
} finally {
|
|
85
|
+
slot.active = prev;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
function setHeadScope(scope) {
|
|
89
|
+
slot.scope = scope;
|
|
90
|
+
}
|
|
91
|
+
function useHead(input) {
|
|
92
|
+
const h = getActiveHead();
|
|
93
|
+
if (!h) {
|
|
94
|
+
const proc = globalThis.process;
|
|
95
|
+
if (proc?.env?.NODE_ENV !== "production") {
|
|
96
|
+
console.warn("[fluixi/head] useHead() called with no active head registry — ignored.");
|
|
97
|
+
}
|
|
98
|
+
return () => {
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
const dispose = h.push(input);
|
|
102
|
+
if (typeof window !== "undefined" && (0, import_signal.getOwner)()) (0, import_signal.onCleanup)(dispose);
|
|
103
|
+
return dispose;
|
|
104
|
+
}
|
|
105
|
+
var seo = useHead;
|
|
106
|
+
function djb2(s) {
|
|
107
|
+
let h = 5381;
|
|
108
|
+
for (let i = 0; i < s.length; i++) h = h * 33 ^ s.charCodeAt(i);
|
|
109
|
+
return (h >>> 0).toString(36);
|
|
110
|
+
}
|
|
111
|
+
function metaKey(a) {
|
|
112
|
+
if (a.charset != null) return "meta:charset";
|
|
113
|
+
if (a.property) return "meta:property:" + a.property;
|
|
114
|
+
if (a.name) return "meta:name:" + a.name;
|
|
115
|
+
if (a["http-equiv"]) return "meta:http-equiv:" + a["http-equiv"];
|
|
116
|
+
return "meta:" + djb2(JSON.stringify(a));
|
|
117
|
+
}
|
|
118
|
+
function pushImage(out, img) {
|
|
119
|
+
const o = typeof img === "string" ? { url: img } : img;
|
|
120
|
+
out.push({ tag: "meta", key: "meta:property:og:image:" + o.url, attrs: { property: "og:image", content: o.url } });
|
|
121
|
+
if (o.secureUrl) out.push({ tag: "meta", key: "meta:property:og:image:secure:" + o.url, attrs: { property: "og:image:secure_url", content: o.secureUrl } });
|
|
122
|
+
if (o.type) out.push({ tag: "meta", key: "meta:property:og:image:type:" + o.url, attrs: { property: "og:image:type", content: o.type } });
|
|
123
|
+
if (o.width != null) out.push({ tag: "meta", key: "meta:property:og:image:width:" + o.url, attrs: { property: "og:image:width", content: String(o.width) } });
|
|
124
|
+
if (o.height != null) out.push({ tag: "meta", key: "meta:property:og:image:height:" + o.url, attrs: { property: "og:image:height", content: String(o.height) } });
|
|
125
|
+
if (o.alt) out.push({ tag: "meta", key: "meta:property:og:image:alt:" + o.url, attrs: { property: "og:image:alt", content: o.alt } });
|
|
126
|
+
}
|
|
127
|
+
function expandOG(og, out) {
|
|
128
|
+
const m = (p, c) => {
|
|
129
|
+
if (c != null) out.push({ tag: "meta", key: "meta:property:og:" + p, attrs: { property: "og:" + p, content: c } });
|
|
130
|
+
};
|
|
131
|
+
m("title", og.title);
|
|
132
|
+
m("description", og.description);
|
|
133
|
+
m("type", og.type);
|
|
134
|
+
m("url", og.url);
|
|
135
|
+
m("site_name", og.siteName);
|
|
136
|
+
m("locale", og.locale);
|
|
137
|
+
for (const l of og.localeAlternate ?? []) out.push({ tag: "meta", key: "meta:property:og:locale:alt:" + l, attrs: { property: "og:locale:alternate", content: l } });
|
|
138
|
+
if (og.image != null) {
|
|
139
|
+
const imgs = Array.isArray(og.image) ? og.image : [og.image];
|
|
140
|
+
for (const i of imgs) pushImage(out, i);
|
|
141
|
+
}
|
|
142
|
+
if (og.video) {
|
|
143
|
+
const v = typeof og.video === "string" ? { url: og.video } : og.video;
|
|
144
|
+
m("video", v.url);
|
|
145
|
+
if ("type" in v && v.type) m("video:type", v.type);
|
|
146
|
+
if ("width" in v && v.width != null) m("video:width", String(v.width));
|
|
147
|
+
if ("height" in v && v.height != null) m("video:height", String(v.height));
|
|
148
|
+
}
|
|
149
|
+
if (og.audio) {
|
|
150
|
+
const a = typeof og.audio === "string" ? { url: og.audio } : og.audio;
|
|
151
|
+
m("audio", a.url);
|
|
152
|
+
if ("type" in a && a.type) m("audio:type", a.type);
|
|
153
|
+
}
|
|
154
|
+
if (og.article) {
|
|
155
|
+
const ar = og.article;
|
|
156
|
+
m("article:published_time", ar.publishedTime);
|
|
157
|
+
m("article:modified_time", ar.modifiedTime);
|
|
158
|
+
m("article:expiration_time", ar.expirationTime);
|
|
159
|
+
m("article:section", ar.section);
|
|
160
|
+
for (const au of [].concat(ar.author ?? [])) 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 ?? [])) out.push({ tag: "meta", key: "meta:property:og:article:tag:" + t, attrs: { property: "article:tag", content: t } });
|
|
162
|
+
}
|
|
163
|
+
if (og.book) {
|
|
164
|
+
const b = og.book;
|
|
165
|
+
m("book:isbn", b.isbn);
|
|
166
|
+
m("book:release_date", b.releaseDate);
|
|
167
|
+
for (const au of [].concat(b.author ?? [])) out.push({ tag: "meta", key: "meta:property:og:book:author:" + au, attrs: { property: "book:author", content: au } });
|
|
168
|
+
for (const t of [].concat(b.tag ?? [])) out.push({ tag: "meta", key: "meta:property:og:book:tag:" + t, attrs: { property: "book:tag", content: t } });
|
|
169
|
+
}
|
|
170
|
+
if (og.profile) {
|
|
171
|
+
const p = og.profile;
|
|
172
|
+
m("profile:first_name", p.firstName);
|
|
173
|
+
m("profile:last_name", p.lastName);
|
|
174
|
+
m("profile:username", p.username);
|
|
175
|
+
m("profile:gender", p.gender);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
function expandTwitter(t, out) {
|
|
179
|
+
const m = (n, c) => {
|
|
180
|
+
if (c != null) out.push({ tag: "meta", key: "meta:name:twitter:" + n, attrs: { name: "twitter:" + n, content: c } });
|
|
181
|
+
};
|
|
182
|
+
m("card", t.card);
|
|
183
|
+
m("site", t.site);
|
|
184
|
+
m("creator", t.creator);
|
|
185
|
+
m("title", t.title);
|
|
186
|
+
m("description", t.description);
|
|
187
|
+
m("image", t.image);
|
|
188
|
+
m("image:alt", t.imageAlt);
|
|
189
|
+
m("player", t.player);
|
|
190
|
+
if (t.playerWidth != null) m("player:width", String(t.playerWidth));
|
|
191
|
+
if (t.playerHeight != null) m("player:height", String(t.playerHeight));
|
|
192
|
+
if (t.app) {
|
|
193
|
+
m("app:name:iphone", t.app.name);
|
|
194
|
+
m("app:id:iphone", t.app.idIphone);
|
|
195
|
+
m("app:id:ipad", t.app.idIpad);
|
|
196
|
+
m("app:id:googleplay", t.app.idGooglePlay);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
function expandIcons(icons, out) {
|
|
200
|
+
const link = (key, attrs) => out.push({ tag: "link", key, attrs });
|
|
201
|
+
if (icons.icon != null) {
|
|
202
|
+
const arr = typeof icons.icon === "string" ? [{ url: icons.icon }] : icons.icon;
|
|
203
|
+
for (const i of arr) link("link:icon:" + i.url, { rel: "icon", href: i.url, ...i.type ? { type: i.type } : {}, ...i.sizes ? { sizes: i.sizes } : {} });
|
|
204
|
+
}
|
|
205
|
+
if (icons.apple != null) {
|
|
206
|
+
const arr = typeof icons.apple === "string" ? [{ url: icons.apple }] : icons.apple;
|
|
207
|
+
for (const i of arr) link("link:apple-touch-icon:" + i.url, { rel: "apple-touch-icon", href: i.url, ...i.sizes ? { sizes: i.sizes } : {} });
|
|
208
|
+
}
|
|
209
|
+
if (icons.mask) link("link:mask-icon", { rel: "mask-icon", href: icons.mask.url, ...icons.mask.color ? { color: icons.mask.color } : {} });
|
|
210
|
+
if (icons.shortcut) link("link:shortcut", { rel: "shortcut icon", href: icons.shortcut });
|
|
211
|
+
if (icons.msTileImage) out.push({ tag: "meta", key: "meta:name:msapplication-TileImage", attrs: { name: "msapplication-TileImage", content: icons.msTileImage } });
|
|
212
|
+
if (icons.msTileColor) out.push({ tag: "meta", key: "meta:name:msapplication-TileColor", attrs: { name: "msapplication-TileColor", content: icons.msTileColor } });
|
|
213
|
+
}
|
|
214
|
+
function expand(input, out, ts) {
|
|
215
|
+
const meta = (key, attrs) => out.push({ tag: "meta", key, attrs });
|
|
216
|
+
const title = val(input.title);
|
|
217
|
+
if (title !== void 0) ts.title = title;
|
|
218
|
+
if (input.titleTemplate !== void 0) ts.template = input.titleTemplate;
|
|
219
|
+
const lang = val(input.lang);
|
|
220
|
+
if (lang !== void 0) out.push({ tag: "htmlAttr", key: "html:lang", name: "lang", value: lang });
|
|
221
|
+
for (const [name, raw] of Object.entries(input.htmlAttrs ?? {})) {
|
|
222
|
+
const value = val(raw);
|
|
223
|
+
if (value !== void 0) out.push({ tag: "htmlAttr", key: `html:${name}`, name, value });
|
|
224
|
+
}
|
|
225
|
+
if (input.charset !== void 0) meta("meta:charset", { charset: input.charset });
|
|
226
|
+
if (input.viewport !== void 0) meta("meta:name:viewport", { name: "viewport", content: input.viewport });
|
|
227
|
+
const desc = val(input.description);
|
|
228
|
+
if (desc !== void 0) meta("meta:name:description", { name: "description", content: desc });
|
|
229
|
+
const kw = val(input.keywords);
|
|
230
|
+
if (kw !== void 0) meta("meta:name:keywords", { name: "keywords", content: Array.isArray(kw) ? kw.join(", ") : kw });
|
|
231
|
+
if (input.author !== void 0) meta("meta:name:author", { name: "author", content: input.author });
|
|
232
|
+
if (input.robots !== void 0) meta("meta:name:robots", { name: "robots", content: robotsToString(input.robots) });
|
|
233
|
+
if (input.generator !== void 0) meta("meta:name:generator", { name: "generator", content: input.generator });
|
|
234
|
+
if (input.applicationName !== void 0) meta("meta:name:application-name", { name: "application-name", content: input.applicationName });
|
|
235
|
+
if (input.referrer !== void 0) meta("meta:name:referrer", { name: "referrer", content: input.referrer });
|
|
236
|
+
const scheme = val(input.colorScheme);
|
|
237
|
+
if (scheme !== void 0) meta("meta:name:color-scheme", { name: "color-scheme", content: scheme });
|
|
238
|
+
const theme = val(input.themeColor);
|
|
239
|
+
if (typeof theme === "string") meta("meta:name:theme-color", { name: "theme-color", content: theme });
|
|
240
|
+
else if (theme) {
|
|
241
|
+
for (const { color, media } of theme) {
|
|
242
|
+
meta(`meta:name:theme-color${media ? `:${media}` : ""}`, media ? { name: "theme-color", content: color, media } : { name: "theme-color", content: color });
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
const canonical = val(input.canonical);
|
|
246
|
+
if (canonical !== void 0) out.push({ tag: "link", key: "link:rel:canonical", attrs: { rel: "canonical", href: canonical } });
|
|
247
|
+
if (input.manifest !== void 0) out.push({ tag: "link", key: "link:rel:manifest", attrs: { rel: "manifest", href: input.manifest } });
|
|
248
|
+
if (input.base !== void 0) out.push({ tag: "base", key: "base", attrs: { href: input.base } });
|
|
249
|
+
if (input.verification) {
|
|
250
|
+
const map = { google: "google-site-verification", bing: "msvalidate.01", yandex: "yandex-verification", pinterest: "p:domain_verify" };
|
|
251
|
+
for (const [k, v] of Object.entries(input.verification)) {
|
|
252
|
+
if (v == null) continue;
|
|
253
|
+
const name = map[k] ?? k;
|
|
254
|
+
meta("meta:name:" + name, { name, content: v });
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
for (const a of input.alternates ?? []) out.push({ tag: "link", key: "link:alternate:hreflang:" + a.hreflang, attrs: { rel: "alternate", hreflang: a.hreflang, href: a.href } });
|
|
258
|
+
for (const f of input.feeds ?? []) 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 } : {} } });
|
|
259
|
+
if (input.og) expandOG(input.og, out);
|
|
260
|
+
if (input.twitter) expandTwitter(input.twitter, out);
|
|
261
|
+
if (input.icons) expandIcons(input.icons, out);
|
|
262
|
+
const ld = val(input.jsonLd);
|
|
263
|
+
if (ld !== void 0) {
|
|
264
|
+
const arr = Array.isArray(ld) ? ld : [ld];
|
|
265
|
+
for (const obj of arr) {
|
|
266
|
+
const json = JSON.stringify(obj);
|
|
267
|
+
out.push({ tag: "script", key: "script:ldjson:" + djb2(json), attrs: { type: "application/ld+json" }, children: json });
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
for (const m of input.meta ?? []) {
|
|
271
|
+
const attrs = {};
|
|
272
|
+
for (const [k, v] of Object.entries(m)) if (v != null) attrs[k === "httpEquiv" ? "http-equiv" : k] = v;
|
|
273
|
+
out.push({ tag: "meta", key: metaKey(attrs), attrs });
|
|
274
|
+
}
|
|
275
|
+
for (const l of input.link ?? []) {
|
|
276
|
+
const attrs = {};
|
|
277
|
+
for (const [k, v] of Object.entries(l)) if (v != null) attrs[k] = v;
|
|
278
|
+
out.push({ tag: "link", key: "link:" + (attrs.rel ?? "") + ":" + (attrs.href ?? djb2(JSON.stringify(attrs))), attrs });
|
|
279
|
+
}
|
|
280
|
+
for (const s of input.script ?? []) {
|
|
281
|
+
const attrs = {};
|
|
282
|
+
let children;
|
|
283
|
+
for (const [k, v] of Object.entries(s)) {
|
|
284
|
+
if (v == null) continue;
|
|
285
|
+
if (k === "children") children = v;
|
|
286
|
+
else attrs[k] = v;
|
|
287
|
+
}
|
|
288
|
+
out.push({ tag: "script", key: "script:" + (attrs.src ?? djb2((children ?? "") + JSON.stringify(attrs))), attrs, children });
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
function applyTemplate(tpl, title) {
|
|
292
|
+
return typeof tpl === "function" ? tpl(title) : tpl.includes("%s") ? tpl.replace("%s", title) : tpl;
|
|
293
|
+
}
|
|
294
|
+
function resolveHead(head) {
|
|
295
|
+
const raw = [];
|
|
296
|
+
const ts = {};
|
|
297
|
+
for (const src of head.sources) {
|
|
298
|
+
const input = typeof src === "function" ? src() : src;
|
|
299
|
+
expand(input, raw, ts);
|
|
300
|
+
}
|
|
301
|
+
if (ts.title !== void 0) {
|
|
302
|
+
raw.push({ tag: "title", key: "title", children: ts.template ? applyTemplate(ts.template, ts.title) : ts.title });
|
|
303
|
+
}
|
|
304
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
305
|
+
const htmlAttrs = {};
|
|
306
|
+
for (const t of raw) {
|
|
307
|
+
if (t.tag === "htmlAttr") {
|
|
308
|
+
htmlAttrs[t.name] = t.value;
|
|
309
|
+
continue;
|
|
310
|
+
}
|
|
311
|
+
byKey.set(t.key, t);
|
|
312
|
+
}
|
|
313
|
+
return { tags: [...byKey.values()], htmlAttrs };
|
|
314
|
+
}
|
package/dist/core.d.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { type Accessor } from '@fluixi/reactive/signal';
|
|
2
2
|
import type { HeadSource, HeadTag } from './types.js';
|
|
3
3
|
export interface HeadRegistry {
|
|
4
|
-
/** Ordered sources
|
|
4
|
+
/** Ordered sources: layouts register before pages, so later entries override. */
|
|
5
5
|
readonly sources: HeadSource[];
|
|
6
|
-
/** Bumps when a source is added/removed
|
|
6
|
+
/** Bumps when a source is added/removed: read it in an effect to re-run on route changes. */
|
|
7
7
|
readonly version: Accessor<number>;
|
|
8
8
|
push(input: HeadSource): () => void;
|
|
9
9
|
}
|
|
@@ -12,11 +12,16 @@ export declare function setActiveHead(h: HeadRegistry | null): void;
|
|
|
12
12
|
export declare function getActiveHead(): HeadRegistry | null;
|
|
13
13
|
export declare function runWithHead<T>(h: HeadRegistry, fn: () => T): T;
|
|
14
14
|
/**
|
|
15
|
-
*
|
|
15
|
+
* Resolve the active registry from a host's own request scope, e.g. its AsyncLocalStorage.
|
|
16
|
+
* Returning nothing falls back to `setActiveHead`. `null` removes the scope.
|
|
17
|
+
*/
|
|
18
|
+
export declare function setHeadScope(scope: (() => HeadRegistry | null | undefined) | null): void;
|
|
19
|
+
/**
|
|
20
|
+
* Register metadata for the current page/layout. Static, async-resolved, or reactive, values
|
|
16
21
|
* that are signals update the document live on the client. Returns a disposer (client dynamic use).
|
|
17
22
|
*/
|
|
18
23
|
export declare function useHead(input: HeadSource): () => void;
|
|
19
|
-
/** Ergonomic alias
|
|
24
|
+
/** Ergonomic alias: `seo({ title, description })`. */
|
|
20
25
|
export declare const seo: typeof useHead;
|
|
21
26
|
export interface ResolvedHead {
|
|
22
27
|
tags: HeadTag[];
|
|
@@ -24,7 +29,7 @@ export interface ResolvedHead {
|
|
|
24
29
|
}
|
|
25
30
|
/**
|
|
26
31
|
* Flatten every source (in registration order: layouts first, page last) into tags, then
|
|
27
|
-
* deduplicate by key
|
|
32
|
+
* deduplicate by key: last write wins for singletons (title, description, canonical, ...),
|
|
28
33
|
* while repeatable tags (extra og:image, hreflang alternates, json-ld) accumulate.
|
|
29
34
|
*/
|
|
30
35
|
export declare function resolveHead(head: HeadRegistry): ResolvedHead;
|
package/dist/core.d.ts.map
CHANGED
|
@@ -1 +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;
|
|
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;AAuBpB,MAAM,WAAW,YAAY;IAC3B,iFAAiF;IACjF,QAAQ,CAAC,OAAO,EAAE,UAAU,EAAE,CAAC;IAC/B,6FAA6F;IAC7F,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;AAmBD,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;AACD;;;GAGG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,CAAC,MAAM,YAAY,GAAG,IAAI,GAAG,SAAS,CAAC,GAAG,IAAI,GAAG,IAAI,CAExF;AAED;;;GAGG;AACH,wBAAgB,OAAO,CAAC,KAAK,EAAE,UAAU,GAAG,MAAM,IAAI,CAerD;AAED,sDAAsD;AACtD,eAAO,MAAM,GAAG,gBAAU,CAAC;AA4M3B,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"}
|