@cosmicdrift/kumiko-headless 0.84.0 → 0.85.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -2
- package/src/apex/__tests__/render.test.ts +82 -0
- package/src/apex/index.ts +40 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmicdrift/kumiko-headless",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.85.0",
|
|
4
4
|
"description": "Headless UI logic for Kumiko — Dispatcher contract, Form-Controller, View-Model, Nav-Resolver. Plattform- und React-frei; jeder Renderer (renderer, renderer-web, renderer-native, …) komponiert darauf.",
|
|
5
5
|
"license": "BUSL-1.1",
|
|
6
6
|
"author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
}
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
|
-
"@cosmicdrift/kumiko-framework": "0.
|
|
35
|
+
"@cosmicdrift/kumiko-framework": "0.85.0",
|
|
36
36
|
"zod": "^4.4.3"
|
|
37
37
|
},
|
|
38
38
|
"publishConfig": {
|
|
@@ -141,4 +141,86 @@ describe("renderApexPage", () => {
|
|
|
141
141
|
expect(html).toContain("id='raw'");
|
|
142
142
|
expect(html).toContain("trust-item");
|
|
143
143
|
});
|
|
144
|
+
|
|
145
|
+
test("renders robots meta when set, absent when omitted", () => {
|
|
146
|
+
const without = renderApexPage(page());
|
|
147
|
+
expect(without).not.toContain("robots");
|
|
148
|
+
const withRobots = renderApexPage(
|
|
149
|
+
page({ head: { lang: "de", title: "T", description: "D", robots: "noindex, nofollow" } }),
|
|
150
|
+
);
|
|
151
|
+
expect(withRobots).toContain('<meta name="robots" content="noindex, nofollow" />');
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
test("renders og:site_name and og:locale", () => {
|
|
155
|
+
const html = renderApexPage(
|
|
156
|
+
page({
|
|
157
|
+
head: {
|
|
158
|
+
lang: "de",
|
|
159
|
+
title: "T",
|
|
160
|
+
description: "D",
|
|
161
|
+
siteName: "Acme",
|
|
162
|
+
locale: "de_DE",
|
|
163
|
+
},
|
|
164
|
+
}),
|
|
165
|
+
);
|
|
166
|
+
expect(html).toContain('<meta property="og:site_name" content="Acme" />');
|
|
167
|
+
expect(html).toContain('<meta property="og:locale" content="de_DE" />');
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
test("renders twitter:card when ogImage is set, plus twitter:site", () => {
|
|
171
|
+
const html = renderApexPage(
|
|
172
|
+
page({
|
|
173
|
+
head: {
|
|
174
|
+
lang: "de",
|
|
175
|
+
title: "T",
|
|
176
|
+
description: "D",
|
|
177
|
+
ogImage: "https://example.com/image.png",
|
|
178
|
+
twitterSite: "@acme",
|
|
179
|
+
},
|
|
180
|
+
}),
|
|
181
|
+
);
|
|
182
|
+
expect(html).toContain('<meta name="twitter:card" content="summary_large_image" />');
|
|
183
|
+
expect(html).toContain('<meta name="twitter:site" content="@acme" />');
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
test("does not render twitter:card without ogImage", () => {
|
|
187
|
+
const html = renderApexPage(
|
|
188
|
+
page({
|
|
189
|
+
head: { lang: "de", title: "T", description: "D", twitterSite: "@acme" },
|
|
190
|
+
}),
|
|
191
|
+
);
|
|
192
|
+
expect(html).not.toContain("twitter:card");
|
|
193
|
+
expect(html).toContain("twitter:site");
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
test("renders preconnect links", () => {
|
|
197
|
+
const html = renderApexPage(
|
|
198
|
+
page({
|
|
199
|
+
head: {
|
|
200
|
+
lang: "de",
|
|
201
|
+
title: "T",
|
|
202
|
+
description: "D",
|
|
203
|
+
preconnects: ["https://fonts.example.com", "https://api.example.com"],
|
|
204
|
+
},
|
|
205
|
+
}),
|
|
206
|
+
);
|
|
207
|
+
expect(html).toContain('<link rel="preconnect" href="https://fonts.example.com" />');
|
|
208
|
+
expect(html).toContain('<link rel="preconnect" href="https://api.example.com" />');
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
test("renders schemaJson as json-ld script tag", () => {
|
|
212
|
+
const html = renderApexPage(
|
|
213
|
+
page({
|
|
214
|
+
head: {
|
|
215
|
+
lang: "de",
|
|
216
|
+
title: "T",
|
|
217
|
+
description: "D",
|
|
218
|
+
schemaJson: { "@context": "https://schema.org", "@type": "WebSite", name: "Acme" },
|
|
219
|
+
},
|
|
220
|
+
}),
|
|
221
|
+
);
|
|
222
|
+
expect(html).toContain('<script type="application/ld+json">');
|
|
223
|
+
expect(html).toContain('{"@context":"https://schema.org","@type":"WebSite","name":"Acme"}');
|
|
224
|
+
expect(html).toContain("</script>");
|
|
225
|
+
});
|
|
144
226
|
});
|
package/src/apex/index.ts
CHANGED
|
@@ -140,6 +140,18 @@ export type ApexHead = {
|
|
|
140
140
|
readonly ogImage?: string;
|
|
141
141
|
/** hreflang alternates for multilingual SEO (e.g. the other language's URL). */
|
|
142
142
|
readonly alternates?: readonly { readonly hreflang: string; readonly href: string }[];
|
|
143
|
+
/** Robots meta content (e.g. "index, follow", "noindex, nofollow"). Omit = no tag. */
|
|
144
|
+
readonly robots?: string;
|
|
145
|
+
/** og:site_name — brand name for social shares. */
|
|
146
|
+
readonly siteName?: string;
|
|
147
|
+
/** og:locale — e.g. "de_DE", "en_US". */
|
|
148
|
+
readonly locale?: string;
|
|
149
|
+
/** twitter:site — @handle for X/Twitter card. */
|
|
150
|
+
readonly twitterSite?: string;
|
|
151
|
+
/** URLs for <link rel="preconnect"> hints (Core Web Vitals). */
|
|
152
|
+
readonly preconnects?: readonly string[];
|
|
153
|
+
/** Arbitrary JSON-LD structured data (Schema.org). Rendered as-is into <script type="application/ld+json">. */
|
|
154
|
+
readonly schemaJson?: Record<string, unknown>;
|
|
143
155
|
};
|
|
144
156
|
|
|
145
157
|
export type ApexPage = {
|
|
@@ -393,6 +405,33 @@ export function renderApexPage(page: ApexPage): string {
|
|
|
393
405
|
`\n <link rel="alternate" hreflang="${escapeHtml(a.hreflang)}" href="${escapeHtml(a.href)}" />`,
|
|
394
406
|
)
|
|
395
407
|
.join("");
|
|
408
|
+
const robots =
|
|
409
|
+
head.robots !== undefined
|
|
410
|
+
? `\n <meta name="robots" content="${escapeHtml(head.robots)}" />`
|
|
411
|
+
: "";
|
|
412
|
+
const siteName =
|
|
413
|
+
head.siteName !== undefined
|
|
414
|
+
? `\n <meta property="og:site_name" content="${escapeHtml(head.siteName)}" />`
|
|
415
|
+
: "";
|
|
416
|
+
const locale =
|
|
417
|
+
head.locale !== undefined
|
|
418
|
+
? `\n <meta property="og:locale" content="${escapeHtml(head.locale)}" />`
|
|
419
|
+
: "";
|
|
420
|
+
const twitterCard =
|
|
421
|
+
head.ogImage !== undefined
|
|
422
|
+
? `\n <meta name="twitter:card" content="summary_large_image" />`
|
|
423
|
+
: "";
|
|
424
|
+
const twitterSite =
|
|
425
|
+
head.twitterSite !== undefined
|
|
426
|
+
? `\n <meta name="twitter:site" content="${escapeHtml(head.twitterSite)}" />`
|
|
427
|
+
: "";
|
|
428
|
+
const preconnects = (head.preconnects ?? [])
|
|
429
|
+
.map((url) => `\n <link rel="preconnect" href="${escapeHtml(url)}" />`)
|
|
430
|
+
.join("");
|
|
431
|
+
const schema =
|
|
432
|
+
head.schemaJson !== undefined
|
|
433
|
+
? `\n <script type="application/ld+json">${JSON.stringify(head.schemaJson)}</script>`
|
|
434
|
+
: "";
|
|
396
435
|
return `<!doctype html>
|
|
397
436
|
<html lang="${escapeHtml(head.lang)}">
|
|
398
437
|
<head>
|
|
@@ -402,7 +441,7 @@ export function renderApexPage(page: ApexPage): string {
|
|
|
402
441
|
<meta name="description" content="${escapeHtml(head.description)}" />
|
|
403
442
|
<meta property="og:title" content="${escapeHtml(head.title)}" />
|
|
404
443
|
<meta property="og:description" content="${escapeHtml(head.description)}" />
|
|
405
|
-
<meta property="og:type" content="website" />${ogUrl}${ogImage}${favicon}${canonical}${alternates}
|
|
444
|
+
<meta property="og:type" content="website" />${ogUrl}${ogImage}${siteName}${locale}${twitterCard}${twitterSite}${favicon}${canonical}${alternates}${robots}${preconnects}${schema}
|
|
406
445
|
<style>${css}</style>
|
|
407
446
|
</head>
|
|
408
447
|
<body${theme === "dark" ? ` class="apex-dark"` : ""}>
|