katanakit-js 2.2.1 → 2.12.9
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/README.md +72 -437
- package/dist/adapters/astro/astro.service.d.ts +103 -15
- package/dist/adapters/astro/astro.service.d.ts.map +1 -1
- package/dist/adapters/astro/astro.service.js +145 -75
- package/dist/adapters/astro/astro.service.js.map +1 -1
- package/dist/adapters/astro/index.d.ts +1 -1
- package/dist/adapters/astro/index.d.ts.map +1 -1
- package/dist/adapters/astro/index.js +1 -1
- package/dist/adapters/astro/index.js.map +1 -1
- package/dist/adapters/astro/rss.service.d.ts +85 -105
- package/dist/adapters/astro/rss.service.d.ts.map +1 -1
- package/dist/adapters/astro/rss.service.js +172 -168
- package/dist/adapters/astro/rss.service.js.map +1 -1
- package/dist/adapters/astro/seo.service.d.ts +10 -0
- package/dist/adapters/astro/seo.service.d.ts.map +1 -0
- package/dist/adapters/astro/seo.service.js +3 -0
- package/dist/adapters/astro/seo.service.js.map +1 -0
- package/dist/adapters/express/app.d.ts +30 -1
- package/dist/adapters/express/app.d.ts.map +1 -1
- package/dist/adapters/express/app.js +31 -3
- package/dist/adapters/express/app.js.map +1 -1
- package/dist/adapters/express/index.d.ts +2 -2
- package/dist/adapters/express/index.d.ts.map +1 -1
- package/dist/adapters/express/index.js +2 -2
- package/dist/adapters/express/index.js.map +1 -1
- package/dist/adapters/express/main.js +2 -2
- package/dist/adapters/express/main.js.map +1 -1
- package/dist/adapters/express/products.controller.d.ts +67 -8
- package/dist/adapters/express/products.controller.d.ts.map +1 -1
- package/dist/adapters/express/products.controller.js +105 -49
- package/dist/adapters/express/products.controller.js.map +1 -1
- package/dist/adapters/express/server.d.ts +37 -13
- package/dist/adapters/express/server.d.ts.map +1 -1
- package/dist/adapters/express/server.js +118 -53
- package/dist/adapters/express/server.js.map +1 -1
- package/dist/adapters/nuxt/nuxt.service.d.ts +8 -6
- package/dist/adapters/nuxt/nuxt.service.d.ts.map +1 -1
- package/dist/adapters/nuxt/nuxt.service.js +34 -10
- package/dist/adapters/nuxt/nuxt.service.js.map +1 -1
- package/dist/adapters/vue/vue.service.d.ts +5 -2
- package/dist/adapters/vue/vue.service.d.ts.map +1 -1
- package/dist/adapters/vue/vue.service.js +41 -7
- package/dist/adapters/vue/vue.service.js.map +1 -1
- package/dist/config/index.d.ts +2 -1
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/index.js +1 -1
- package/dist/config/index.js.map +1 -1
- package/dist/config/seo-meta.types.d.ts +177 -0
- package/dist/config/seo-meta.types.d.ts.map +1 -0
- package/dist/config/seo-meta.types.js +11 -0
- package/dist/config/seo-meta.types.js.map +1 -0
- package/dist/config/seo.service.d.ts +41 -58
- package/dist/config/seo.service.d.ts.map +1 -1
- package/dist/config/seo.service.js +468 -136
- package/dist/config/seo.service.js.map +1 -1
- package/dist/config/site.config.d.ts +10 -50
- package/dist/config/site.config.d.ts.map +1 -1
- package/dist/config/site.config.js +8 -36
- package/dist/config/site.config.js.map +1 -1
- package/dist/core/index.d.ts +1 -3
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +1 -3
- package/dist/core/index.js.map +1 -1
- package/dist/core/services/dates.service.d.ts +149 -22
- package/dist/core/services/dates.service.d.ts.map +1 -1
- package/dist/core/services/dates.service.js +201 -54
- package/dist/core/services/dates.service.js.map +1 -1
- package/dist/core/services/error.service.d.ts +32 -20
- package/dist/core/services/error.service.d.ts.map +1 -1
- package/dist/core/services/error.service.js +44 -32
- package/dist/core/services/error.service.js.map +1 -1
- package/dist/core/services/formatter.service.d.ts +245 -35
- package/dist/core/services/formatter.service.d.ts.map +1 -1
- package/dist/core/services/formatter.service.js +281 -57
- package/dist/core/services/formatter.service.js.map +1 -1
- package/dist/core/services/generator.service.d.ts +115 -26
- package/dist/core/services/generator.service.d.ts.map +1 -1
- package/dist/core/services/generator.service.js +191 -56
- package/dist/core/services/generator.service.js.map +1 -1
- package/dist/core/services/geometry.service.d.ts +369 -39
- package/dist/core/services/geometry.service.d.ts.map +1 -1
- package/dist/core/services/geometry.service.js +424 -66
- package/dist/core/services/geometry.service.js.map +1 -1
- package/dist/core/services/http.service.d.ts +244 -21
- package/dist/core/services/http.service.d.ts.map +1 -1
- package/dist/core/services/http.service.js +419 -98
- package/dist/core/services/http.service.js.map +1 -1
- package/dist/core/services/logger.service.d.ts +95 -29
- package/dist/core/services/logger.service.d.ts.map +1 -1
- package/dist/core/services/logger.service.js +167 -41
- package/dist/core/services/logger.service.js.map +1 -1
- package/dist/core/services/reactive.service.d.ts +146 -19
- package/dist/core/services/reactive.service.d.ts.map +1 -1
- package/dist/core/services/reactive.service.js +278 -137
- package/dist/core/services/reactive.service.js.map +1 -1
- package/dist/core/services/timing.service.d.ts +142 -20
- package/dist/core/services/timing.service.d.ts.map +1 -1
- package/dist/core/services/timing.service.js +324 -185
- package/dist/core/services/timing.service.js.map +1 -1
- package/dist/core/services/utils.service.d.ts +228 -43
- package/dist/core/services/utils.service.d.ts.map +1 -1
- package/dist/core/services/utils.service.js +305 -125
- package/dist/core/services/utils.service.js.map +1 -1
- package/dist/infrastructure/dom/dom.service.d.ts +307 -44
- package/dist/infrastructure/dom/dom.service.d.ts.map +1 -1
- package/dist/infrastructure/dom/dom.service.js +408 -129
- package/dist/infrastructure/dom/dom.service.js.map +1 -1
- package/dist/infrastructure/index.d.ts +0 -3
- package/dist/infrastructure/index.d.ts.map +1 -1
- package/dist/infrastructure/index.js +0 -3
- package/dist/infrastructure/index.js.map +1 -1
- package/dist/infrastructure/observer/observer.service.d.ts +124 -31
- package/dist/infrastructure/observer/observer.service.d.ts.map +1 -1
- package/dist/infrastructure/observer/observer.service.js +227 -131
- package/dist/infrastructure/observer/observer.service.js.map +1 -1
- package/dist/infrastructure/sensors/sensors.service.d.ts +154 -22
- package/dist/infrastructure/sensors/sensors.service.d.ts.map +1 -1
- package/dist/infrastructure/sensors/sensors.service.js +264 -112
- package/dist/infrastructure/sensors/sensors.service.js.map +1 -1
- package/dist/infrastructure/storage/storage.service.d.ts +123 -45
- package/dist/infrastructure/storage/storage.service.d.ts.map +1 -1
- package/dist/infrastructure/storage/storage.service.js +236 -66
- package/dist/infrastructure/storage/storage.service.js.map +1 -1
- package/dist/infrastructure/theme/theme.service.d.ts +93 -29
- package/dist/infrastructure/theme/theme.service.d.ts.map +1 -1
- package/dist/infrastructure/theme/theme.service.js +198 -96
- package/dist/infrastructure/theme/theme.service.js.map +1 -1
- package/dist/infrastructure/viewport/viewport.service.d.ts +233 -35
- package/dist/infrastructure/viewport/viewport.service.d.ts.map +1 -1
- package/dist/infrastructure/viewport/viewport.service.js +358 -139
- package/dist/infrastructure/viewport/viewport.service.js.map +1 -1
- package/dist/infrastructure/worker/worker.service.d.ts +84 -38
- package/dist/infrastructure/worker/worker.service.d.ts.map +1 -1
- package/dist/infrastructure/worker/worker.service.js +234 -133
- package/dist/infrastructure/worker/worker.service.js.map +1 -1
- package/dist/prisma/index.d.ts +2 -0
- package/dist/prisma/index.d.ts.map +1 -0
- package/dist/prisma/index.js +2 -0
- package/dist/prisma/index.js.map +1 -0
- package/dist/prisma/schema.json +11 -33
- package/dist/prisma/use-prisma.d.ts +68 -0
- package/dist/prisma/use-prisma.d.ts.map +1 -0
- package/dist/prisma/use-prisma.js +82 -0
- package/dist/prisma/use-prisma.js.map +1 -0
- package/dist/types/index.d.ts +20 -12
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +124 -125
- package/CHANGELOG.md +0 -101
|
@@ -1,174 +1,468 @@
|
|
|
1
|
+
import { siteConfig as defaultSiteConfig } from "./site.config.js";
|
|
1
2
|
/**
|
|
2
|
-
*
|
|
3
|
-
* canonical URL, and JSON-LD structured data.
|
|
4
|
-
*
|
|
5
|
-
* @param config - Site-wide configuration.
|
|
6
|
-
* @param meta - Page-specific meta.
|
|
7
|
-
* @returns HTML string of meta tags to inject in `<head>`.
|
|
3
|
+
* SEO helper: one flat object in → one flat object out (+ html/tags).
|
|
8
4
|
*
|
|
9
5
|
* @example
|
|
10
|
-
* ```
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* description: "A great post about TypeScript",
|
|
18
|
-
* url: "https://example.com/blog/my-post/",
|
|
19
|
-
* ogType: "article",
|
|
20
|
-
* publishedTime: "2024-01-15T00:00:00Z",
|
|
6
|
+
* ```ts
|
|
7
|
+
* const seo = useSeoMeta({
|
|
8
|
+
* site: "https://example.com",
|
|
9
|
+
* siteTitle: "My Site",
|
|
10
|
+
* title: "Home",
|
|
11
|
+
* description: "…",
|
|
12
|
+
* ogImage: "https://example.com/image.png",
|
|
21
13
|
* });
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
* <Fragment set:html={metaTags} />
|
|
25
|
-
* </head>
|
|
14
|
+
* // seo.title, seo.site, seo.ogImage — once each
|
|
15
|
+
* // seo.html / seo.tags — head injection
|
|
26
16
|
* ```
|
|
27
17
|
*/
|
|
18
|
+
export function useSeoMeta(opts, defaults = defaultSiteConfig) {
|
|
19
|
+
const { config, meta } = splitUseSeoMetaOptions(opts, defaults);
|
|
20
|
+
const merged = mergeSeoDefaults(meta, config);
|
|
21
|
+
const tags = flattenSeoMetaToTags(merged, config);
|
|
22
|
+
const rss = buildRssTagNode(config);
|
|
23
|
+
if (rss)
|
|
24
|
+
tags.push(rss);
|
|
25
|
+
const jsonLd = config.seo.jsonLd ? buildJsonLdFromInput(merged, config) : undefined;
|
|
26
|
+
if (jsonLd) {
|
|
27
|
+
tags.push({
|
|
28
|
+
tag: "script",
|
|
29
|
+
attrs: { type: "application/ld+json" },
|
|
30
|
+
text: safeJsonLd(jsonLd),
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
const title = String(merged.title ?? config.title);
|
|
34
|
+
const description = String(merged.description ?? config.description);
|
|
35
|
+
const url = String(merged.ogUrl ?? merged.canonical ?? merged.url ?? config.site);
|
|
36
|
+
const ogImage = resolveOgImageUrl(merged, config);
|
|
37
|
+
return {
|
|
38
|
+
...merged,
|
|
39
|
+
site: config.site,
|
|
40
|
+
siteTitle: config.title,
|
|
41
|
+
lang: config.lang,
|
|
42
|
+
rss: config.rss,
|
|
43
|
+
seo: {
|
|
44
|
+
noindex: config.seo.noindex,
|
|
45
|
+
canonical: config.seo.canonical,
|
|
46
|
+
openGraph: config.seo.openGraph,
|
|
47
|
+
jsonLd: config.seo.jsonLd,
|
|
48
|
+
},
|
|
49
|
+
nav: config.nav,
|
|
50
|
+
title,
|
|
51
|
+
description,
|
|
52
|
+
url,
|
|
53
|
+
ogUrl: merged.ogUrl ?? url,
|
|
54
|
+
ogImage: ogImage ?? (typeof merged.ogImage === "string" ? merged.ogImage : undefined),
|
|
55
|
+
canonical: merged.canonical ?? (config.seo.canonical ? url : merged.canonical),
|
|
56
|
+
html: serializeSeoTags(tags),
|
|
57
|
+
tags,
|
|
58
|
+
jsonLd,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* @deprecated Prefer {@link useSeoMeta} with a unified flat object.
|
|
63
|
+
* Legacy: `useSeoTag(siteConfig, { title, description, ... })`.
|
|
64
|
+
*/
|
|
65
|
+
export function useSeoTag(config, meta) {
|
|
66
|
+
return useSeoMeta({ ...siteConfigToOpts(config), ...legacySeoMetaToInput(meta) });
|
|
67
|
+
}
|
|
68
|
+
/** @deprecated Prefer {@link useSeoMeta}. */
|
|
69
|
+
export const useSeoTags = useSeoTag;
|
|
70
|
+
/** Maps a {@link SiteConfig} into unified {@link UseSeoMetaOptions} site fields. */
|
|
71
|
+
function siteConfigToOpts(config) {
|
|
72
|
+
return {
|
|
73
|
+
site: config.site,
|
|
74
|
+
siteTitle: config.title,
|
|
75
|
+
lang: config.lang,
|
|
76
|
+
author: config.author,
|
|
77
|
+
ogImage: config.ogImage,
|
|
78
|
+
description: config.description,
|
|
79
|
+
rss: config.rss,
|
|
80
|
+
seo: {
|
|
81
|
+
noindex: config.seo.noindex,
|
|
82
|
+
canonical: config.seo.canonical,
|
|
83
|
+
openGraph: config.seo.openGraph,
|
|
84
|
+
jsonLd: config.seo.jsonLd,
|
|
85
|
+
},
|
|
86
|
+
nav: config.nav,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
function splitUseSeoMetaOptions(opts, defaults) {
|
|
90
|
+
const { site, siteTitle, lang, rss, seo, nav, ...metaRest } = opts;
|
|
91
|
+
const brand = siteTitle ??
|
|
92
|
+
(typeof opts.ogSiteName === "string" ? opts.ogSiteName : undefined) ??
|
|
93
|
+
defaults.title;
|
|
94
|
+
const config = {
|
|
95
|
+
site: site ?? defaults.site,
|
|
96
|
+
title: brand,
|
|
97
|
+
description: (typeof metaRest.description === "string" ? metaRest.description : undefined) ??
|
|
98
|
+
defaults.description,
|
|
99
|
+
lang: lang ?? defaults.lang,
|
|
100
|
+
author: (typeof metaRest.author === "string" ? metaRest.author : undefined) ?? defaults.author,
|
|
101
|
+
ogImage: typeof metaRest.ogImage === "string" ? metaRest.ogImage : (defaults.ogImage ?? undefined),
|
|
102
|
+
twitter: defaults.twitter,
|
|
103
|
+
rss: {
|
|
104
|
+
enabled: rss?.enabled ?? defaults.rss.enabled,
|
|
105
|
+
path: rss?.path ?? defaults.rss.path,
|
|
106
|
+
title: rss?.title ?? defaults.rss.title,
|
|
107
|
+
description: rss?.description ?? defaults.rss.description,
|
|
108
|
+
limit: rss?.limit ?? defaults.rss.limit,
|
|
109
|
+
},
|
|
110
|
+
seo: {
|
|
111
|
+
noindex: seo?.noindex ?? defaults.seo.noindex,
|
|
112
|
+
canonical: seo?.canonical ?? defaults.seo.canonical,
|
|
113
|
+
openGraph: seo?.openGraph ?? defaults.seo.openGraph,
|
|
114
|
+
twitterCard: defaults.seo.twitterCard,
|
|
115
|
+
jsonLd: seo?.jsonLd ?? defaults.seo.jsonLd,
|
|
116
|
+
},
|
|
117
|
+
nav: nav ?? defaults.nav,
|
|
118
|
+
};
|
|
119
|
+
return { config, meta: { ...metaRest } };
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Applies tags from a {@link SeoTagResult} into `document.head` (Vanilla / SPA).
|
|
123
|
+
* No-op when `document` is unavailable (SSR).
|
|
124
|
+
*/
|
|
125
|
+
export function useApplySeoTag(seo, head = typeof document !== "undefined" ? document.head : null) {
|
|
126
|
+
if (!head || typeof document === "undefined")
|
|
127
|
+
return;
|
|
128
|
+
for (const el of head.querySelectorAll("[data-katanakit-seo]")) {
|
|
129
|
+
el.remove();
|
|
130
|
+
}
|
|
131
|
+
for (const node of seo.tags) {
|
|
132
|
+
const el = document.createElement(node.tag);
|
|
133
|
+
el.setAttribute("data-katanakit-seo", "");
|
|
134
|
+
if (node.attrs) {
|
|
135
|
+
for (const [key, value] of Object.entries(node.attrs)) {
|
|
136
|
+
el.setAttribute(key, value);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
if (node.text !== undefined) {
|
|
140
|
+
el.textContent = node.text;
|
|
141
|
+
}
|
|
142
|
+
head.appendChild(el);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
/** HTML string of meta tags (no RSS). Prefer {@link useSeoMeta}. */
|
|
28
146
|
export function useGenerateMetaTags(config, meta) {
|
|
147
|
+
const { tags } = useSeoMeta({
|
|
148
|
+
...siteConfigToOpts({ ...config, rss: { ...config.rss, enabled: false } }),
|
|
149
|
+
...legacySeoMetaToInput(meta),
|
|
150
|
+
});
|
|
151
|
+
return serializeSeoTags(tags.filter((t) => !(t.tag === "link" && t.attrs?.rel === "alternate")));
|
|
152
|
+
}
|
|
153
|
+
export function useTitle(config, pageTitle) {
|
|
154
|
+
if (!pageTitle || pageTitle === config.title) {
|
|
155
|
+
return `<title>${escapeHtml(config.title)}</title>`;
|
|
156
|
+
}
|
|
157
|
+
return `<title>${escapeHtml(`${pageTitle} | ${config.title}`)}</title>`;
|
|
158
|
+
}
|
|
159
|
+
export function useRssHeadLink(config) {
|
|
160
|
+
const node = buildRssTagNode(config);
|
|
161
|
+
return node ? serializeSeoTag(node) : "";
|
|
162
|
+
}
|
|
163
|
+
/** Full `<head>` HTML (meta + RSS). Accepts legacy {@link SeoMeta} or flat {@link SeoMetaInput}. */
|
|
164
|
+
export function useHeadTags(config, meta) {
|
|
165
|
+
if (isLegacySeoMeta(meta)) {
|
|
166
|
+
return useSeoTag(config, meta).html;
|
|
167
|
+
}
|
|
168
|
+
return useSeoMeta({ ...siteConfigToOpts(config), ...meta }).html;
|
|
169
|
+
}
|
|
170
|
+
function isLegacySeoMeta(meta) {
|
|
171
|
+
return ("publishedTime" in meta ||
|
|
172
|
+
"modifiedTime" in meta ||
|
|
173
|
+
"noindex" in meta ||
|
|
174
|
+
("tags" in meta && !("articleTag" in meta)));
|
|
175
|
+
}
|
|
176
|
+
// --- Merge & legacy ---
|
|
177
|
+
function legacySeoMetaToInput(meta) {
|
|
178
|
+
const input = {
|
|
179
|
+
title: meta.title,
|
|
180
|
+
};
|
|
181
|
+
if (meta.description != null)
|
|
182
|
+
input.description = meta.description;
|
|
183
|
+
if (meta.canonical != null)
|
|
184
|
+
input.canonical = meta.canonical;
|
|
185
|
+
if (meta.url != null) {
|
|
186
|
+
input.url = meta.url;
|
|
187
|
+
input.ogUrl = meta.url;
|
|
188
|
+
}
|
|
189
|
+
if (meta.ogImage != null)
|
|
190
|
+
input.ogImage = meta.ogImage;
|
|
191
|
+
if (meta.ogType != null)
|
|
192
|
+
input.ogType = meta.ogType;
|
|
193
|
+
if (meta.author != null)
|
|
194
|
+
input.author = meta.author;
|
|
195
|
+
if (meta.ogType === "article") {
|
|
196
|
+
if (meta.publishedTime != null)
|
|
197
|
+
input.articlePublishedTime = meta.publishedTime;
|
|
198
|
+
if (meta.modifiedTime != null)
|
|
199
|
+
input.articleModifiedTime = meta.modifiedTime;
|
|
200
|
+
if (meta.tags != null)
|
|
201
|
+
input.articleTag = meta.tags;
|
|
202
|
+
if (meta.author)
|
|
203
|
+
input.articleAuthor = [meta.author];
|
|
204
|
+
}
|
|
205
|
+
if (meta.noindex) {
|
|
206
|
+
input.robots = "noindex, nofollow";
|
|
207
|
+
}
|
|
208
|
+
return input;
|
|
209
|
+
}
|
|
210
|
+
function mergeSeoDefaults(input, config) {
|
|
211
|
+
if (!config)
|
|
212
|
+
return { ...input };
|
|
213
|
+
const pageTitle = input.title ?? config.title;
|
|
214
|
+
const siteSuffix = ` | ${config.title}`;
|
|
215
|
+
const alreadySuffixed = typeof pageTitle === "string" && pageTitle !== config.title && pageTitle.endsWith(siteSuffix);
|
|
216
|
+
const title = pageTitle === config.title || !pageTitle || alreadySuffixed
|
|
217
|
+
? (pageTitle ?? config.title)
|
|
218
|
+
: `${pageTitle} | ${config.title}`;
|
|
219
|
+
const description = input.description ?? config.description;
|
|
220
|
+
const url = input.ogUrl ?? input.canonical ?? input.url ?? config.site;
|
|
221
|
+
const ogImage = input.ogImage ?? config.ogImage;
|
|
222
|
+
const merged = {
|
|
223
|
+
...input,
|
|
224
|
+
title,
|
|
225
|
+
description,
|
|
226
|
+
ogTitle: input.ogTitle ?? title,
|
|
227
|
+
ogDescription: input.ogDescription ?? description,
|
|
228
|
+
ogUrl: input.ogUrl ?? url,
|
|
229
|
+
ogSiteName: input.ogSiteName ?? config.title,
|
|
230
|
+
ogLocale: input.ogLocale ?? config.lang.replace("-", "_"),
|
|
231
|
+
ogType: input.ogType ?? "website",
|
|
232
|
+
author: input.author ?? config.author,
|
|
233
|
+
canonical: input.canonical ?? (config.seo.canonical ? url : input.canonical),
|
|
234
|
+
url,
|
|
235
|
+
};
|
|
236
|
+
if (ogImage !== undefined && ogImage !== null) {
|
|
237
|
+
merged.ogImage = ogImage;
|
|
238
|
+
}
|
|
239
|
+
if (config.seo.noindex && merged.robots == null) {
|
|
240
|
+
merged.robots = "noindex, nofollow";
|
|
241
|
+
}
|
|
242
|
+
if (!config.seo.openGraph) {
|
|
243
|
+
for (const key of Object.keys(merged)) {
|
|
244
|
+
if (String(key).startsWith("og") || String(key).startsWith("article")) {
|
|
245
|
+
delete merged[key];
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
merged.title = title;
|
|
249
|
+
merged.description = description;
|
|
250
|
+
merged.author = input.author ?? config.author;
|
|
251
|
+
merged.canonical = input.canonical ?? (config.seo.canonical ? url : undefined);
|
|
252
|
+
}
|
|
253
|
+
return merged;
|
|
254
|
+
}
|
|
255
|
+
function resolveOgImageUrl(input, config) {
|
|
256
|
+
const raw = input.ogImage ?? input.ogImageUrl ?? config?.ogImage;
|
|
257
|
+
if (raw == null)
|
|
258
|
+
return undefined;
|
|
259
|
+
if (typeof raw === "string") {
|
|
260
|
+
return absoluteUrl(raw, config?.site);
|
|
261
|
+
}
|
|
262
|
+
const first = Array.isArray(raw) ? raw[0] : raw;
|
|
263
|
+
if (first && typeof first === "object" && "url" in first && first.url) {
|
|
264
|
+
return absoluteUrl(String(first.url), config?.site);
|
|
265
|
+
}
|
|
266
|
+
return undefined;
|
|
267
|
+
}
|
|
268
|
+
function absoluteUrl(value, site) {
|
|
269
|
+
if (value.startsWith("http://") || value.startsWith("https://"))
|
|
270
|
+
return value;
|
|
271
|
+
if (!site)
|
|
272
|
+
return value;
|
|
273
|
+
return `${site.replace(/\/$/, "")}${value.startsWith("/") ? value : `/${value}`}`;
|
|
274
|
+
}
|
|
275
|
+
// --- Flatten input → nodes ---
|
|
276
|
+
const NAME_KEYS = new Set(["description", "keywords", "author", "viewport", "robots"]);
|
|
277
|
+
const SKIP_KEYS = new Set([
|
|
278
|
+
"url",
|
|
279
|
+
"title",
|
|
280
|
+
"canonical",
|
|
281
|
+
"charset",
|
|
282
|
+
"site",
|
|
283
|
+
"siteTitle",
|
|
284
|
+
"lang",
|
|
285
|
+
"rss",
|
|
286
|
+
"seo",
|
|
287
|
+
"nav",
|
|
288
|
+
]);
|
|
289
|
+
function flattenSeoMetaToTags(input, config) {
|
|
29
290
|
const tags = [];
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
const canonical = meta.canonical ?? url;
|
|
48
|
-
tags.push(`<link rel="canonical" href="${escapeAttr(canonical)}" />`);
|
|
49
|
-
}
|
|
50
|
-
// Noindex.
|
|
51
|
-
if (meta.noindex || config.seo.noindex) {
|
|
52
|
-
tags.push('<meta name="robots" content="noindex, nofollow" />');
|
|
53
|
-
}
|
|
54
|
-
// Open Graph.
|
|
55
|
-
if (config.seo.openGraph) {
|
|
56
|
-
tags.push(`<meta property="og:type" content="${meta.ogType ?? "website"}" />`);
|
|
57
|
-
tags.push(`<meta property="og:title" content="${escapeAttr(fullTitle)}" />`);
|
|
58
|
-
tags.push(`<meta property="og:description" content="${escapeAttr(description)}" />`);
|
|
59
|
-
tags.push(`<meta property="og:site_name" content="${escapeAttr(config.title)}" />`);
|
|
60
|
-
tags.push(`<meta property="og:locale" content="${escapeAttr(config.lang.replace("-", "_"))}" />`);
|
|
61
|
-
if (url) {
|
|
62
|
-
tags.push(`<meta property="og:url" content="${escapeAttr(url)}" />`);
|
|
291
|
+
if (input.charset) {
|
|
292
|
+
tags.push({ tag: "meta", attrs: { charset: String(input.charset) } });
|
|
293
|
+
}
|
|
294
|
+
if (input.title != null && input.title !== "") {
|
|
295
|
+
tags.push({ tag: "title", text: String(input.title) });
|
|
296
|
+
}
|
|
297
|
+
if (input.canonical) {
|
|
298
|
+
tags.push({ tag: "link", attrs: { rel: "canonical", href: String(input.canonical) } });
|
|
299
|
+
}
|
|
300
|
+
for (const [key, raw] of Object.entries(input)) {
|
|
301
|
+
if (raw === undefined || raw === null)
|
|
302
|
+
continue;
|
|
303
|
+
if (SKIP_KEYS.has(key))
|
|
304
|
+
continue;
|
|
305
|
+
if (key === "ogImage") {
|
|
306
|
+
tags.push(...flattenOgImage(raw, config));
|
|
307
|
+
continue;
|
|
63
308
|
}
|
|
64
|
-
if (
|
|
65
|
-
|
|
66
|
-
|
|
309
|
+
if (key === "robots") {
|
|
310
|
+
const content = serializeRobots(raw);
|
|
311
|
+
if (content)
|
|
312
|
+
tags.push({ tag: "meta", attrs: { name: "robots", content } });
|
|
313
|
+
continue;
|
|
67
314
|
}
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
if (
|
|
74
|
-
tags.push(
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
for (const tag of meta.tags) {
|
|
81
|
-
tags.push(`<meta property="article:tag" content="${escapeAttr(tag)}" />`);
|
|
82
|
-
}
|
|
315
|
+
if (key === "viewport" && typeof raw === "object") {
|
|
316
|
+
const content = Object.entries(raw)
|
|
317
|
+
.filter(([, v]) => v != null)
|
|
318
|
+
.map(([k, v]) => `${camelToKebab(k)}=${v}`)
|
|
319
|
+
.join(", ");
|
|
320
|
+
if (content)
|
|
321
|
+
tags.push({ tag: "meta", attrs: { name: "viewport", content } });
|
|
322
|
+
continue;
|
|
323
|
+
}
|
|
324
|
+
if (Array.isArray(raw)) {
|
|
325
|
+
for (const item of raw) {
|
|
326
|
+
pushScalarMeta(tags, key, item);
|
|
83
327
|
}
|
|
328
|
+
continue;
|
|
84
329
|
}
|
|
330
|
+
pushScalarMeta(tags, key, raw);
|
|
331
|
+
}
|
|
332
|
+
return tags;
|
|
333
|
+
}
|
|
334
|
+
function pushScalarMeta(tags, key, value) {
|
|
335
|
+
const content = String(value);
|
|
336
|
+
if (NAME_KEYS.has(key)) {
|
|
337
|
+
tags.push({ tag: "meta", attrs: { name: camelToMetaName(key), content } });
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
if (key.startsWith("og") || key.startsWith("article")) {
|
|
341
|
+
tags.push({ tag: "meta", attrs: { property: camelToMetaProperty(key), content } });
|
|
342
|
+
return;
|
|
85
343
|
}
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
344
|
+
tags.push({ tag: "meta", attrs: { name: camelToMetaName(key), content } });
|
|
345
|
+
}
|
|
346
|
+
function flattenOgImage(raw, config) {
|
|
347
|
+
const tags = [];
|
|
348
|
+
const prop = "og:image";
|
|
349
|
+
if (typeof raw === "string") {
|
|
350
|
+
tags.push({
|
|
351
|
+
tag: "meta",
|
|
352
|
+
attrs: { property: prop, content: absoluteUrl(raw, config?.site) },
|
|
353
|
+
});
|
|
354
|
+
return tags;
|
|
355
|
+
}
|
|
356
|
+
const list = Array.isArray(raw) ? raw : [raw];
|
|
357
|
+
for (const item of list) {
|
|
358
|
+
if (!item || typeof item !== "object")
|
|
359
|
+
continue;
|
|
360
|
+
const obj = item;
|
|
361
|
+
if (obj.url) {
|
|
362
|
+
tags.push({
|
|
363
|
+
tag: "meta",
|
|
364
|
+
attrs: { property: prop, content: absoluteUrl(String(obj.url), config?.site) },
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
if (obj.secureUrl) {
|
|
368
|
+
tags.push({
|
|
369
|
+
tag: "meta",
|
|
370
|
+
attrs: { property: `${prop}:secure_url`, content: String(obj.secureUrl) },
|
|
371
|
+
});
|
|
94
372
|
}
|
|
95
|
-
if (
|
|
96
|
-
tags.push(
|
|
373
|
+
if (obj.type)
|
|
374
|
+
tags.push({ tag: "meta", attrs: { property: `${prop}:type`, content: String(obj.type) } });
|
|
375
|
+
if (obj.width != null) {
|
|
376
|
+
tags.push({ tag: "meta", attrs: { property: `${prop}:width`, content: String(obj.width) } });
|
|
97
377
|
}
|
|
378
|
+
if (obj.height != null) {
|
|
379
|
+
tags.push({ tag: "meta", attrs: { property: `${prop}:height`, content: String(obj.height) } });
|
|
380
|
+
}
|
|
381
|
+
if (obj.alt)
|
|
382
|
+
tags.push({ tag: "meta", attrs: { property: `${prop}:alt`, content: String(obj.alt) } });
|
|
98
383
|
}
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
384
|
+
return tags;
|
|
385
|
+
}
|
|
386
|
+
function serializeRobots(value) {
|
|
387
|
+
if (typeof value === "string")
|
|
388
|
+
return value;
|
|
389
|
+
const parts = [];
|
|
390
|
+
const map = {
|
|
391
|
+
index: "index",
|
|
392
|
+
follow: "follow",
|
|
393
|
+
noindex: "noindex",
|
|
394
|
+
nofollow: "nofollow",
|
|
395
|
+
none: "none",
|
|
396
|
+
noarchive: "noarchive",
|
|
397
|
+
nosnippet: "nosnippet",
|
|
398
|
+
};
|
|
399
|
+
for (const [k, directive] of Object.entries(map)) {
|
|
400
|
+
if (truthy(value[k]))
|
|
401
|
+
parts.push(directive);
|
|
108
402
|
}
|
|
109
|
-
return
|
|
403
|
+
return parts.join(", ");
|
|
110
404
|
}
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
405
|
+
function truthy(v) {
|
|
406
|
+
return v === true || v === "true" || v === 1 || v === "1";
|
|
407
|
+
}
|
|
408
|
+
function camelToKebab(key) {
|
|
409
|
+
return key.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase();
|
|
410
|
+
}
|
|
411
|
+
function camelToMetaName(key) {
|
|
412
|
+
return camelToKebab(key);
|
|
413
|
+
}
|
|
414
|
+
function camelToMetaProperty(key) {
|
|
415
|
+
const overrides = {
|
|
416
|
+
ogSiteName: "og:site_name",
|
|
417
|
+
ogImageSecureUrl: "og:image:secure_url",
|
|
418
|
+
};
|
|
419
|
+
if (overrides[key])
|
|
420
|
+
return overrides[key];
|
|
421
|
+
if (key.startsWith("og")) {
|
|
422
|
+
const rest = key.slice(2);
|
|
423
|
+
return `og:${rest.replace(/([a-z0-9])([A-Z])/g, "$1:$2").toLowerCase()}`;
|
|
117
424
|
}
|
|
118
|
-
|
|
425
|
+
if (key.startsWith("article")) {
|
|
426
|
+
const rest = key.slice("article".length);
|
|
427
|
+
return `article:${rest.replace(/([a-z0-9])([A-Z])/g, "$1_$2").toLowerCase()}`;
|
|
428
|
+
}
|
|
429
|
+
return camelToKebab(key);
|
|
119
430
|
}
|
|
120
|
-
|
|
121
|
-
* Generates the RSS `<link>` tag for the `<head>`.
|
|
122
|
-
*/
|
|
123
|
-
export function useRssHeadLink(config) {
|
|
431
|
+
function buildRssTagNode(config) {
|
|
124
432
|
if (!config.rss.enabled)
|
|
125
|
-
return
|
|
433
|
+
return null;
|
|
126
434
|
const path = config.rss.path ?? "/rss.xml";
|
|
127
435
|
const title = config.rss.title ?? config.title;
|
|
128
|
-
return
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
const metaTags = useGenerateMetaTags(config, meta);
|
|
138
|
-
const rssLink = useRssHeadLink(config);
|
|
139
|
-
return rssLink ? `${metaTags}\n${rssLink}` : metaTags;
|
|
140
|
-
}
|
|
141
|
-
// --- Internal helpers ---
|
|
142
|
-
function escapeHtml(text) {
|
|
143
|
-
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
144
|
-
}
|
|
145
|
-
function escapeAttr(text) {
|
|
146
|
-
return text
|
|
147
|
-
.replace(/&/g, "&")
|
|
148
|
-
.replace(/"/g, """)
|
|
149
|
-
.replace(/</g, "<")
|
|
150
|
-
.replace(/>/g, ">");
|
|
436
|
+
return {
|
|
437
|
+
tag: "link",
|
|
438
|
+
attrs: {
|
|
439
|
+
rel: "alternate",
|
|
440
|
+
type: "application/rss+xml",
|
|
441
|
+
title,
|
|
442
|
+
href: path,
|
|
443
|
+
},
|
|
444
|
+
};
|
|
151
445
|
}
|
|
152
|
-
function
|
|
153
|
-
const url =
|
|
154
|
-
if (
|
|
446
|
+
function buildJsonLdFromInput(input, config) {
|
|
447
|
+
const url = String(input.ogUrl ?? input.canonical ?? input.url ?? config.site);
|
|
448
|
+
if (input.ogType === "article") {
|
|
155
449
|
return {
|
|
156
450
|
"@context": "https://schema.org",
|
|
157
451
|
"@type": "Article",
|
|
158
|
-
headline:
|
|
159
|
-
description:
|
|
452
|
+
headline: input.ogTitle ?? input.title,
|
|
453
|
+
description: input.ogDescription ?? input.description ?? config.description,
|
|
160
454
|
url,
|
|
161
455
|
author: {
|
|
162
456
|
"@type": "Person",
|
|
163
|
-
name:
|
|
457
|
+
name: input.articleAuthor?.[0] ?? input.author ?? config.author,
|
|
164
458
|
},
|
|
165
459
|
publisher: {
|
|
166
460
|
"@type": "Organization",
|
|
167
461
|
name: config.title,
|
|
168
462
|
},
|
|
169
|
-
datePublished:
|
|
170
|
-
dateModified:
|
|
171
|
-
image:
|
|
463
|
+
datePublished: input.articlePublishedTime,
|
|
464
|
+
dateModified: input.articleModifiedTime ?? input.articlePublishedTime,
|
|
465
|
+
image: resolveOgImageUrl(input, config),
|
|
172
466
|
mainEntityOfPage: {
|
|
173
467
|
"@type": "WebPage",
|
|
174
468
|
"@id": url,
|
|
@@ -187,4 +481,42 @@ function buildJsonLd(config, meta) {
|
|
|
187
481
|
},
|
|
188
482
|
};
|
|
189
483
|
}
|
|
484
|
+
function safeJsonLd(data) {
|
|
485
|
+
return JSON.stringify(data)
|
|
486
|
+
.replace(/</g, "\\u003c")
|
|
487
|
+
.replace(/>/g, "\\u003e")
|
|
488
|
+
.replace(/&/g, "\\u0026");
|
|
489
|
+
}
|
|
490
|
+
function serializeSeoTags(tags) {
|
|
491
|
+
return tags.map(serializeSeoTag).join("\n");
|
|
492
|
+
}
|
|
493
|
+
function serializeSeoTag(node) {
|
|
494
|
+
if (node.tag === "title") {
|
|
495
|
+
return `<title>${escapeHtml(node.text ?? "")}</title>`;
|
|
496
|
+
}
|
|
497
|
+
if (node.tag === "script") {
|
|
498
|
+
return `<script${formatAttrs(node.attrs)}>${node.text ?? ""}</script>`;
|
|
499
|
+
}
|
|
500
|
+
if (node.tag === "meta" && node.attrs?.charset) {
|
|
501
|
+
return `<meta charset="${escapeAttr(node.attrs.charset)}" />`;
|
|
502
|
+
}
|
|
503
|
+
return `<${node.tag}${formatAttrs(node.attrs)} />`;
|
|
504
|
+
}
|
|
505
|
+
function formatAttrs(attrs) {
|
|
506
|
+
if (!attrs)
|
|
507
|
+
return "";
|
|
508
|
+
return Object.entries(attrs)
|
|
509
|
+
.map(([key, value]) => ` ${key}="${escapeAttr(value)}"`)
|
|
510
|
+
.join("");
|
|
511
|
+
}
|
|
512
|
+
function escapeHtml(text) {
|
|
513
|
+
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
514
|
+
}
|
|
515
|
+
function escapeAttr(text) {
|
|
516
|
+
return text
|
|
517
|
+
.replace(/&/g, "&")
|
|
518
|
+
.replace(/"/g, """)
|
|
519
|
+
.replace(/</g, "<")
|
|
520
|
+
.replace(/>/g, ">");
|
|
521
|
+
}
|
|
190
522
|
//# sourceMappingURL=seo.service.js.map
|