@vectoriox/iox-ui 4.34.0 → 4.36.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/index.d.ts CHANGED
@@ -308,6 +308,12 @@ declare class BuilderIconComponent {
308
308
  /** Full Phosphor icon CSS class string, e.g. "ph-thin ph-star" */
309
309
  iconClass: string;
310
310
  nodeId: string;
311
+ /**
312
+ * Repaired at render time: a Phosphor class saved without its weight (`ph-house`) matches no CSS
313
+ * rule and renders as an invisible box. The picker used to emit exactly that, so this keeps
314
+ * every already-saved icon working. Shared with the SSR engine — see `normalizeIconClass`.
315
+ */
316
+ get normalizedIconClass(): string;
311
317
  static ɵfac: i0.ɵɵFactoryDeclaration<BuilderIconComponent, never>;
312
318
  static ɵcmp: i0.ɵɵComponentDeclaration<BuilderIconComponent, "i[iox-icon]", never, { "iconClass": { "alias": "iconClass"; "required": false; }; "nodeId": { "alias": "nodeId"; "required": false; }; }, {}, never, never, false, never>;
313
319
  }
@@ -1928,6 +1934,26 @@ declare function declaredInputNames(type: Type<unknown>): Set<string>;
1928
1934
  */
1929
1935
  declare function safeSetInput(ref: ComponentRef<unknown>, name: string, value: unknown): boolean;
1930
1936
 
1937
+ /**
1938
+ * Phosphor's CSS needs TWO classes — a weight and a name (`.ph-thin.ph-house:before`). A class
1939
+ * string carrying only the name matches nothing at all: no `font-family`, no glyph, so the element
1940
+ * renders as an invisible, zero-glyph box.
1941
+ *
1942
+ * The icon picker used to emit exactly that (`ph-house`), so every icon chosen through it was blank
1943
+ * on the canvas, in the picker grid and on the live site. Fixing the picker alone would leave every
1944
+ * icon already saved broken, so the repair happens at RENDER time, here, and is shared by the
1945
+ * builder's Icon component and the SSR engine's renderer.
1946
+ *
1947
+ * Everything that is not a weightless Phosphor class passes through untouched — Iconoir
1948
+ * (`iconoir-home`), custom icon-set fonts, Font Awesome, and Phosphor strings that already carry a
1949
+ * weight.
1950
+ *
1951
+ * See iox-ai-guidance `architecture/cms/custom-icon-fonts.md`.
1952
+ */
1953
+ /** The weight assumed when a Phosphor class has none — the weight this platform uses everywhere. */
1954
+ declare const DEFAULT_PHOSPHOR_WEIGHT = "ph-thin";
1955
+ declare function normalizeIconClass(iconClass: string | null | undefined): string;
1956
+
1931
1957
  /**
1932
1958
  * THE binding path resolver — one implementation, shared by every renderer.
1933
1959
  *
@@ -1941,6 +1967,115 @@ declare function safeSetInput(ref: ComponentRef<unknown>, name: string, value: u
1941
1967
  */
1942
1968
  declare function resolvePath(obj: any, path: string): any;
1943
1969
 
1970
+ /** The part of an `IoxBinding` that turns source data into a value. */
1971
+ interface BoundValueSpec {
1972
+ /** Dot/bracket path into the source data. Empty or absent means the source value itself. */
1973
+ path?: string;
1974
+ /** Render-time transforms (UI label "Filters"), applied after the path lookup. */
1975
+ pipes?: IoxPipe[];
1976
+ }
1977
+ /**
1978
+ * Resolve one binding's value out of its already-selected source data: path lookup, then pipes.
1979
+ *
1980
+ * Trivial on its own, which is exactly why it had been retyped at three call sites — the engine's
1981
+ * string renderer, the engine's component renderer and the builder canvas — plus a fourth needed by
1982
+ * page SEO. `resolvePath` itself was forked five times before it was shared, and the copy that
1983
+ * drifted caused a repeater to render in the SSR HTML and then blank on hydration.
1984
+ *
1985
+ * What is NOT here: picking the source (`resolvedData[binding.source]`) and deciding what an empty
1986
+ * context slot means. Those are per-renderer policy — the string renderer clears the prop, the
1987
+ * builder skips it — so they stay with the caller.
1988
+ */
1989
+ declare function resolveBoundValue(sourceData: unknown, binding: BoundValueSpec, ctx?: PipeContext): unknown;
1990
+
1991
+ /**
1992
+ * The canonical shape of a page's SEO metadata, plus the flat tag list both renderers emit.
1993
+ *
1994
+ * ⚠️ This file is the ONLY declaration of that shape. Before it existed, `iox-cms` stored
1995
+ * `seo.og.title` while the SSR engine read a flat `seo.ogTitle`, so no Open Graph tag had ever been
1996
+ * emitted, and `iox-cms-client` carried a third, hand-copied variant of the same schema. See
1997
+ * iox-ai-guidance `architecture/cms/seo-plan.md`.
1998
+ */
1999
+ /** Open Graph subset we author and emit. */
2000
+ interface SeoOg {
2001
+ title?: string;
2002
+ description?: string;
2003
+ image?: string;
2004
+ }
2005
+ /** Twitter-card subset. Emitted only when `card` is set — see `seoToTags`. */
2006
+ interface SeoTwitter {
2007
+ card?: string;
2008
+ title?: string;
2009
+ description?: string;
2010
+ image?: string;
2011
+ }
2012
+ /** Sitemap participation for this page. Consumed by the sitemap route (plan phase 3). */
2013
+ interface SeoSitemap {
2014
+ include?: boolean;
2015
+ priority?: number;
2016
+ changeFreq?: string;
2017
+ }
2018
+ /**
2019
+ * A page's stored SEO metadata, normalized.
2020
+ *
2021
+ * `keywords` has no authoring field — search engines have ignored the tag for over a decade — but
2022
+ * it is read and emitted when a document already carries one, because the engine used to read it.
2023
+ */
2024
+ interface SeoDoc {
2025
+ title?: string;
2026
+ description?: string;
2027
+ keywords?: string;
2028
+ canonicalUrl?: string;
2029
+ robots?: string;
2030
+ og?: SeoOg;
2031
+ twitter?: SeoTwitter;
2032
+ structuredData?: unknown;
2033
+ sitemap?: SeoSitemap;
2034
+ }
2035
+
2036
+ /**
2037
+ * Turn a page's STORED SEO document into the resolved one a crawler should see.
2038
+ *
2039
+ * Runs the ladder, per field: `override` → `binding` → auto. It lives in the main entry point rather
2040
+ * than `seo-core` because the binding rung needs `resolvePath` + `applyBindingPipes` from here, and
2041
+ * `iox-cms` (which imports `seo-core` under plain Node) never resolves anything — it only stores and
2042
+ * declares. `seoToTags` then turns this output into head tags.
2043
+ *
2044
+ * The reason this exists: `applyPageSeo(page.seo)` used to hand the stored strings straight to the
2045
+ * head, never consulting the resolved route item, so all 24 pages rendered from one template shipped
2046
+ * the same `<title>`. See iox-ai-guidance `architecture/cms/seo-plan.md`.
2047
+ */
2048
+ /** What the ladder's lower two rungs need. All optional — a static page supplies almost none of it. */
2049
+ interface SeoResolveContext {
2050
+ /** The page's resolved data sources, keyed by alias (`$current` on a template page). */
2051
+ resolvedData?: Record<string, unknown> | null;
2052
+ /** The page's layout tree, for the "derive from the content" rung. */
2053
+ tree?: readonly unknown[] | null;
2054
+ /** Title of last resort, when the page has no heading at all. */
2055
+ pageName?: string;
2056
+ /** Absolute URL of the page being rendered — the automatic canonical. */
2057
+ pageUrl?: string;
2058
+ }
2059
+ /** What the layout tree yields for the bottom rung. */
2060
+ interface SeoDerivedContent {
2061
+ title?: string;
2062
+ description?: string;
2063
+ image?: string;
2064
+ }
2065
+ declare function resolveSeo(seo: unknown, ctx?: SeoResolveContext): SeoDoc;
2066
+ /**
2067
+ * Derive a title, description and image from the page's own content, in document order.
2068
+ *
2069
+ * ⚠️ Node bindings are resolved here too. On a template page the `<h1>` is normally bound to
2070
+ * `$current.title`, so reading `props.content` alone would return the design-time placeholder — the
2071
+ * same trap that made context slots fall through to component defaults. An unresolved binding yields
2072
+ * nothing rather than the placeholder.
2073
+ *
2074
+ * Node shapes are the renderers' own: `Heading` has `level` + `content`, `Text` has `content` (rich
2075
+ * HTML from the editor) + `tag`, `Image` has `src`.
2076
+ */
2077
+ declare function deriveSeoFromContent(tree: readonly unknown[] | null | undefined, resolvedData?: Record<string, unknown> | null): SeoDerivedContent;
2078
+
1944
2079
  /**
1945
2080
  * Element context state — shared types.
1946
2081
  *
@@ -2441,5 +2576,5 @@ declare class GalleryLoop {
2441
2576
  private emit;
2442
2577
  }
2443
2578
 
2444
- export { AOSService, AnalyticsService, BREAKPOINTS, Breakpoint, BreakpointQueryKind, BuilderButtonComponent, BuilderDividerComponent, BuilderHeadingComponent, BuilderIconComponent, BuilderImageComponent, BuilderLinkComponent, BuilderSpacerComponent, ButtonBlockComponent, CMSClientInfraModule, CONTEXT_ACTION_TYPES, CardComponent, ClientGalleryComponent, ClientListComponent, ClientListItemComponent, ClientRepeaterComponent, ClientSliderContainerComponent, ClientSliderSlideComponent, ComponentInstanceRegistryService, ConsentService, ContainerComponent, ContentService, ContextStore, DEFAULT_GALLERY_SCHEDULER, DEFAULT_PAGE_TRANSITION, DEFAULT_PAGE_TRANSITION_DURATION, DEFAULT_PAGE_TRANSITION_EASING, DEFAULT_PAGE_TRANSITION_PERSPECTIVE, DEFAULT_WIDTH_BY_TYPE, DeclarationContext, ENVIRONMENT, FormRenderState, FormRuntimeService, GALLERY_EFFECTS, GALLERY_EFFECT_DESCRIPTORS, GalleryLoop, IOX_BUILDER_EVENTS, IOX_FORM_MESSAGES, IOX_FORM_NAVIGATOR, IOX_FORM_TRANSPORT, IS_PREVIEW, IoxAnimateContainer, IoxAosDirective, IoxAosModule, IoxBuilderComponentsModule, IoxComponentRegistryService, IoxFieldComponent, IoxFormComponent, IoxFormErrorComponent, IoxFormSuccessComponent, IoxPageComponent, IoxPageModule, IoxSubmitButtonComponent, IoxUiModule, LinkedContainerComponent, PAGE_TRANSITION_CATEGORIES, PAGE_TRANSITION_PRESETS, PIPE_DESCRIPTORS, PIPE_REGISTRY, PageScrollProvider, PrivacyModalComponent, PrivacyModalService, PrivacyModelEvent, RESERVED_ALIAS_PREFIX, SUPPRESSED_STYLE_PROPS_BY_TYPE, SWIPER_RESPONSIVE_TRAITS, SectionComponent, TRANSITION_FEELS, TextBlockComponent, VIRTUAL_TRAIT_GROUPS, VIRTUAL_TRAIT_KEYS, ViewPositionDirective, ViewPositionModule, WebSettingsService, activeActions, applyBindingPipes, applyFeel, breakpointDef, breakpointForWidth, breakpointQueryFor, buildDataSourceFilter, buildRelatedFilter, buildSwiperBreakpoints, buildTargetedStateSelector, buildTransitionTimeline, cascadeChain, clearInputOverride, clearOverride, collectContextSlotKeys, collectReferencedAliases, compileDeclarations, composeVirtualTraits, computeRelative, contextActionEffect, contextSlotKeys, countOverrides, dataSourcePlanToRequest, declaredInputNames, defaultMessageResolver, effectiveTransitionId, excludeSelf, feelFor, getByPath, initialSlotValues, isActionEnabled, isContextAction, isInputOverriddenAt, isOverriddenAt, isReservedAlias, isValidSlotKey, legacyEnterToTransition, matchRouteParams, mediaQueryFor, mergeSwiperBreakpoints, nearestCommonAncestorId, nodeCssId, nodeUsesContext, normalizeCollectionResult, normalizeGalleryImage, parentBreakpoint, parseUrlList, planDataSource, planResponsiveRules, referencedDataSources, referencedDataSourcesDeep, renderDefaultDeclarations, resolveContextValue, resolveDerivedInto, resolveGalleryEffect, resolveGalleryUrls, resolveInputChain, resolveInputs, resolvePageTransition, resolvePath, resolveRelativeInto, resolveRepeaterItems, resolveSiblings, resolveSingleItemId, resolveStateStyles, resolveStyleProps, resolveValueChain, rewriteViewportUnits, safeSetInput, setInputOverride, setOverride, stateNamesIn, stripSuppressedProps, styleKeyToKebab, variableAliases, variableItemsFrom, withContextSlots, wrapInBreakpointQuery, wrapInMediaQuery };
2445
- export type { BreakpointDef, BuildRelatedFilterOptions, ContextActionEffect, ContextActionLike, ContextItemRef, ContextSlotDefault, ContextValueSpec, ContextWriteScope, DataSourceDomain, DataSourceEndpoints, DataSourceMode, DataSourcePlan, DataSourceQueryFilter, DataSourceRequest, DataSourceRequestSpec, DataSourceRouteFilter, DataSourceSpec, FormMessageResolver, GalleryBoundSource, GalleryDurations, GalleryEffect, GalleryEffectDescriptor, GalleryImage, GalleryLoopOptions, GalleryLoopScheduler, GalleryLoopState, GallerySlideState, IBuilderEventEmitter, InputMap, IoxContextSlot, IoxPipe, OverrideBreakpoint, PageTransitionCategory, PageTransitionCategoryMeta, PageTransitionOptions, PageTransitionPreset, PipeArgSpec, PipeContext, PipeDescriptor, RelativeInputs, RelativeSourceKind, RelativeSourceSpec, ResolveSiblingsOptions, ResolvedPageTransition, ResponsiveLayer, ResponsiveOverrides, ResponsiveRuleSet, RouteContext, SetContextParams, SiblingsResult, StateStyleMap, StyleMap, StyleTreeNode, SwiperBreakpointMap, ToggleableAction, TransitionFeel, TransitionPhase, TransitionTimelineInput, ValueSource, VirtualTraitGroup };
2579
+ export { AOSService, AnalyticsService, BREAKPOINTS, Breakpoint, BreakpointQueryKind, BuilderButtonComponent, BuilderDividerComponent, BuilderHeadingComponent, BuilderIconComponent, BuilderImageComponent, BuilderLinkComponent, BuilderSpacerComponent, ButtonBlockComponent, CMSClientInfraModule, CONTEXT_ACTION_TYPES, CardComponent, ClientGalleryComponent, ClientListComponent, ClientListItemComponent, ClientRepeaterComponent, ClientSliderContainerComponent, ClientSliderSlideComponent, ComponentInstanceRegistryService, ConsentService, ContainerComponent, ContentService, ContextStore, DEFAULT_GALLERY_SCHEDULER, DEFAULT_PAGE_TRANSITION, DEFAULT_PAGE_TRANSITION_DURATION, DEFAULT_PAGE_TRANSITION_EASING, DEFAULT_PAGE_TRANSITION_PERSPECTIVE, DEFAULT_PHOSPHOR_WEIGHT, DEFAULT_WIDTH_BY_TYPE, DeclarationContext, ENVIRONMENT, FormRenderState, FormRuntimeService, GALLERY_EFFECTS, GALLERY_EFFECT_DESCRIPTORS, GalleryLoop, IOX_BUILDER_EVENTS, IOX_FORM_MESSAGES, IOX_FORM_NAVIGATOR, IOX_FORM_TRANSPORT, IS_PREVIEW, IoxAnimateContainer, IoxAosDirective, IoxAosModule, IoxBuilderComponentsModule, IoxComponentRegistryService, IoxFieldComponent, IoxFormComponent, IoxFormErrorComponent, IoxFormSuccessComponent, IoxPageComponent, IoxPageModule, IoxSubmitButtonComponent, IoxUiModule, LinkedContainerComponent, PAGE_TRANSITION_CATEGORIES, PAGE_TRANSITION_PRESETS, PIPE_DESCRIPTORS, PIPE_REGISTRY, PageScrollProvider, PrivacyModalComponent, PrivacyModalService, PrivacyModelEvent, RESERVED_ALIAS_PREFIX, SUPPRESSED_STYLE_PROPS_BY_TYPE, SWIPER_RESPONSIVE_TRAITS, SectionComponent, TRANSITION_FEELS, TextBlockComponent, VIRTUAL_TRAIT_GROUPS, VIRTUAL_TRAIT_KEYS, ViewPositionDirective, ViewPositionModule, WebSettingsService, activeActions, applyBindingPipes, applyFeel, breakpointDef, breakpointForWidth, breakpointQueryFor, buildDataSourceFilter, buildRelatedFilter, buildSwiperBreakpoints, buildTargetedStateSelector, buildTransitionTimeline, cascadeChain, clearInputOverride, clearOverride, collectContextSlotKeys, collectReferencedAliases, compileDeclarations, composeVirtualTraits, computeRelative, contextActionEffect, contextSlotKeys, countOverrides, dataSourcePlanToRequest, declaredInputNames, defaultMessageResolver, deriveSeoFromContent, effectiveTransitionId, excludeSelf, feelFor, getByPath, initialSlotValues, isActionEnabled, isContextAction, isInputOverriddenAt, isOverriddenAt, isReservedAlias, isValidSlotKey, legacyEnterToTransition, matchRouteParams, mediaQueryFor, mergeSwiperBreakpoints, nearestCommonAncestorId, nodeCssId, nodeUsesContext, normalizeCollectionResult, normalizeGalleryImage, normalizeIconClass, parentBreakpoint, parseUrlList, planDataSource, planResponsiveRules, referencedDataSources, referencedDataSourcesDeep, renderDefaultDeclarations, resolveBoundValue, resolveContextValue, resolveDerivedInto, resolveGalleryEffect, resolveGalleryUrls, resolveInputChain, resolveInputs, resolvePageTransition, resolvePath, resolveRelativeInto, resolveRepeaterItems, resolveSeo, resolveSiblings, resolveSingleItemId, resolveStateStyles, resolveStyleProps, resolveValueChain, rewriteViewportUnits, safeSetInput, setInputOverride, setOverride, stateNamesIn, stripSuppressedProps, styleKeyToKebab, variableAliases, variableItemsFrom, withContextSlots, wrapInBreakpointQuery, wrapInMediaQuery };
2580
+ export type { BoundValueSpec, BreakpointDef, BuildRelatedFilterOptions, ContextActionEffect, ContextActionLike, ContextItemRef, ContextSlotDefault, ContextValueSpec, ContextWriteScope, DataSourceDomain, DataSourceEndpoints, DataSourceMode, DataSourcePlan, DataSourceQueryFilter, DataSourceRequest, DataSourceRequestSpec, DataSourceRouteFilter, DataSourceSpec, FormMessageResolver, GalleryBoundSource, GalleryDurations, GalleryEffect, GalleryEffectDescriptor, GalleryImage, GalleryLoopOptions, GalleryLoopScheduler, GalleryLoopState, GallerySlideState, IBuilderEventEmitter, InputMap, IoxContextSlot, IoxPipe, OverrideBreakpoint, PageTransitionCategory, PageTransitionCategoryMeta, PageTransitionOptions, PageTransitionPreset, PipeArgSpec, PipeContext, PipeDescriptor, RelativeInputs, RelativeSourceKind, RelativeSourceSpec, ResolveSiblingsOptions, ResolvedPageTransition, ResponsiveLayer, ResponsiveOverrides, ResponsiveRuleSet, RouteContext, SeoDerivedContent, SeoResolveContext, SetContextParams, SiblingsResult, StateStyleMap, StyleMap, StyleTreeNode, SwiperBreakpointMap, ToggleableAction, TransitionFeel, TransitionPhase, TransitionTimelineInput, ValueSource, VirtualTraitGroup };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vectoriox/iox-ui",
3
- "version": "4.34.0",
3
+ "version": "4.36.0",
4
4
  "peerDependencies": {
5
5
  "@angular/common": ">=20.0.0",
6
6
  "@angular/core": ">=20.0.0",
@@ -42,6 +42,52 @@ interface SeoDoc {
42
42
  structuredData?: unknown;
43
43
  sitemap?: SeoSitemap;
44
44
  }
45
+ /**
46
+ * A binding on an SEO field. Structurally the resolvable part of an `IoxBinding` — `source` picks an
47
+ * entry in the page's resolved data (`$current` on a template page), `path` walks into it, `pipes`
48
+ * are the render-time transforms. Typed loosely because the pipe catalog lives in the main
49
+ * `@vectoriox/iox-ui` entry point, which this framework-free one must not import.
50
+ */
51
+ interface SeoBinding {
52
+ source: string;
53
+ path?: string;
54
+ pipes?: readonly unknown[];
55
+ }
56
+ /**
57
+ * One SEO field as stored once it carries more than a typed string.
58
+ *
59
+ * The ladder is `override` → `binding` → auto; `auto: false` opts that field out of the automatic
60
+ * value, leaving it genuinely empty. A bare `string` in a stored document means `{ override }`, so
61
+ * nothing has to be migrated.
62
+ */
63
+ interface SeoFieldValue {
64
+ override?: string;
65
+ binding?: SeoBinding;
66
+ auto?: false;
67
+ }
68
+ /** A stored SEO document, before resolution: any text field may be a plain string or a value object. */
69
+ type SeoRawDoc = Record<string, unknown>;
70
+ /**
71
+ * The text fields that can carry a binding, as dotted paths into the document.
72
+ *
73
+ * One list, used by the resolver and (phase 2) by the panel that draws the bind affordance — so a
74
+ * field can never be bindable in the UI and ignored at render time. `robots`, `sitemap.*` and
75
+ * `structuredData` are deliberately absent: they are pick-lists and blobs, not prose.
76
+ */
77
+ declare const SEO_BINDABLE_FIELDS: readonly string[];
78
+ /** Where an automatic value comes from when a field has neither an override nor a binding. */
79
+ type SeoAutoSource = 'contentTitle' | 'contentDescription' | 'contentImage' | 'pageUrl';
80
+ /**
81
+ * The bottom rung of the ladder, as a table rather than a chain of ifs.
82
+ *
83
+ * Deliberately short. `og:title` and `twitter:*` are NOT here — `seoToTags` already falls back to
84
+ * the page title for them, and materialising a copy of a value we did not author would only make
85
+ * later drift harder to see.
86
+ */
87
+ declare const SEO_AUTO_SOURCES: ReadonlyArray<{
88
+ field: string;
89
+ source: SeoAutoSource;
90
+ }>;
45
91
  /** `<title>`. */
46
92
  interface SeoTitleTag {
47
93
  kind: 'title';
@@ -101,6 +147,14 @@ declare const SEO_CHANGE_FREQ_OPTIONS: ReadonlyArray<{
101
147
  * Empty branches are dropped rather than left as `{}`, so `if (seo.og)` is a meaningful test.
102
148
  */
103
149
  declare function normalizeSeoDoc(raw: unknown): SeoDoc;
150
+ /**
151
+ * A field's value as a trimmed string, or `undefined`. Exported because the resolver needs the exact
152
+ * same coercion for a value it has just pulled out of a data source.
153
+ *
154
+ * Unwraps the value object: a bound or auto field has no `override`, so it correctly resolves to
155
+ * `undefined` here and `resolveSeo` fills it in from the lower rungs.
156
+ */
157
+ declare function seoString(value: unknown): string | undefined;
104
158
 
105
159
  /**
106
160
  * Turn a page's SEO document into the flat list of head tags to emit.
@@ -131,5 +185,5 @@ declare function seoToTags(seo: SeoDoc | unknown, ctx?: SeoTagContext): SeoTag[]
131
185
  */
132
186
  declare function buildSeoSchemaProperties(): Record<string, unknown>;
133
187
 
134
- export { SEO_CHANGE_FREQ_OPTIONS, SEO_DESCRIPTION_MAX, SEO_ROBOTS_OPTIONS, SEO_TITLE_MAX, SEO_TWITTER_CARD_OPTIONS, buildSeoSchemaProperties, normalizeSeoDoc, seoToTags };
135
- export type { SeoDoc, SeoLinkTag, SeoMetaTag, SeoOg, SeoSitemap, SeoTag, SeoTagContext, SeoTitleTag, SeoTwitter };
188
+ export { SEO_AUTO_SOURCES, SEO_BINDABLE_FIELDS, SEO_CHANGE_FREQ_OPTIONS, SEO_DESCRIPTION_MAX, SEO_ROBOTS_OPTIONS, SEO_TITLE_MAX, SEO_TWITTER_CARD_OPTIONS, buildSeoSchemaProperties, normalizeSeoDoc, seoString, seoToTags };
189
+ export type { SeoAutoSource, SeoBinding, SeoDoc, SeoFieldValue, SeoLinkTag, SeoMetaTag, SeoOg, SeoRawDoc, SeoSitemap, SeoTag, SeoTagContext, SeoTitleTag, SeoTwitter };
@@ -18,32 +18,32 @@ export function normalizeSeoDoc(raw: unknown): SeoDoc {
18
18
  if (!src) return {};
19
19
 
20
20
  const og = compact<SeoOg>({
21
- title: str(pick(src, 'og', 'title') ?? src['ogTitle']),
22
- description: str(pick(src, 'og', 'description') ?? src['ogDescription']),
23
- image: str(pick(src, 'og', 'image') ?? src['ogImage']),
21
+ title: seoString(pick(src, 'og', 'title') ?? src['ogTitle']),
22
+ description: seoString(pick(src, 'og', 'description') ?? src['ogDescription']),
23
+ image: seoString(pick(src, 'og', 'image') ?? src['ogImage']),
24
24
  });
25
25
 
26
26
  const twitter = compact<SeoTwitter>({
27
- card: str(pick(src, 'twitter', 'card')),
28
- title: str(pick(src, 'twitter', 'title')),
29
- description: str(pick(src, 'twitter', 'description')),
30
- image: str(pick(src, 'twitter', 'image')),
27
+ card: seoString(pick(src, 'twitter', 'card')),
28
+ title: seoString(pick(src, 'twitter', 'title')),
29
+ description: seoString(pick(src, 'twitter', 'description')),
30
+ image: seoString(pick(src, 'twitter', 'image')),
31
31
  });
32
32
 
33
33
  const sitemap = compact<SeoSitemap>({
34
34
  include: bool(pick(src, 'sitemap', 'include')),
35
35
  priority: num(pick(src, 'sitemap', 'priority')),
36
- changeFreq: str(pick(src, 'sitemap', 'changeFreq')),
36
+ changeFreq: seoString(pick(src, 'sitemap', 'changeFreq')),
37
37
  });
38
38
 
39
39
  const structuredData = structured(src['structuredData']);
40
40
 
41
41
  const doc = compact<SeoDoc>({
42
- title: str(src['title']),
43
- description: str(src['description']),
44
- keywords: str(src['keywords']),
45
- canonicalUrl: str(src['canonicalUrl']),
46
- robots: str(src['robots']),
42
+ title: seoString(src['title']),
43
+ description: seoString(src['description']),
44
+ keywords: seoString(src['keywords']),
45
+ canonicalUrl: seoString(src['canonicalUrl']),
46
+ robots: seoString(src['robots']),
47
47
  og,
48
48
  twitter,
49
49
  sitemap,
@@ -60,12 +60,13 @@ function pick(src: Record<string, unknown>, branch: string, leaf: string): unkno
60
60
  }
61
61
 
62
62
  /**
63
- * A field's value as a trimmed string, or `undefined`.
63
+ * A field's value as a trimmed string, or `undefined`. Exported because the resolver needs the exact
64
+ * same coercion for a value it has just pulled out of a data source.
64
65
  *
65
- * Unwraps the phase-2 value object: a bound or auto field has no `override`, so it correctly
66
- * resolves to `undefined` here and the binding layer fills it in later.
66
+ * Unwraps the value object: a bound or auto field has no `override`, so it correctly resolves to
67
+ * `undefined` here and `resolveSeo` fills it in from the lower rungs.
67
68
  */
68
- function str(value: unknown): string | undefined {
69
+ export function seoString(value: unknown): string | undefined {
69
70
  const wrapper = asRecord(value);
70
71
  const raw = wrapper ? wrapper['override'] : value;
71
72
  if (typeof raw === 'number') return String(raw);
@@ -47,6 +47,70 @@ export interface SeoDoc {
47
47
  sitemap?: SeoSitemap;
48
48
  }
49
49
 
50
+ /**
51
+ * A binding on an SEO field. Structurally the resolvable part of an `IoxBinding` — `source` picks an
52
+ * entry in the page's resolved data (`$current` on a template page), `path` walks into it, `pipes`
53
+ * are the render-time transforms. Typed loosely because the pipe catalog lives in the main
54
+ * `@vectoriox/iox-ui` entry point, which this framework-free one must not import.
55
+ */
56
+ export interface SeoBinding {
57
+ source: string;
58
+ path?: string;
59
+ pipes?: readonly unknown[];
60
+ }
61
+
62
+ /**
63
+ * One SEO field as stored once it carries more than a typed string.
64
+ *
65
+ * The ladder is `override` → `binding` → auto; `auto: false` opts that field out of the automatic
66
+ * value, leaving it genuinely empty. A bare `string` in a stored document means `{ override }`, so
67
+ * nothing has to be migrated.
68
+ */
69
+ export interface SeoFieldValue {
70
+ override?: string;
71
+ binding?: SeoBinding;
72
+ auto?: false;
73
+ }
74
+
75
+ /** A stored SEO document, before resolution: any text field may be a plain string or a value object. */
76
+ export type SeoRawDoc = Record<string, unknown>;
77
+
78
+ /**
79
+ * The text fields that can carry a binding, as dotted paths into the document.
80
+ *
81
+ * One list, used by the resolver and (phase 2) by the panel that draws the bind affordance — so a
82
+ * field can never be bindable in the UI and ignored at render time. `robots`, `sitemap.*` and
83
+ * `structuredData` are deliberately absent: they are pick-lists and blobs, not prose.
84
+ */
85
+ export const SEO_BINDABLE_FIELDS: readonly string[] = [
86
+ 'title',
87
+ 'description',
88
+ 'canonicalUrl',
89
+ 'og.title',
90
+ 'og.description',
91
+ 'og.image',
92
+ 'twitter.title',
93
+ 'twitter.description',
94
+ 'twitter.image',
95
+ ];
96
+
97
+ /** Where an automatic value comes from when a field has neither an override nor a binding. */
98
+ export type SeoAutoSource = 'contentTitle' | 'contentDescription' | 'contentImage' | 'pageUrl';
99
+
100
+ /**
101
+ * The bottom rung of the ladder, as a table rather than a chain of ifs.
102
+ *
103
+ * Deliberately short. `og:title` and `twitter:*` are NOT here — `seoToTags` already falls back to
104
+ * the page title for them, and materialising a copy of a value we did not author would only make
105
+ * later drift harder to see.
106
+ */
107
+ export const SEO_AUTO_SOURCES: ReadonlyArray<{ field: string; source: SeoAutoSource }> = [
108
+ { field: 'title', source: 'contentTitle' },
109
+ { field: 'description', source: 'contentDescription' },
110
+ { field: 'og.image', source: 'contentImage' },
111
+ { field: 'canonicalUrl', source: 'pageUrl' },
112
+ ];
113
+
50
114
  /** `<title>`. */
51
115
  export interface SeoTitleTag {
52
116
  kind: 'title';