@cosmicdrift/kumiko-headless 0.84.0 → 0.86.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cosmicdrift/kumiko-headless",
3
- "version": "0.84.0",
3
+ "version": "0.86.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.84.0",
35
+ "@cosmicdrift/kumiko-framework": "0.86.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"` : ""}>