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.
Files changed (148) hide show
  1. package/README.md +72 -437
  2. package/dist/adapters/astro/astro.service.d.ts +103 -15
  3. package/dist/adapters/astro/astro.service.d.ts.map +1 -1
  4. package/dist/adapters/astro/astro.service.js +145 -75
  5. package/dist/adapters/astro/astro.service.js.map +1 -1
  6. package/dist/adapters/astro/index.d.ts +1 -1
  7. package/dist/adapters/astro/index.d.ts.map +1 -1
  8. package/dist/adapters/astro/index.js +1 -1
  9. package/dist/adapters/astro/index.js.map +1 -1
  10. package/dist/adapters/astro/rss.service.d.ts +85 -105
  11. package/dist/adapters/astro/rss.service.d.ts.map +1 -1
  12. package/dist/adapters/astro/rss.service.js +172 -168
  13. package/dist/adapters/astro/rss.service.js.map +1 -1
  14. package/dist/adapters/astro/seo.service.d.ts +10 -0
  15. package/dist/adapters/astro/seo.service.d.ts.map +1 -0
  16. package/dist/adapters/astro/seo.service.js +3 -0
  17. package/dist/adapters/astro/seo.service.js.map +1 -0
  18. package/dist/adapters/express/app.d.ts +30 -1
  19. package/dist/adapters/express/app.d.ts.map +1 -1
  20. package/dist/adapters/express/app.js +31 -3
  21. package/dist/adapters/express/app.js.map +1 -1
  22. package/dist/adapters/express/index.d.ts +2 -2
  23. package/dist/adapters/express/index.d.ts.map +1 -1
  24. package/dist/adapters/express/index.js +2 -2
  25. package/dist/adapters/express/index.js.map +1 -1
  26. package/dist/adapters/express/main.js +2 -2
  27. package/dist/adapters/express/main.js.map +1 -1
  28. package/dist/adapters/express/products.controller.d.ts +67 -8
  29. package/dist/adapters/express/products.controller.d.ts.map +1 -1
  30. package/dist/adapters/express/products.controller.js +105 -49
  31. package/dist/adapters/express/products.controller.js.map +1 -1
  32. package/dist/adapters/express/server.d.ts +37 -13
  33. package/dist/adapters/express/server.d.ts.map +1 -1
  34. package/dist/adapters/express/server.js +118 -53
  35. package/dist/adapters/express/server.js.map +1 -1
  36. package/dist/adapters/nuxt/nuxt.service.d.ts +8 -6
  37. package/dist/adapters/nuxt/nuxt.service.d.ts.map +1 -1
  38. package/dist/adapters/nuxt/nuxt.service.js +34 -10
  39. package/dist/adapters/nuxt/nuxt.service.js.map +1 -1
  40. package/dist/adapters/vue/vue.service.d.ts +5 -2
  41. package/dist/adapters/vue/vue.service.d.ts.map +1 -1
  42. package/dist/adapters/vue/vue.service.js +41 -7
  43. package/dist/adapters/vue/vue.service.js.map +1 -1
  44. package/dist/config/index.d.ts +2 -1
  45. package/dist/config/index.d.ts.map +1 -1
  46. package/dist/config/index.js +1 -1
  47. package/dist/config/index.js.map +1 -1
  48. package/dist/config/seo-meta.types.d.ts +177 -0
  49. package/dist/config/seo-meta.types.d.ts.map +1 -0
  50. package/dist/config/seo-meta.types.js +11 -0
  51. package/dist/config/seo-meta.types.js.map +1 -0
  52. package/dist/config/seo.service.d.ts +41 -58
  53. package/dist/config/seo.service.d.ts.map +1 -1
  54. package/dist/config/seo.service.js +468 -136
  55. package/dist/config/seo.service.js.map +1 -1
  56. package/dist/config/site.config.d.ts +10 -50
  57. package/dist/config/site.config.d.ts.map +1 -1
  58. package/dist/config/site.config.js +8 -36
  59. package/dist/config/site.config.js.map +1 -1
  60. package/dist/core/index.d.ts +1 -3
  61. package/dist/core/index.d.ts.map +1 -1
  62. package/dist/core/index.js +1 -3
  63. package/dist/core/index.js.map +1 -1
  64. package/dist/core/services/dates.service.d.ts +149 -22
  65. package/dist/core/services/dates.service.d.ts.map +1 -1
  66. package/dist/core/services/dates.service.js +201 -54
  67. package/dist/core/services/dates.service.js.map +1 -1
  68. package/dist/core/services/error.service.d.ts +32 -20
  69. package/dist/core/services/error.service.d.ts.map +1 -1
  70. package/dist/core/services/error.service.js +44 -32
  71. package/dist/core/services/error.service.js.map +1 -1
  72. package/dist/core/services/formatter.service.d.ts +245 -35
  73. package/dist/core/services/formatter.service.d.ts.map +1 -1
  74. package/dist/core/services/formatter.service.js +281 -57
  75. package/dist/core/services/formatter.service.js.map +1 -1
  76. package/dist/core/services/generator.service.d.ts +115 -26
  77. package/dist/core/services/generator.service.d.ts.map +1 -1
  78. package/dist/core/services/generator.service.js +191 -56
  79. package/dist/core/services/generator.service.js.map +1 -1
  80. package/dist/core/services/geometry.service.d.ts +369 -39
  81. package/dist/core/services/geometry.service.d.ts.map +1 -1
  82. package/dist/core/services/geometry.service.js +424 -66
  83. package/dist/core/services/geometry.service.js.map +1 -1
  84. package/dist/core/services/http.service.d.ts +244 -21
  85. package/dist/core/services/http.service.d.ts.map +1 -1
  86. package/dist/core/services/http.service.js +419 -98
  87. package/dist/core/services/http.service.js.map +1 -1
  88. package/dist/core/services/logger.service.d.ts +95 -29
  89. package/dist/core/services/logger.service.d.ts.map +1 -1
  90. package/dist/core/services/logger.service.js +167 -41
  91. package/dist/core/services/logger.service.js.map +1 -1
  92. package/dist/core/services/reactive.service.d.ts +146 -19
  93. package/dist/core/services/reactive.service.d.ts.map +1 -1
  94. package/dist/core/services/reactive.service.js +278 -137
  95. package/dist/core/services/reactive.service.js.map +1 -1
  96. package/dist/core/services/timing.service.d.ts +142 -20
  97. package/dist/core/services/timing.service.d.ts.map +1 -1
  98. package/dist/core/services/timing.service.js +324 -185
  99. package/dist/core/services/timing.service.js.map +1 -1
  100. package/dist/core/services/utils.service.d.ts +228 -43
  101. package/dist/core/services/utils.service.d.ts.map +1 -1
  102. package/dist/core/services/utils.service.js +305 -125
  103. package/dist/core/services/utils.service.js.map +1 -1
  104. package/dist/infrastructure/dom/dom.service.d.ts +307 -44
  105. package/dist/infrastructure/dom/dom.service.d.ts.map +1 -1
  106. package/dist/infrastructure/dom/dom.service.js +408 -129
  107. package/dist/infrastructure/dom/dom.service.js.map +1 -1
  108. package/dist/infrastructure/index.d.ts +0 -3
  109. package/dist/infrastructure/index.d.ts.map +1 -1
  110. package/dist/infrastructure/index.js +0 -3
  111. package/dist/infrastructure/index.js.map +1 -1
  112. package/dist/infrastructure/observer/observer.service.d.ts +124 -31
  113. package/dist/infrastructure/observer/observer.service.d.ts.map +1 -1
  114. package/dist/infrastructure/observer/observer.service.js +227 -131
  115. package/dist/infrastructure/observer/observer.service.js.map +1 -1
  116. package/dist/infrastructure/sensors/sensors.service.d.ts +154 -22
  117. package/dist/infrastructure/sensors/sensors.service.d.ts.map +1 -1
  118. package/dist/infrastructure/sensors/sensors.service.js +264 -112
  119. package/dist/infrastructure/sensors/sensors.service.js.map +1 -1
  120. package/dist/infrastructure/storage/storage.service.d.ts +123 -45
  121. package/dist/infrastructure/storage/storage.service.d.ts.map +1 -1
  122. package/dist/infrastructure/storage/storage.service.js +236 -66
  123. package/dist/infrastructure/storage/storage.service.js.map +1 -1
  124. package/dist/infrastructure/theme/theme.service.d.ts +93 -29
  125. package/dist/infrastructure/theme/theme.service.d.ts.map +1 -1
  126. package/dist/infrastructure/theme/theme.service.js +198 -96
  127. package/dist/infrastructure/theme/theme.service.js.map +1 -1
  128. package/dist/infrastructure/viewport/viewport.service.d.ts +233 -35
  129. package/dist/infrastructure/viewport/viewport.service.d.ts.map +1 -1
  130. package/dist/infrastructure/viewport/viewport.service.js +358 -139
  131. package/dist/infrastructure/viewport/viewport.service.js.map +1 -1
  132. package/dist/infrastructure/worker/worker.service.d.ts +84 -38
  133. package/dist/infrastructure/worker/worker.service.d.ts.map +1 -1
  134. package/dist/infrastructure/worker/worker.service.js +234 -133
  135. package/dist/infrastructure/worker/worker.service.js.map +1 -1
  136. package/dist/prisma/index.d.ts +2 -0
  137. package/dist/prisma/index.d.ts.map +1 -0
  138. package/dist/prisma/index.js +2 -0
  139. package/dist/prisma/index.js.map +1 -0
  140. package/dist/prisma/schema.json +11 -33
  141. package/dist/prisma/use-prisma.d.ts +68 -0
  142. package/dist/prisma/use-prisma.d.ts.map +1 -0
  143. package/dist/prisma/use-prisma.js +82 -0
  144. package/dist/prisma/use-prisma.js.map +1 -0
  145. package/dist/types/index.d.ts +20 -12
  146. package/dist/types/index.d.ts.map +1 -1
  147. package/package.json +124 -125
  148. package/CHANGELOG.md +0 -101
@@ -1,174 +1,468 @@
1
+ import { siteConfig as defaultSiteConfig } from "./site.config.js";
1
2
  /**
2
- * Generates all `<head>` meta tags for SEO, Open Graph, Twitter Card,
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
- * ```astro
11
- * ---
12
- * import { siteConfig } from "@/config/site.config";
13
- * import { useGenerateMetaTags } from "@/config/seo.service";
14
- *
15
- * const metaTags = useGenerateMetaTags(siteConfig, {
16
- * title: "My Blog Post",
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
- * <head>
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
- const fullTitle = meta.title === config.title ? config.title : `${meta.title} | ${config.title}`;
31
- const description = meta.description ?? config.description;
32
- const url = meta.url ?? config.site;
33
- const ogImage = meta.ogImage ?? config.ogImage;
34
- const ogImageUrl = ogImage
35
- ? ogImage.startsWith("http")
36
- ? ogImage
37
- : `${config.site}${ogImage}`
38
- : undefined;
39
- // Basic meta tags.
40
- tags.push(`<title>${escapeHtml(fullTitle)}</title>`);
41
- tags.push(`<meta name="description" content="${escapeAttr(description)}" />`);
42
- if (config.author) {
43
- tags.push(`<meta name="author" content="${escapeAttr(config.author)}" />`);
44
- }
45
- // Canonical URL.
46
- if (config.seo.canonical && url) {
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 (ogImageUrl) {
65
- tags.push(`<meta property="og:image" content="${escapeAttr(ogImageUrl)}" />`);
66
- tags.push(`<meta property="og:image:alt" content="${escapeAttr(fullTitle)}" />`);
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
- // Article-specific OG tags.
69
- if (meta.ogType === "article") {
70
- if (meta.publishedTime) {
71
- tags.push(`<meta property="article:published_time" content="${escapeAttr(meta.publishedTime)}" />`);
72
- }
73
- if (meta.modifiedTime) {
74
- tags.push(`<meta property="article:modified_time" content="${escapeAttr(meta.modifiedTime)}" />`);
75
- }
76
- if (meta.author) {
77
- tags.push(`<meta property="article:author" content="${escapeAttr(meta.author)}" />`);
78
- }
79
- if (meta.tags) {
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
- // Twitter Card.
87
- if (config.seo.twitterCard) {
88
- tags.push('<meta name="twitter:card" content="summary_large_image" />');
89
- tags.push(`<meta name="twitter:title" content="${escapeAttr(fullTitle)}" />`);
90
- tags.push(`<meta name="twitter:description" content="${escapeAttr(description)}" />`);
91
- if (config.twitter) {
92
- tags.push(`<meta name="twitter:site" content="@${escapeAttr(config.twitter)}" />`);
93
- tags.push(`<meta name="twitter:creator" content="@${escapeAttr(config.twitter)}" />`);
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 (ogImageUrl) {
96
- tags.push(`<meta name="twitter:image" content="${escapeAttr(ogImageUrl)}" />`);
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
- // JSON-LD structured data.
100
- if (config.seo.jsonLd) {
101
- const jsonLd = buildJsonLd(config, meta);
102
- // Escape <, >, & to prevent XSS via </script> breakout.
103
- const safeJson = JSON.stringify(jsonLd)
104
- .replace(/</g, "\\u003c")
105
- .replace(/>/g, "\\u003e")
106
- .replace(/&/g, "\\u0026");
107
- tags.push(`<script type="application/ld+json">${safeJson}</script>`);
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 tags.join("\n");
403
+ return parts.join(", ");
110
404
  }
111
- /**
112
- * Generates a `<title>` tag string.
113
- */
114
- export function useTitle(config, pageTitle) {
115
- if (!pageTitle || pageTitle === config.title) {
116
- return `<title>${escapeHtml(config.title)}</title>`;
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
- return `<title>${escapeHtml(`${pageTitle} | ${config.title}`)}</title>`;
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 `<link rel="alternate" type="application/rss+xml" title="${escapeAttr(title)}" href="${escapeAttr(path)}" />`;
129
- }
130
- /**
131
- * Generates all default `<head>` meta tags for a page (title, description,
132
- * canonical, OG, Twitter, JSON-LD, RSS link).
133
- *
134
- * Convenience wrapper around `useGenerateMetaTags` + `useRssHeadLink`.
135
- */
136
- export function useHeadTags(config, meta) {
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, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
144
- }
145
- function escapeAttr(text) {
146
- return text
147
- .replace(/&/g, "&amp;")
148
- .replace(/"/g, "&quot;")
149
- .replace(/</g, "&lt;")
150
- .replace(/>/g, "&gt;");
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 buildJsonLd(config, meta) {
153
- const url = meta.url ?? config.site;
154
- if (meta.ogType === "article") {
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: meta.title,
159
- description: meta.description ?? config.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: meta.author ?? config.author,
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: meta.publishedTime,
170
- dateModified: meta.modifiedTime ?? meta.publishedTime,
171
- image: meta.ogImage ?? config.ogImage,
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, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
514
+ }
515
+ function escapeAttr(text) {
516
+ return text
517
+ .replace(/&/g, "&amp;")
518
+ .replace(/"/g, "&quot;")
519
+ .replace(/</g, "&lt;")
520
+ .replace(/>/g, "&gt;");
521
+ }
190
522
  //# sourceMappingURL=seo.service.js.map