@caelo-cms/shared 0.10.23 → 0.10.24
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/dist/ai-tools.d.ts +0 -60
- package/dist/ai-tools.d.ts.map +1 -1
- package/dist/ai-tools.js +1 -59
- package/dist/ai-tools.js.map +1 -1
- package/dist/base-css.d.ts +11 -0
- package/dist/base-css.d.ts.map +1 -1
- package/dist/base-css.js +13 -1
- package/dist/base-css.js.map +1 -1
- package/dist/design-draft-shell.d.ts +21 -0
- package/dist/design-draft-shell.d.ts.map +1 -0
- package/dist/design-draft-shell.js +81 -0
- package/dist/design-draft-shell.js.map +1 -0
- package/dist/genesis.d.ts +43 -3
- package/dist/genesis.d.ts.map +1 -1
- package/dist/genesis.js +72 -5
- package/dist/genesis.js.map +1 -1
- package/dist/i18n.d.ts +0 -50
- package/dist/i18n.d.ts.map +1 -1
- package/dist/i18n.js +8 -149
- package/dist/i18n.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/preview-compose.d.ts +9 -15
- package/dist/preview-compose.d.ts.map +1 -1
- package/dist/preview-compose.js +3 -43
- package/dist/preview-compose.js.map +1 -1
- package/dist/seo.d.ts +8 -21
- package/dist/seo.d.ts.map +1 -1
- package/dist/seo.js +4 -22
- package/dist/seo.js.map +1 -1
- package/dist/structured-sets.d.ts +0 -36
- package/dist/structured-sets.d.ts.map +1 -1
- package/dist/structured-sets.js +1 -52
- package/dist/structured-sets.js.map +1 -1
- package/dist/version.d.ts +2 -2
- package/dist/version.js +1 -1
- package/package.json +2 -2
- package/src/ai-tools.ts +1 -75
- package/src/base-css.ts +13 -1
- package/src/design-draft-shell.test.ts +85 -0
- package/src/design-draft-shell.ts +109 -0
- package/src/genesis.ts +76 -5
- package/src/i18n.test.ts +1 -217
- package/src/i18n.ts +8 -186
- package/src/index.ts +1 -1
- package/src/preview-compose.test.ts +35 -0
- package/src/preview-compose.ts +3 -56
- package/src/seo.test.ts +0 -40
- package/src/seo.ts +8 -36
- package/src/structured-sets.ts +1 -67
- package/src/version.ts +1 -1
- package/dist/translation.d.ts +0 -127
- package/dist/translation.d.ts.map +0 -1
- package/dist/translation.js +0 -208
- package/dist/translation.js.map +0 -1
- package/src/translation.test.ts +0 -160
- package/src/translation.ts +0 -295
package/src/index.ts
CHANGED
|
@@ -11,6 +11,7 @@ export * from "./content.js";
|
|
|
11
11
|
export * from "./context.js";
|
|
12
12
|
export * from "./css-gradient-scan.js";
|
|
13
13
|
export * from "./css-var-scan.js";
|
|
14
|
+
export * from "./design-draft-shell.js";
|
|
14
15
|
export * from "./design-manifest.js";
|
|
15
16
|
export * from "./fonts.js";
|
|
16
17
|
export * from "./genesis.js";
|
|
@@ -55,5 +56,4 @@ export * from "./theme-ramp.js";
|
|
|
55
56
|
export * from "./theme-render.js";
|
|
56
57
|
export * from "./themes.js";
|
|
57
58
|
export * from "./themes-errors.js";
|
|
58
|
-
export * from "./translation.js";
|
|
59
59
|
export { CAELO_VERSION, CALEO_VERSION, type CaeloVersion, parseVersion } from "./version.js";
|
|
@@ -107,6 +107,22 @@ describe("composePagePreview", () => {
|
|
|
107
107
|
});
|
|
108
108
|
expect(out.missingSlots).toEqual(["missing"]);
|
|
109
109
|
});
|
|
110
|
+
|
|
111
|
+
it("applies the theme's --font-body to body via the technical baseline (issue #410)", () => {
|
|
112
|
+
// Regression: --font-body was declared by the theme renderer but no
|
|
113
|
+
// rule consumed it, so fresh sites rendered the UA serif. The base
|
|
114
|
+
// block must apply it (with a declared cascade default for
|
|
115
|
+
// themeless installs).
|
|
116
|
+
const out = composePagePreview({
|
|
117
|
+
templateHtml: `<head></head><body><caelo-slot name="x">_</caelo-slot></body>`,
|
|
118
|
+
templateCss: "",
|
|
119
|
+
blocks: [{ blockName: "x", modules: [{ ...blankModule, html: "<p>x</p>" }] }],
|
|
120
|
+
});
|
|
121
|
+
const base = out.html.indexOf('<style data-source="base">');
|
|
122
|
+
expect(base).toBeGreaterThan(-1);
|
|
123
|
+
const baseBlock = out.html.slice(base, out.html.indexOf("</style>", base));
|
|
124
|
+
expect(baseBlock).toContain("body{font-family:var(--font-body,system-ui,sans-serif)}");
|
|
125
|
+
});
|
|
110
126
|
});
|
|
111
127
|
|
|
112
128
|
describe("tagModuleId (P6.7 live-edit overlay support)", () => {
|
|
@@ -536,6 +552,25 @@ describe("composePageWithLayout", () => {
|
|
|
536
552
|
expect(baseBlock).not.toMatch(/#[0-9a-f]{3}|font-size|color:|rem\b/i);
|
|
537
553
|
});
|
|
538
554
|
|
|
555
|
+
it("applies the theme's --font-body to body via the technical baseline (issue #410)", () => {
|
|
556
|
+
// Same regression as the composePagePreview variant: both compose
|
|
557
|
+
// paths must ship a consumer for --font-body or the theme's chosen
|
|
558
|
+
// stack never reaches an element.
|
|
559
|
+
const out = composePageWithLayout({
|
|
560
|
+
templateHtml,
|
|
561
|
+
templateCss: "",
|
|
562
|
+
blocks: [],
|
|
563
|
+
layoutHtml,
|
|
564
|
+
layoutCss: "",
|
|
565
|
+
layoutBlocks: [],
|
|
566
|
+
layoutSlug: "test",
|
|
567
|
+
});
|
|
568
|
+
const base = out.html.indexOf('<style data-source="base">');
|
|
569
|
+
expect(base).toBeGreaterThan(-1);
|
|
570
|
+
const baseBlock = out.html.slice(base, out.html.indexOf("</style>", base));
|
|
571
|
+
expect(baseBlock).toContain("body{font-family:var(--font-body,system-ui,sans-serif)}");
|
|
572
|
+
});
|
|
573
|
+
|
|
539
574
|
it("emits a twice-placed module's CSS/JS exactly once (issue #158)", () => {
|
|
540
575
|
const shared = {
|
|
541
576
|
...blankModule,
|
package/src/preview-compose.ts
CHANGED
|
@@ -34,7 +34,6 @@ import {
|
|
|
34
34
|
listSlotNames,
|
|
35
35
|
} from "./preview-scanner.js";
|
|
36
36
|
import { stripCdataGuards } from "./strip-cdata.js";
|
|
37
|
-
import { type LanguageSelectorOverride, renderLanguageSelector } from "./structured-sets.js";
|
|
38
37
|
import { renderTemplate, type TemplateField } from "./template-engine.js";
|
|
39
38
|
import { renderThemeCss as renderThemeCssFromTokens } from "./theme-render.js";
|
|
40
39
|
import type { ThemeDocument } from "./themes.js";
|
|
@@ -90,21 +89,6 @@ export interface ComposeStructuredSets {
|
|
|
90
89
|
readonly byKindSlug: Readonly<Record<string, readonly unknown[]>>;
|
|
91
90
|
}
|
|
92
91
|
|
|
93
|
-
/**
|
|
94
|
-
* P9 — language-selector context. The caller (preview op + static
|
|
95
|
-
* generator) resolves the current page's per-locale URLs once and
|
|
96
|
-
* threads them in. The composer renders the selector when a module
|
|
97
|
-
* slug starts with `language-selector-`.
|
|
98
|
-
*/
|
|
99
|
-
export interface ComposeLanguageSelector {
|
|
100
|
-
readonly availableLocales: ReadonlyArray<{
|
|
101
|
-
code: string;
|
|
102
|
-
displayName: string;
|
|
103
|
-
href: string;
|
|
104
|
-
isCurrent: boolean;
|
|
105
|
-
}>;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
92
|
/**
|
|
109
93
|
* v0.11.0 — Theme context resolved by the preview op + static generator
|
|
110
94
|
* (#45 Phase 3). Carries the active theme's DTCG tokens jsonb plus the
|
|
@@ -147,7 +131,6 @@ export interface ComposeInput {
|
|
|
147
131
|
readonly templateCss: string;
|
|
148
132
|
readonly blocks: readonly ComposeBlock[];
|
|
149
133
|
readonly structuredSets?: ComposeStructuredSets;
|
|
150
|
-
readonly languageSelector?: ComposeLanguageSelector;
|
|
151
134
|
/**
|
|
152
135
|
* v0.11.0 — active theme threaded through from the preview op + static
|
|
153
136
|
* generator. Undefined when no theme row exists OR when the caller
|
|
@@ -180,8 +163,10 @@ function injectBefore(source: string, marker: RegExp, fragment: string): string
|
|
|
180
163
|
* `crossorigin` attribute is REQUIRED for font preloads even same-origin,
|
|
181
164
|
* per the fetch spec's font-destination CORS rule) + the @font-face
|
|
182
165
|
* block. Empty css with no preloads → null (system-stack-only theme).
|
|
166
|
+
* Exported for the design-draft theme shell (#375) so draft previews
|
|
167
|
+
* load the identical fonts as the page preview.
|
|
183
168
|
*/
|
|
184
|
-
function fontsHeadFragment(fonts: ComposeFonts | undefined): string | null {
|
|
169
|
+
export function fontsHeadFragment(fonts: ComposeFonts | undefined): string | null {
|
|
185
170
|
if (fonts === undefined) return null;
|
|
186
171
|
const links = fonts.preloads
|
|
187
172
|
.map((href) => `<link rel="preload" as="font" type="font/woff2" crossorigin href="${href}">`)
|
|
@@ -214,13 +199,10 @@ export function composePagePreview(input: ComposeInput): ComposeOutput {
|
|
|
214
199
|
// module HTML.
|
|
215
200
|
const renderedModuleHtml = block.modules.map((m) => {
|
|
216
201
|
const navMenuItems = lookupNavMenuItems(m.slug, input.structuredSets);
|
|
217
|
-
const langSelector = lookupLanguageSelector(m.slug, input);
|
|
218
202
|
let baseHtml: string;
|
|
219
203
|
if (navMenuItems !== null) {
|
|
220
204
|
navRendered = true;
|
|
221
205
|
baseHtml = renderNavMenuHtml(navMenuItems);
|
|
222
|
-
} else if (langSelector !== null) {
|
|
223
|
-
baseHtml = langSelector;
|
|
224
206
|
} else {
|
|
225
207
|
baseHtml = applyFieldSubstitution(m.html, m.fields, m.contentValues, input.theme);
|
|
226
208
|
}
|
|
@@ -314,35 +296,6 @@ function lookupNavMenuItems(
|
|
|
314
296
|
return items ?? null;
|
|
315
297
|
}
|
|
316
298
|
|
|
317
|
-
/**
|
|
318
|
-
* P9 — return rendered language-selector HTML when a module's slug
|
|
319
|
-
* starts with `language-selector-`. The set's items act as overrides
|
|
320
|
-
* (relabel a locale, hide one); the available-locale list comes from
|
|
321
|
-
* the caller via `input.languageSelector`. Returns null when the
|
|
322
|
-
* module is not a language selector.
|
|
323
|
-
*
|
|
324
|
-
* Convention: a module slug `language-selector-header` resolves to
|
|
325
|
-
* structuredSets[`language-selector/header`] for overrides, and the
|
|
326
|
-
* rendered HTML lists every locale that has a published variant of
|
|
327
|
-
* the current page.
|
|
328
|
-
*/
|
|
329
|
-
function lookupLanguageSelector(moduleSlug: string, input: ComposeInput): string | null {
|
|
330
|
-
const prefix = "language-selector-";
|
|
331
|
-
if (!moduleSlug.startsWith(prefix)) return null;
|
|
332
|
-
if (!input.languageSelector || input.languageSelector.availableLocales.length === 0) {
|
|
333
|
-
return null;
|
|
334
|
-
}
|
|
335
|
-
const setSlug = moduleSlug.slice(prefix.length);
|
|
336
|
-
const overridesUnknown = input.structuredSets?.byKindSlug[`language-selector/${setSlug}`];
|
|
337
|
-
const overrides = Array.isArray(overridesUnknown)
|
|
338
|
-
? (overridesUnknown as LanguageSelectorOverride[])
|
|
339
|
-
: undefined;
|
|
340
|
-
return renderLanguageSelector({
|
|
341
|
-
availableLocales: input.languageSelector.availableLocales,
|
|
342
|
-
overrides,
|
|
343
|
-
});
|
|
344
|
-
}
|
|
345
|
-
|
|
346
299
|
interface NavMenuItem {
|
|
347
300
|
label: string;
|
|
348
301
|
href: string;
|
|
@@ -593,13 +546,10 @@ export function composePageWithLayout(input: ComposeWithLayoutInput): ComposeOut
|
|
|
593
546
|
for (const block of input.blocks) {
|
|
594
547
|
const renderedModuleHtml = block.modules.map((m) => {
|
|
595
548
|
const navMenuItems = lookupNavMenuItems(m.slug, input.structuredSets);
|
|
596
|
-
const langSelector = lookupLanguageSelector(m.slug, input);
|
|
597
549
|
let baseHtml: string;
|
|
598
550
|
if (navMenuItems !== null) {
|
|
599
551
|
navRendered = true;
|
|
600
552
|
baseHtml = renderNavMenuHtml(navMenuItems);
|
|
601
|
-
} else if (langSelector !== null) {
|
|
602
|
-
baseHtml = langSelector;
|
|
603
553
|
} else {
|
|
604
554
|
baseHtml = applyFieldSubstitution(m.html, m.fields, m.contentValues, input.theme);
|
|
605
555
|
}
|
|
@@ -628,13 +578,10 @@ export function composePageWithLayout(input: ComposeWithLayoutInput): ComposeOut
|
|
|
628
578
|
if (block.blockName === "content") continue; // reserved for the page body
|
|
629
579
|
const renderedModuleHtml = block.modules.map((m) => {
|
|
630
580
|
const navMenuItems = lookupNavMenuItems(m.slug, input.structuredSets);
|
|
631
|
-
const langSelector = lookupLanguageSelector(m.slug, input);
|
|
632
581
|
let baseHtml: string;
|
|
633
582
|
if (navMenuItems !== null) {
|
|
634
583
|
navRendered = true;
|
|
635
584
|
baseHtml = renderNavMenuHtml(navMenuItems);
|
|
636
|
-
} else if (langSelector !== null) {
|
|
637
|
-
baseHtml = langSelector;
|
|
638
585
|
} else {
|
|
639
586
|
baseHtml = applyFieldSubstitution(m.html, m.fields, m.contentValues, input.theme);
|
|
640
587
|
}
|
package/src/seo.test.ts
CHANGED
|
@@ -17,7 +17,6 @@ describe("resolveCanonicalUrl", () => {
|
|
|
17
17
|
resolveCanonicalUrl({
|
|
18
18
|
siteBaseUrl: "https://example.com",
|
|
19
19
|
pageSlug: "anything",
|
|
20
|
-
pageLocale: "en",
|
|
21
20
|
override: "https://canonical.example.com/x",
|
|
22
21
|
}),
|
|
23
22
|
).toBe("https://canonical.example.com/x");
|
|
@@ -28,7 +27,6 @@ describe("resolveCanonicalUrl", () => {
|
|
|
28
27
|
resolveCanonicalUrl({
|
|
29
28
|
siteBaseUrl: "https://example.com",
|
|
30
29
|
pageSlug: "home",
|
|
31
|
-
pageLocale: "en",
|
|
32
30
|
override: null,
|
|
33
31
|
}),
|
|
34
32
|
).toBe("https://example.com/");
|
|
@@ -39,7 +37,6 @@ describe("resolveCanonicalUrl", () => {
|
|
|
39
37
|
resolveCanonicalUrl({
|
|
40
38
|
siteBaseUrl: "https://example.com/",
|
|
41
39
|
pageSlug: "about",
|
|
42
|
-
pageLocale: "en",
|
|
43
40
|
override: null,
|
|
44
41
|
}),
|
|
45
42
|
).toBe("https://example.com/about/");
|
|
@@ -51,7 +48,6 @@ describe("resolveCanonicalUrl", () => {
|
|
|
51
48
|
resolveCanonicalUrl({
|
|
52
49
|
siteBaseUrl: "https://example.com",
|
|
53
50
|
pageSlug: "about",
|
|
54
|
-
pageLocale: "en",
|
|
55
51
|
override: null,
|
|
56
52
|
pageUrlStyle: "no-extension",
|
|
57
53
|
}),
|
|
@@ -63,37 +59,17 @@ describe("resolveCanonicalUrl", () => {
|
|
|
63
59
|
resolveCanonicalUrl({
|
|
64
60
|
siteBaseUrl: "https://example.com",
|
|
65
61
|
pageSlug: "home",
|
|
66
|
-
pageLocale: "en",
|
|
67
62
|
override: null,
|
|
68
63
|
pageUrlStyle: "no-extension",
|
|
69
64
|
}),
|
|
70
65
|
).toBe("https://example.com/");
|
|
71
66
|
});
|
|
72
67
|
|
|
73
|
-
it("subdirectory locale strategy keeps the locale prefix, no trailing slash on slug", () => {
|
|
74
|
-
expect(
|
|
75
|
-
resolveCanonicalUrl({
|
|
76
|
-
siteBaseUrl: "https://example.com",
|
|
77
|
-
pageSlug: "about",
|
|
78
|
-
pageLocale: "de",
|
|
79
|
-
override: null,
|
|
80
|
-
pageUrlStyle: "no-extension",
|
|
81
|
-
localeConfig: {
|
|
82
|
-
code: "de",
|
|
83
|
-
urlStrategy: "subdirectory",
|
|
84
|
-
urlHost: null,
|
|
85
|
-
isDefault: false,
|
|
86
|
-
},
|
|
87
|
-
}),
|
|
88
|
-
).toBe("https://example.com/de/about");
|
|
89
|
-
});
|
|
90
|
-
|
|
91
68
|
it("default style preserves pre-v0.2.85 trailing-slash behavior", () => {
|
|
92
69
|
expect(
|
|
93
70
|
resolveCanonicalUrl({
|
|
94
71
|
siteBaseUrl: "https://example.com",
|
|
95
72
|
pageSlug: "about",
|
|
96
|
-
pageLocale: "en",
|
|
97
73
|
override: null,
|
|
98
74
|
}),
|
|
99
75
|
).toBe("https://example.com/about/");
|
|
@@ -108,7 +84,6 @@ describe("renderSeoHead", () => {
|
|
|
108
84
|
canonical: "https://example.com/",
|
|
109
85
|
noindex: false,
|
|
110
86
|
ogImageUrl: null,
|
|
111
|
-
hreflang: [] as { locale: string; url: string }[],
|
|
112
87
|
organization: {},
|
|
113
88
|
};
|
|
114
89
|
|
|
@@ -136,21 +111,6 @@ describe("renderSeoHead", () => {
|
|
|
136
111
|
expect(head).toContain('<meta name="twitter:card" content="summary_large_image" />');
|
|
137
112
|
});
|
|
138
113
|
|
|
139
|
-
it("emits hreflang per row + x-default when any rows are present", () => {
|
|
140
|
-
const head = renderSeoHead({
|
|
141
|
-
...base,
|
|
142
|
-
hreflang: [
|
|
143
|
-
{ locale: "de", url: "https://example.com/de/" },
|
|
144
|
-
{ locale: "fr", url: "https://example.com/fr/" },
|
|
145
|
-
],
|
|
146
|
-
});
|
|
147
|
-
expect(head).toContain('<link rel="alternate" hreflang="de" href="https://example.com/de/" />');
|
|
148
|
-
expect(head).toContain('<link rel="alternate" hreflang="fr" href="https://example.com/fr/" />');
|
|
149
|
-
expect(head).toContain(
|
|
150
|
-
'<link rel="alternate" hreflang="x-default" href="https://example.com/" />',
|
|
151
|
-
);
|
|
152
|
-
});
|
|
153
|
-
|
|
154
114
|
it("emits a JSON-LD WebPage block; encodes the < character to avoid script-tag breaks", () => {
|
|
155
115
|
const head = renderSeoHead({
|
|
156
116
|
...base,
|
package/src/seo.ts
CHANGED
|
@@ -108,22 +108,14 @@ export interface SiteSeoSettings {
|
|
|
108
108
|
|
|
109
109
|
/**
|
|
110
110
|
* Resolve the canonical URL for a page. If `pages_seo.canonical_url`
|
|
111
|
-
* is set it wins; otherwise build `<siteBaseUrl>/<
|
|
112
|
-
* the
|
|
113
|
-
*
|
|
111
|
+
* is set it wins; otherwise build `<siteBaseUrl>/<slug>` (home pages
|
|
112
|
+
* canonicalize to the bare base URL). URL shaping beyond base + slug
|
|
113
|
+
* becomes a plugin contribution on the URL composition point (#390).
|
|
114
114
|
*/
|
|
115
115
|
export function resolveCanonicalUrl(args: {
|
|
116
116
|
siteBaseUrl: string;
|
|
117
117
|
pageSlug: string;
|
|
118
|
-
pageLocale: string;
|
|
119
118
|
override: string | null;
|
|
120
|
-
/** P9 — when present, the locale's url_strategy decides the URL shape. */
|
|
121
|
-
localeConfig?: {
|
|
122
|
-
code: string;
|
|
123
|
-
urlStrategy: "none" | "subdirectory" | "subdomain" | "domain";
|
|
124
|
-
urlHost: string | null;
|
|
125
|
-
isDefault: boolean;
|
|
126
|
-
};
|
|
127
119
|
/**
|
|
128
120
|
* v0.2.85 — page emission style. 'directory' (default) → URLs end
|
|
129
121
|
* in `/<slug>/`; 'no-extension' → URLs end in `/<slug>` (no
|
|
@@ -132,11 +124,10 @@ export function resolveCanonicalUrl(args: {
|
|
|
132
124
|
*/
|
|
133
125
|
pageUrlStyle?: "directory" | "no-extension";
|
|
134
126
|
/**
|
|
135
|
-
* 0184 — explicit homepage designation
|
|
136
|
-
*
|
|
137
|
-
*
|
|
138
|
-
*
|
|
139
|
-
* emitted output path.
|
|
127
|
+
* 0184 — explicit homepage designation. When true this page IS the
|
|
128
|
+
* site root and canonicalizes to `<base>/` regardless of its slug.
|
|
129
|
+
* Uses the same predicate (`isHomeSlug`) as the magic-slug fallback
|
|
130
|
+
* so canonical agrees with the emitted output path.
|
|
140
131
|
*/
|
|
141
132
|
isHomePage?: boolean;
|
|
142
133
|
}): string {
|
|
@@ -149,16 +140,6 @@ export function resolveCanonicalUrl(args: {
|
|
|
149
140
|
// shape dictated by the page-emission style. Home pages always
|
|
150
141
|
// resolve to the base URL (no tail) regardless of style.
|
|
151
142
|
const tail = cleanSlug ? (style === "no-extension" ? cleanSlug : `${cleanSlug}/`) : "";
|
|
152
|
-
const cfg = args.localeConfig;
|
|
153
|
-
if (cfg && cfg.urlStrategy !== "none") {
|
|
154
|
-
if (cfg.urlStrategy === "subdirectory") {
|
|
155
|
-
return tail ? `${base}/${cfg.code}/${tail}` : `${base}/${cfg.code}/`;
|
|
156
|
-
}
|
|
157
|
-
if ((cfg.urlStrategy === "subdomain" || cfg.urlStrategy === "domain") && cfg.urlHost) {
|
|
158
|
-
const protocol = base.startsWith("http://") ? "http://" : "https://";
|
|
159
|
-
return tail ? `${protocol}${cfg.urlHost}/${tail}` : `${protocol}${cfg.urlHost}/`;
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
143
|
return tail ? `${base}/${tail}` : `${base}/`;
|
|
163
144
|
}
|
|
164
145
|
|
|
@@ -168,7 +149,6 @@ export interface SeoMetaInput {
|
|
|
168
149
|
canonical: string;
|
|
169
150
|
noindex: boolean;
|
|
170
151
|
ogImageUrl: string | null;
|
|
171
|
-
hreflang: { locale: string; url: string }[];
|
|
172
152
|
organization: SiteSeoSettings["organization"];
|
|
173
153
|
}
|
|
174
154
|
|
|
@@ -177,7 +157,7 @@ export interface SeoMetaInput {
|
|
|
177
157
|
* the renderer injects just before </head>. Order is canonical
|
|
178
158
|
* (W3C-recommended): charset / viewport stay in the layout; meta
|
|
179
159
|
* description + canonical + robots + Open Graph + Twitter card +
|
|
180
|
-
*
|
|
160
|
+
* JSON-LD.
|
|
181
161
|
*
|
|
182
162
|
* No raw HTML escape hatch — every variable is HTML-attribute-encoded.
|
|
183
163
|
*/
|
|
@@ -208,14 +188,6 @@ export function renderSeoHead(input: SeoMetaInput): string {
|
|
|
208
188
|
lines.push(
|
|
209
189
|
`<meta name="twitter:card" content="${input.ogImageUrl ? "summary_large_image" : "summary"}" />`,
|
|
210
190
|
);
|
|
211
|
-
// Hreflang per locale + x-default. P8 ships zero rows; P9 i18n
|
|
212
|
-
// populates pages_hreflang and the existing renderer iterates.
|
|
213
|
-
for (const h of input.hreflang) {
|
|
214
|
-
lines.push(`<link rel="alternate" hreflang="${enc(h.locale)}" href="${enc(h.url)}" />`);
|
|
215
|
-
}
|
|
216
|
-
if (input.hreflang.length > 0) {
|
|
217
|
-
lines.push(`<link rel="alternate" hreflang="x-default" href="${enc(input.canonical)}" />`);
|
|
218
|
-
}
|
|
219
191
|
// JSON-LD WebPage block referencing the Organization (when set).
|
|
220
192
|
const ld: Record<string, unknown> = {
|
|
221
193
|
"@context": "https://schema.org",
|
package/src/structured-sets.ts
CHANGED
|
@@ -75,38 +75,13 @@ export const linkListItem = z
|
|
|
75
75
|
})
|
|
76
76
|
.strict();
|
|
77
77
|
|
|
78
|
-
/**
|
|
79
|
-
* P9 review-pass — `language-selector` kind. Items are user-supplied
|
|
80
|
-
* overrides (e.g. force a specific display label per locale); empty
|
|
81
|
-
* array means "auto-populate from the locale registry at render time."
|
|
82
|
-
* The renderer (apps/static-generator + preview op) reads the locale
|
|
83
|
-
* registry + the current page's slug + resolveLocaleUrl to emit
|
|
84
|
-
* `<a hreflang lang href>` rows. Closes CMS_REQUIREMENTS §7.8.
|
|
85
|
-
*/
|
|
86
|
-
export const languageSelectorOverride = z
|
|
87
|
-
.object({
|
|
88
|
-
/** BCP-47 locale code; must match a row in the locales registry. */
|
|
89
|
-
locale: z.string().min(2).max(10),
|
|
90
|
-
/** Override the locale's display_name when rendering this entry. */
|
|
91
|
-
label: z.string().min(1).max(120).optional(),
|
|
92
|
-
/** Hide this locale from the rendered selector even if it has a published page. */
|
|
93
|
-
hidden: z.boolean().optional(),
|
|
94
|
-
})
|
|
95
|
-
.strict();
|
|
96
|
-
|
|
97
78
|
/** Discriminated kind → items array. The Query API op picks the
|
|
98
79
|
* validator at runtime by reading `kind` first.
|
|
99
80
|
*
|
|
100
81
|
* v0.11.0 (#45) — `theme` is no longer a structured-set kind. The
|
|
101
82
|
* theme primitive moved to its own `themes` table with DTCG-shaped
|
|
102
83
|
* jsonb tokens; see `themes.ts` + `theme-render.ts`. */
|
|
103
|
-
export const structuredSetKind = z.enum([
|
|
104
|
-
"nav-menu",
|
|
105
|
-
"taxonomy",
|
|
106
|
-
"tags",
|
|
107
|
-
"link-list",
|
|
108
|
-
"language-selector",
|
|
109
|
-
]);
|
|
84
|
+
export const structuredSetKind = z.enum(["nav-menu", "taxonomy", "tags", "link-list"]);
|
|
110
85
|
export type StructuredSetKind = z.infer<typeof structuredSetKind>;
|
|
111
86
|
|
|
112
87
|
/** Validate `items` against the right schema for `kind`. Throws a
|
|
@@ -130,8 +105,6 @@ export function validateStructuredSetItems(kind: StructuredSetKind, items: unkno
|
|
|
130
105
|
return items.map((it) => tagItem.parse(it));
|
|
131
106
|
case "link-list":
|
|
132
107
|
return items.map((it) => linkListItem.parse(it));
|
|
133
|
-
case "language-selector":
|
|
134
|
-
return items.map((it) => languageSelectorOverride.parse(it));
|
|
135
108
|
}
|
|
136
109
|
}
|
|
137
110
|
|
|
@@ -139,42 +112,3 @@ export type NavMenuItem = z.infer<typeof navMenuItem>;
|
|
|
139
112
|
export type TaxonomyItem = z.infer<typeof taxonomyItem>;
|
|
140
113
|
export type TagItem = z.infer<typeof tagItem>;
|
|
141
114
|
export type LinkListItem = z.infer<typeof linkListItem>;
|
|
142
|
-
export type LanguageSelectorOverride = z.infer<typeof languageSelectorOverride>;
|
|
143
|
-
|
|
144
|
-
/**
|
|
145
|
-
* Render a `<nav class="caelo-language-selector">` with one `<a>`
|
|
146
|
-
* per locale that has a published variant of the current page. Pure
|
|
147
|
-
* function — caller threads the page's published-locale list and a
|
|
148
|
-
* URL resolver. Used by `composePagePreview` (admin) and the static
|
|
149
|
-
* generator (deploy) so byte-for-byte output parity holds.
|
|
150
|
-
*
|
|
151
|
-
* Per CMS_REQUIREMENTS §7.8: locales with no published variant for the
|
|
152
|
-
* current page are excluded.
|
|
153
|
-
*/
|
|
154
|
-
export function renderLanguageSelector(args: {
|
|
155
|
-
/** Each locale that has a published variant of the current page. */
|
|
156
|
-
readonly availableLocales: ReadonlyArray<{
|
|
157
|
-
code: string;
|
|
158
|
-
displayName: string;
|
|
159
|
-
href: string;
|
|
160
|
-
isCurrent: boolean;
|
|
161
|
-
}>;
|
|
162
|
-
/** Owner-supplied overrides (relabel a locale, hide one). */
|
|
163
|
-
readonly overrides?: ReadonlyArray<LanguageSelectorOverride>;
|
|
164
|
-
}): string {
|
|
165
|
-
const overrideByLocale = new Map<string, LanguageSelectorOverride>(
|
|
166
|
-
(args.overrides ?? []).map((o) => [o.locale, o]),
|
|
167
|
-
);
|
|
168
|
-
const enc = (s: string): string =>
|
|
169
|
-
s.replace(/&/g, "&").replace(/"/g, """).replace(/</g, "<").replace(/>/g, ">");
|
|
170
|
-
const items = args.availableLocales
|
|
171
|
-
.filter((l) => !overrideByLocale.get(l.code)?.hidden)
|
|
172
|
-
.map((l) => {
|
|
173
|
-
const override = overrideByLocale.get(l.code);
|
|
174
|
-
const label = override?.label ?? l.displayName;
|
|
175
|
-
const aria = l.isCurrent ? ' aria-current="true"' : "";
|
|
176
|
-
return `<a hreflang="${enc(l.code)}" lang="${enc(l.code)}" href="${enc(l.href)}"${aria}>${enc(label)}</a>`;
|
|
177
|
-
});
|
|
178
|
-
if (items.length === 0) return "";
|
|
179
|
-
return `<nav class="caelo-language-selector" aria-label="Language">${items.join("")}</nav>`;
|
|
180
|
-
}
|
package/src/version.ts
CHANGED
package/dist/translation.d.ts
DELETED
|
@@ -1,127 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* P10 — translation primitives shared between the AI prompt builder,
|
|
3
|
-
* the Mode 1 / Mode 2 op handlers, and the dashboard.
|
|
4
|
-
*
|
|
5
|
-
* computeBlockDiff(source, variant) — block-level diff per
|
|
6
|
-
* CMS_REQUIREMENTS §7.6: matches modules by (block_name, position),
|
|
7
|
-
* returns typed `changed` / `added` / `removed` operations. Pure
|
|
8
|
-
* function; the AI receives this as Mode 2 prompt context.
|
|
9
|
-
*
|
|
10
|
-
* buildModeOnePrompt(...) / buildModeTwoPrompt(...) — assemble the
|
|
11
|
-
* §7.6 sample-payload shapes with glossary + style guide injected.
|
|
12
|
-
* Returns the prompt + a structured payload for unit-tests to
|
|
13
|
-
* assert against.
|
|
14
|
-
*/
|
|
15
|
-
import { z } from "zod";
|
|
16
|
-
export interface ModuleBlockSlot {
|
|
17
|
-
/** The block (e.g. "content", "header") this module sits in. */
|
|
18
|
-
blockName: string;
|
|
19
|
-
/** Position within the block. Blocks are ordered + 0-based. */
|
|
20
|
-
position: number;
|
|
21
|
-
/** Module id — locked across locales (translations share refs). */
|
|
22
|
-
moduleId: string;
|
|
23
|
-
/** Display name shown to the AI for context. */
|
|
24
|
-
moduleSlug: string;
|
|
25
|
-
/** The translatable fields of the module. P10 supports HTML body
|
|
26
|
-
* and three SEO-style metadata fields. Extend as new typed fields
|
|
27
|
-
* land (e.g. structured-content blocks in P12A). */
|
|
28
|
-
html: string;
|
|
29
|
-
altText: string | null;
|
|
30
|
-
caption: string | null;
|
|
31
|
-
}
|
|
32
|
-
export type BlockDiffOp = {
|
|
33
|
-
kind: "changed";
|
|
34
|
-
blockName: string;
|
|
35
|
-
position: number;
|
|
36
|
-
moduleId: string;
|
|
37
|
-
before: ModuleBlockSlot;
|
|
38
|
-
after: ModuleBlockSlot;
|
|
39
|
-
} | {
|
|
40
|
-
kind: "added";
|
|
41
|
-
blockName: string;
|
|
42
|
-
position: number;
|
|
43
|
-
module: ModuleBlockSlot;
|
|
44
|
-
} | {
|
|
45
|
-
kind: "removed";
|
|
46
|
-
blockName: string;
|
|
47
|
-
position: number;
|
|
48
|
-
module: ModuleBlockSlot;
|
|
49
|
-
};
|
|
50
|
-
/**
|
|
51
|
-
* Diff a source page's modules against a variant's modules. Match by
|
|
52
|
-
* (blockName, position) — the §7.5 contract is that translations
|
|
53
|
-
* share STRUCTURE with the source (block + position alignment) and
|
|
54
|
-
* differ only in CONTENT (html / alt / caption per locale). Anything
|
|
55
|
-
* mis-aligned is `added` (source has it, variant doesn't) or
|
|
56
|
-
* `removed` (variant has it, source doesn't).
|
|
57
|
-
*
|
|
58
|
-
* `changed` rows contain BOTH before + after so the AI prompt can
|
|
59
|
-
* show the structured diff per the §7.6 sample payload.
|
|
60
|
-
*/
|
|
61
|
-
export declare function computeBlockDiff(source: readonly ModuleBlockSlot[], variant: readonly ModuleBlockSlot[]): BlockDiffOp[];
|
|
62
|
-
export interface GlossaryEntry {
|
|
63
|
-
sourceTerm: string;
|
|
64
|
-
translation: string;
|
|
65
|
-
context: string | null;
|
|
66
|
-
}
|
|
67
|
-
export interface ModeOnePromptInput {
|
|
68
|
-
/** Source-locale code (e.g. 'en'). */
|
|
69
|
-
sourceLocale: string;
|
|
70
|
-
/** Target locale code (e.g. 'de-AT'). */
|
|
71
|
-
targetLocale: string;
|
|
72
|
-
/** Optional locale displayName for context (e.g. "German (Austria)"). */
|
|
73
|
-
targetLocaleDisplayName?: string;
|
|
74
|
-
/** Source-page modules. The AI translates each per (blockName, position). */
|
|
75
|
-
sourceModules: readonly ModuleBlockSlot[];
|
|
76
|
-
glossary: readonly GlossaryEntry[];
|
|
77
|
-
styleGuide: string | null;
|
|
78
|
-
}
|
|
79
|
-
export interface ModeTwoPromptInput {
|
|
80
|
-
sourceLocale: string;
|
|
81
|
-
targetLocale: string;
|
|
82
|
-
targetLocaleDisplayName?: string;
|
|
83
|
-
sourceModules: readonly ModuleBlockSlot[];
|
|
84
|
-
/** The CURRENT variant — passed in full so the AI keeps the prior
|
|
85
|
-
* translation quality on unchanged blocks. */
|
|
86
|
-
variantModules: readonly ModuleBlockSlot[];
|
|
87
|
-
/** Pre-computed diff (call computeBlockDiff first). */
|
|
88
|
-
diff: readonly BlockDiffOp[];
|
|
89
|
-
glossary: readonly GlossaryEntry[];
|
|
90
|
-
styleGuide: string | null;
|
|
91
|
-
}
|
|
92
|
-
/**
|
|
93
|
-
* The AI returns this shape for both Mode 1 and Mode 2: one entry per
|
|
94
|
-
* source module slot, with the translated content fields. The handler
|
|
95
|
-
* matches entries back to the variant page's module rows by
|
|
96
|
-
* (blockName, position).
|
|
97
|
-
*
|
|
98
|
-
* Mode 1 returns one entry per source module.
|
|
99
|
-
* Mode 2 returns entries ONLY for changed blocks (preserves prior
|
|
100
|
-
* translation on unchanged ones).
|
|
101
|
-
*/
|
|
102
|
-
export declare const translationResultModule: z.ZodObject<{
|
|
103
|
-
blockName: z.ZodString;
|
|
104
|
-
position: z.ZodNumber;
|
|
105
|
-
html: z.ZodString;
|
|
106
|
-
altText: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
107
|
-
caption: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
108
|
-
}, z.core.$strict>;
|
|
109
|
-
export declare const translationResultPayload: z.ZodObject<{
|
|
110
|
-
modules: z.ZodArray<z.ZodObject<{
|
|
111
|
-
blockName: z.ZodString;
|
|
112
|
-
position: z.ZodNumber;
|
|
113
|
-
html: z.ZodString;
|
|
114
|
-
altText: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
115
|
-
caption: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
116
|
-
}, z.core.$strict>>;
|
|
117
|
-
}, z.core.$strict>;
|
|
118
|
-
export type TranslationResultPayload = z.infer<typeof translationResultPayload>;
|
|
119
|
-
export declare function buildModeOnePrompt(input: ModeOnePromptInput): {
|
|
120
|
-
system: string;
|
|
121
|
-
user: string;
|
|
122
|
-
};
|
|
123
|
-
export declare function buildModeTwoPrompt(input: ModeTwoPromptInput): {
|
|
124
|
-
system: string;
|
|
125
|
-
user: string;
|
|
126
|
-
};
|
|
127
|
-
//# sourceMappingURL=translation.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"translation.d.ts","sourceRoot":"","sources":["../src/translation.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;GAaG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,MAAM,WAAW,eAAe;IAC9B,gEAAgE;IAChE,SAAS,EAAE,MAAM,CAAC;IAClB,+DAA+D;IAC/D,QAAQ,EAAE,MAAM,CAAC;IACjB,mEAAmE;IACnE,QAAQ,EAAE,MAAM,CAAC;IACjB,gDAAgD;IAChD,UAAU,EAAE,MAAM,CAAC;IACnB;;wDAEoD;IACpD,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;CACxB;AAED,MAAM,MAAM,WAAW,GACnB;IACE,IAAI,EAAE,SAAS,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,eAAe,CAAC;IACxB,KAAK,EAAE,eAAe,CAAC;CACxB,GACD;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,eAAe,CAAA;CAAE,GAC/E;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,eAAe,CAAA;CAAE,CAAC;AAEtF;;;;;;;;;;GAUG;AACH,wBAAgB,gBAAgB,CAC9B,MAAM,EAAE,SAAS,eAAe,EAAE,EAClC,OAAO,EAAE,SAAS,eAAe,EAAE,GAClC,WAAW,EAAE,CAgCf;AAMD,MAAM,WAAW,aAAa;IAC5B,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;CACxB;AAED,MAAM,WAAW,kBAAkB;IACjC,sCAAsC;IACtC,YAAY,EAAE,MAAM,CAAC;IACrB,yCAAyC;IACzC,YAAY,EAAE,MAAM,CAAC;IACrB,yEAAyE;IACzE,uBAAuB,CAAC,EAAE,MAAM,CAAC;IACjC,6EAA6E;IAC7E,aAAa,EAAE,SAAS,eAAe,EAAE,CAAC;IAC1C,QAAQ,EAAE,SAAS,aAAa,EAAE,CAAC;IACnC,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;CAC3B;AAED,MAAM,WAAW,kBAAkB;IACjC,YAAY,EAAE,MAAM,CAAC;IACrB,YAAY,EAAE,MAAM,CAAC;IACrB,uBAAuB,CAAC,EAAE,MAAM,CAAC;IACjC,aAAa,EAAE,SAAS,eAAe,EAAE,CAAC;IAC1C;kDAC8C;IAC9C,cAAc,EAAE,SAAS,eAAe,EAAE,CAAC;IAC3C,uDAAuD;IACvD,IAAI,EAAE,SAAS,WAAW,EAAE,CAAC;IAC7B,QAAQ,EAAE,SAAS,aAAa,EAAE,CAAC;IACnC,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;CAC3B;AAED;;;;;;;;;GASG;AACH,eAAO,MAAM,uBAAuB;;;;;;kBASzB,CAAC;AAEZ,eAAO,MAAM,wBAAwB;;;;;;;;kBAI1B,CAAC;AACZ,MAAM,MAAM,wBAAwB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,wBAAwB,CAAC,CAAC;AAiChF,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,kBAAkB,GAAG;IAC7D,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;CACd,CAuBA;AAED,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,kBAAkB,GAAG;IAC7D,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;CACd,CAuEA"}
|