@salesforce/afv-skills 1.45.0 → 1.47.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.
Files changed (171) hide show
  1. package/package.json +1 -1
  2. package/skills/agentforce-observe/SKILL.md +32 -4
  3. package/skills/agentforce-observe/references/ahm-alerts.md +719 -0
  4. package/skills/automation-flow-generate/SKILL.md +11 -5
  5. package/skills/consumer-goods-promotion-bo-api-deploy/SKILL.md +275 -0
  6. package/skills/consumer-goods-promotion-bo-api-deploy/assets/set-comment-value/README.md +32 -0
  7. package/skills/consumer-goods-promotion-bo-api-deploy/assets/set-comment-value/SetCommentValue.cls +75 -0
  8. package/skills/consumer-goods-promotion-bo-api-deploy/assets/set-comment-value/SetCommentValue.cls-meta.xml +5 -0
  9. package/skills/consumer-goods-promotion-bo-api-deploy/assets/set-comment-value/interview-answers.json +13 -0
  10. package/skills/consumer-goods-promotion-bo-api-deploy/assets/set-comment-value/payloads/copy.json +10 -0
  11. package/skills/consumer-goods-promotion-bo-api-deploy/assets/set-comment-value/payloads/create.json +20 -0
  12. package/skills/consumer-goods-promotion-bo-api-deploy/assets/set-comment-value/payloads/update.json +16 -0
  13. package/skills/consumer-goods-promotion-bo-api-deploy/references/conventions-and-payload-rules.md +273 -0
  14. package/skills/consumer-goods-promotion-bo-api-deploy/references/generate-and-wire.md +236 -0
  15. package/skills/consumer-goods-promotion-bo-api-deploy/references/reference-example-set-comment-value.md +132 -0
  16. package/skills/consumer-goods-promotion-bo-api-deploy/references/smoke-and-verify.md +211 -0
  17. package/skills/dx-code-analyzer-configure/scripts/validate-config.sh +14 -10
  18. package/skills/dx-code-analyzer-run/scripts/apply-fixes.js +45 -4
  19. package/skills/dx-code-analyzer-run/scripts/describe-rule.js +52 -32
  20. package/skills/dx-devops-project-manage/SKILL.md +197 -0
  21. package/skills/dx-devops-project-manage/examples/common-workflows.md +197 -0
  22. package/skills/dx-devops-project-manage/references/cli-commands.md +295 -0
  23. package/skills/dx-devops-project-manage/scripts/create-project.sh +48 -0
  24. package/skills/dx-devops-project-manage/scripts/list-projects.sh +51 -0
  25. package/skills/dx-devops-project-manage/scripts/update-project.sh +96 -0
  26. package/skills/education-cloud-academic-calendar-generate/SKILL.md +225 -0
  27. package/skills/education-cloud-academic-calendar-generate/examples/quarter-calendar.json +47 -0
  28. package/skills/education-cloud-academic-calendar-generate/examples/sample-output.md +57 -0
  29. package/skills/education-cloud-academic-calendar-generate/examples/semester-calendar.json +54 -0
  30. package/skills/education-cloud-academic-calendar-generate/references/calendar-systems.md +127 -0
  31. package/skills/education-cloud-academic-calendar-generate/references/date-validation.md +222 -0
  32. package/skills/education-cloud-academic-calendar-generate/references/foundation_prerequisites.md +40 -0
  33. package/skills/education-cloud-academic-calendar-generate/scripts/validate_calendar_dates.py +143 -0
  34. package/skills/education-cloud-course-catalog-migrate/SKILL.md +321 -0
  35. package/skills/education-cloud-course-catalog-migrate/references/gotchas-detail.md +16 -0
  36. package/skills/education-cloud-course-catalog-migrate/references/gotchas.md +16 -0
  37. package/skills/education-cloud-course-catalog-migrate/references/large-catalog-handling.md +42 -0
  38. package/skills/education-cloud-course-catalog-migrate/scripts/batch_courses.py +36 -0
  39. package/skills/education-cloud-course-catalog-migrate/scripts/detect_linked_courses.py +51 -0
  40. package/skills/education-cloud-course-catalog-migrate/scripts/detect_modality_variants.py +48 -0
  41. package/skills/education-cloud-course-catalog-migrate/scripts/resolve_api_version.py +43 -0
  42. package/skills/education-cloud-course-catalog-migrate/scripts/split_course_code.py +39 -0
  43. package/skills/education-cloud-course-catalog-migrate/scripts/validate_completeness.py +54 -0
  44. package/skills/education-cloud-multi-campus-configure/references/foundation_prerequisites.md +3 -5
  45. package/skills/education-cloud-student-recruitment-agent-configure/SKILL.md +177 -0
  46. package/skills/education-cloud-student-recruitment-agent-configure/references/agent-and-subagents.md +151 -0
  47. package/skills/education-cloud-student-recruitment-agent-configure/references/customer-narration.md +34 -0
  48. package/skills/education-cloud-student-recruitment-agent-configure/references/execution-model.md +54 -0
  49. package/skills/education-cloud-student-recruitment-agent-configure/references/flows.md +82 -0
  50. package/skills/education-cloud-student-recruitment-agent-configure/references/grounding.md +199 -0
  51. package/skills/education-cloud-student-recruitment-agent-configure/references/permissions.md +183 -0
  52. package/skills/education-cloud-student-recruitment-agent-configure/references/platform-enablement.md +82 -0
  53. package/skills/education-cloud-student-recruitment-agent-configure/references/prerequisites.md +158 -0
  54. package/skills/education-cloud-student-recruitment-agent-configure/references/routing.md +141 -0
  55. package/skills/experience-cms-brand-apply/SKILL.md +5 -5
  56. package/skills/experience-cms-brand-create/SKILL.md +2 -2
  57. package/skills/experience-cms-content-generate/SKILL.md +1 -0
  58. package/skills/experience-cms-content-render/SKILL.md +173 -0
  59. package/skills/experience-cms-content-render/assets/angular/DetailPage.component.ts +25 -0
  60. package/skills/experience-cms-content-render/assets/angular/MediaRenderer.component.ts +133 -0
  61. package/skills/experience-cms-content-render/assets/angular/TypeList.component.ts +38 -0
  62. package/skills/experience-cms-content-render/assets/angular/TypeRenderer.component.ts +90 -0
  63. package/skills/experience-cms-content-render/assets/angular/cms-content.component.ts +248 -0
  64. package/skills/experience-cms-content-render/assets/angular/cms-item.service.ts +100 -0
  65. package/skills/experience-cms-content-render/assets/react/DetailPage.tsx +20 -0
  66. package/skills/experience-cms-content-render/assets/react/MediaRenderer.tsx +129 -0
  67. package/skills/experience-cms-content-render/assets/react/TypeList.tsx +40 -0
  68. package/skills/experience-cms-content-render/assets/react/TypeRenderer.tsx +64 -0
  69. package/skills/experience-cms-content-render/assets/react/heuristicRenderer.tsx +310 -0
  70. package/skills/experience-cms-content-render/assets/react/useCmsItem.ts +129 -0
  71. package/skills/experience-cms-content-render/assets/shared/cmsContentType.ts +49 -0
  72. package/skills/experience-cms-content-render/assets/shared/cmsCore.types.ts +96 -0
  73. package/skills/experience-cms-content-render/assets/shared/externalRefs.ts +55 -0
  74. package/skills/experience-cms-content-render/references/bulk-loading.md +60 -0
  75. package/skills/experience-cms-content-render/references/codegen-guardrails.md +111 -0
  76. package/skills/experience-cms-content-render/references/detail-pages.md +87 -0
  77. package/skills/experience-cms-content-render/references/embed-recipes.md +127 -0
  78. package/skills/experience-cms-content-render/references/failure-modes.md +96 -0
  79. package/skills/experience-cms-content-render/references/heuristic-render-rules.md +131 -0
  80. package/skills/experience-cms-content-render/references/init-scaffold.md +122 -0
  81. package/skills/experience-cms-content-render/references/interaction-model.md +173 -0
  82. package/skills/experience-cms-content-render/references/package-api.md +106 -0
  83. package/skills/experience-cms-content-render/references/schema-sync.md +114 -0
  84. package/skills/experience-cms-content-render/references/styling-scopes.md +65 -0
  85. package/skills/experience-cms-content-render/references/verify.md +49 -0
  86. package/skills/experience-cms-content-type-generate/SKILL.md +2 -2
  87. package/skills/experience-content-media-stock-image-search/SKILL.md +5 -4
  88. package/skills/experience-search-coordinate/SKILL.md +198 -0
  89. package/skills/experience-search-coordinate/assets/search-payload-template.json +25 -0
  90. package/skills/experience-search-coordinate/references/content-route.md +313 -0
  91. package/skills/experience-search-coordinate/references/content-type-discovery.md +57 -0
  92. package/skills/experience-search-coordinate/references/media-route.md +172 -0
  93. package/skills/experience-search-coordinate/references/scope-resolution.md +14 -0
  94. package/skills/experience-ui-bundle-localize/SKILL.md +1 -1
  95. package/skills/experience-ui-bundle-localize/references/i18n-setup.md +5 -3
  96. package/skills/experience-ui-bundle-project-generate/SKILL.md +18 -14
  97. package/skills/experience-ui-bundle-project-generate/references/angular-project-generate.md +22 -0
  98. package/skills/experience-ui-bundle-project-generate/references/react-project-generate.md +20 -0
  99. package/skills/experience-ui-bundle-salesforce-data-access/SKILL.md +58 -54
  100. package/skills/experience-ui-bundle-salesforce-data-access/references/caching.md +6 -0
  101. package/skills/experience-ui-bundle-salesforce-data-access/references/graphiti-cli.md +2 -2
  102. package/skills/experience-ui-bundle-salesforce-data-access/references/migration.md +6 -0
  103. package/skills/experience-ui-bundle-salesforce-data-access/references/rest-and-integration.md +2 -1
  104. package/skills/experience-ui-bundle-salesforce-data-access/references/sdk-api.md +6 -0
  105. package/skills/experience-ui-bundle-site-generate/SKILL.md +59 -8
  106. package/skills/experience-ui-bundle-site-generate/references/configure-metadata-digital-experience.md +8 -3
  107. package/skills/experience-ui-bundle-site-generate/references/configure-metadata-language-settings.md +120 -0
  108. package/skills/life-sciences-fieldsalesrep-coordinate/SKILL.md +336 -0
  109. package/skills/life-sciences-fieldsalesrep-coordinate/references/orchestration-flow.md +143 -0
  110. package/skills/life-sciences-fieldsalesrep-coordinate/references/stage-2-starter-config-application-flexipage-mapping.md +127 -0
  111. package/skills/life-sciences-fieldsalesrep-coordinate/references/stage-2-starter-config-deploy-commands.md +116 -0
  112. package/skills/life-sciences-fieldsalesrep-coordinate/references/stage-2-starter-config-lifesci-metadata-deploy.md +111 -0
  113. package/skills/life-sciences-fieldsalesrep-coordinate/references/stage-2-starter-config-overview.md +312 -0
  114. package/skills/life-sciences-fieldsalesrep-coordinate/references/stage-2-starter-config-profile-layout-assignments.md +171 -0
  115. package/skills/life-sciences-fieldsalesrep-coordinate/references/stage-2-starter-config-state-tracking.md +64 -0
  116. package/skills/life-sciences-fieldsalesrep-coordinate/references/stage-2-starter-config-trigger-handlers.md +122 -0
  117. package/skills/life-sciences-fieldsalesrep-coordinate/references/stage-4-user-provisioning-overview.md +335 -0
  118. package/skills/life-sciences-fieldsalesrep-coordinate/references/stage-4-user-provisioning-user-provisioning-details.md +140 -0
  119. package/skills/life-sciences-fieldsalesrep-coordinate/references/stage-5-visit-creation-execution-state-and-recovery.md +196 -0
  120. package/skills/life-sciences-fieldsalesrep-coordinate/references/stage-5-visit-creation-metadata-cache-generation.md +155 -0
  121. package/skills/life-sciences-fieldsalesrep-coordinate/references/stage-5-visit-creation-overview.md +307 -0
  122. package/skills/life-sciences-fieldsalesrep-coordinate/references/stage-5-visit-creation-visit-creation-data.md +211 -0
  123. package/skills/life-sciences-fieldsalesrep-coordinate/references/state-machine-and-changes.md +108 -0
  124. package/skills/life-sciences-kam-coordinate/SKILL.md +241 -0
  125. package/skills/life-sciences-kam-coordinate/references/orchestration-flow.md +152 -0
  126. package/skills/life-sciences-kam-coordinate/references/stage-2-starter-config-application-flexipage-mapping.md +79 -0
  127. package/skills/life-sciences-kam-coordinate/references/stage-2-starter-config-deploy-commands.md +131 -0
  128. package/skills/life-sciences-kam-coordinate/references/stage-2-starter-config-kam-config-records.md +85 -0
  129. package/skills/life-sciences-kam-coordinate/references/stage-2-starter-config-lifesci-metadata-deploy.md +112 -0
  130. package/skills/life-sciences-kam-coordinate/references/stage-2-starter-config-overview.md +202 -0
  131. package/skills/life-sciences-kam-coordinate/references/stage-2-starter-config-profile-layout-assignments.md +67 -0
  132. package/skills/life-sciences-kam-coordinate/references/stage-2-starter-config-state-tracking.md +65 -0
  133. package/skills/life-sciences-kam-coordinate/references/stage-2-starter-config-trigger-handlers.md +123 -0
  134. package/skills/life-sciences-kam-coordinate/references/stage-4-participant-role-and-sprint.md +89 -0
  135. package/skills/life-sciences-kam-coordinate/references/stage-5-data-and-plan-templates-overview.md +337 -0
  136. package/skills/life-sciences-kam-coordinate/references/stage-5-data-creation-data.md +248 -0
  137. package/skills/life-sciences-kam-coordinate/references/stage-6-ipad-validation-script.md +35 -0
  138. package/skills/life-sciences-kam-coordinate/references/stage-6-metadata-cache-generation.md +155 -0
  139. package/skills/life-sciences-kam-coordinate/references/stage-6-user-provisioning-details.md +146 -0
  140. package/skills/life-sciences-kam-coordinate/references/stage-6-user-provisioning-overview.md +89 -0
  141. package/skills/life-sciences-kam-coordinate/references/state-machine-and-changes.md +114 -0
  142. package/skills/life-sciences-prerequisites-validate/SKILL.md +138 -0
  143. package/skills/life-sciences-prerequisites-validate/references/checks-org-settings.md +190 -0
  144. package/skills/life-sciences-prerequisites-validate/references/checks-user-and-package.md +211 -0
  145. package/skills/life-sciences-territory-configure/SKILL.md +217 -0
  146. package/skills/life-sciences-territory-configure/references/territory-metadata.md +262 -0
  147. package/skills/platform-apex-logs-debug/SKILL.md +7 -7
  148. package/skills/platform-custom-application-generate/SKILL.md +4 -4
  149. package/skills/platform-custom-object-generate/SKILL.md +7 -7
  150. package/skills/platform-custom-tab-generate/SKILL.md +1 -1
  151. package/skills/platform-dsar-policy-manage/SKILL.md +272 -0
  152. package/skills/platform-dsar-policy-manage/references/configure.md +106 -0
  153. package/skills/platform-dsar-policy-manage/references/export-and-history.md +123 -0
  154. package/skills/platform-dsar-policy-manage/references/gap-analysis-guide.md +150 -0
  155. package/skills/platform-dsar-policy-manage/references/gap-scan.md +129 -0
  156. package/skills/platform-dsar-policy-manage/references/headless-sor.md +59 -0
  157. package/skills/platform-dsar-policy-manage/references/report-format.md +59 -0
  158. package/skills/platform-dsar-policy-manage/scripts/tests/__init__.py +0 -0
  159. package/skills/platform-dsar-policy-manage/scripts/tests/test_validate_policy_tree.py +76 -0
  160. package/skills/platform-dsar-policy-manage/scripts/validate-policy-tree.py +130 -0
  161. package/skills/platform-flexipage-generate/SKILL.md +4 -0
  162. package/skills/platform-list-view-generate/SKILL.md +1 -0
  163. package/skills/platform-salesforce-connect-adapter-generate/SKILL.md +359 -0
  164. package/skills/platform-salesforce-connect-adapter-generate/references/official-examples.md +69 -0
  165. package/skills/platform-salesforce-connect-adapter-generate/references/scenarios.md +187 -0
  166. package/skills/platform-soql-query/SKILL.md +8 -8
  167. package/skills/platform-value-set-generate/SKILL.md +2 -2
  168. package/skills/service-itsm-agentic-setup-cmdb-coordinate/SKILL.md +20 -27
  169. package/skills/service-native-voice-recording-transcription-configure/SKILL.md +47 -27
  170. package/skills/service-native-voice-recording-transcription-configure/references/thunderbird-voice-settings.md +13 -9
  171. package/skills/service-native-voice-recording-transcription-configure/scripts/enable-recording-transcription.sh +104 -45
@@ -0,0 +1,60 @@
1
+ # Bulk content loading — a direct `getCmsContentByKeys` call
2
+
3
+ To read a KNOWN set of `contentKey`s in ONE authenticated Connect round-trip, call
4
+ the toolkit's `getCmsContentByKeys` directly. There is no batch-loader seam to
5
+ scaffold and nothing to register — the per-item hook/service (`useCmsItem` /
6
+ `CmsItemService`) is unchanged. Rendering N `<Type>Renderer`/`cms-<type>` cards
7
+ still works (each reads individually); reach for bulk only when the extra round-
8
+ trips matter and you already hold the keys.
9
+
10
+ ## When to use it
11
+
12
+ On an explicit prompt to batch a list — e.g. *"bulk/batch-load the list"*, *"load
13
+ these in one request"*, *"optimize the list fetching"*, AND the caller already
14
+ knows the `contentKey`s (a curated set). Otherwise render per-item.
15
+
16
+ ## The call
17
+
18
+ ```ts
19
+ import { getCmsContentByKeys } from '@salesforce/ui-bundle-template-feature-cms-toolkit';
20
+ import type { CmsBulkResult } from '@salesforce/ui-bundle-template-feature-cms-toolkit';
21
+
22
+ // Best-effort: never throws (except on abort). Each requested key comes back as a
23
+ // CmsBulkResult with EITHER `body` OR `error`, echoing the requested contentKey.
24
+ const results: CmsBulkResult<NewsBody>[] = await getCmsContentByKeys<NewsBody>(
25
+ ['M3ASD4KHASDB73', 'MCABCDEF123456'],
26
+ { channelId }, // optional; omit to let the catalog supply it
27
+ );
28
+
29
+ for (const r of results) {
30
+ if (r.error) {
31
+ // unpublished / wrong-channel / absent → CmsNotFoundError (per-key isolation)
32
+ console.warn(r.contentKey, r.error.message);
33
+ } else {
34
+ render(r.body); // already unwrapped, envelope title lifted in
35
+ }
36
+ }
37
+ ```
38
+
39
+ - **Best-effort:** only an `AbortError` (via `options.signal`) rejects; every other
40
+ per-key failure comes back as `r.error` (a `CmsNotFoundError`).
41
+ - **Match BY `contentKey`, never positionally** — each result echoes the requested
42
+ key even though the toolkit remaps source→target internally.
43
+ - Keys are catalog-remapped (fail-open) and the channel resolves exactly as the
44
+ single-key read — `references/package-api.md`.
45
+
46
+ ## contentKey-only + mixed pages
47
+
48
+ Only `contentKey`s batch. A `CmsExternalRef` (public `unauthenticatedUrl`) has no
49
+ contentKey and is often a different channel/org — for news/structured content read it
50
+ with `getCmsContentByUrl` individually. A mixed list becomes 1 bulk call for the key
51
+ subset + N individual CDN fetches; wire the results into your own components. When you
52
+ render one card per ref instead, each card's independent read already isolates failures.
53
+ (A foreign `url` **media** ref is not fetched at all — its url is the asset src directly,
54
+ Rule 1 — so it neither batches nor needs an individual read.)
55
+
56
+ ## Out of scope
57
+
58
+ No DYNAMIC feed ("latest 10 news") — `getCmsContentByKeys` reads KNOWN keys; it
59
+ does not discover or query a collection. Endpoint + response contract:
60
+ `references/package-api.md`. Failure rows: `references/failure-modes.md`.
@@ -0,0 +1,111 @@
1
+ # Codegen guardrails — Rules 1–5 (full)
2
+
3
+ Five rules encoding defects a prior scaffold reproduced. Every generated app MUST
4
+ satisfy all five; a violation is a blocking bug, not a style nit. They are baked into
5
+ the templates under `assets/shared/`, `assets/react/`, `assets/angular/`; keep them
6
+ true when editing. SKILL.md carries the one-line form of each rule — this file is the
7
+ full text.
8
+
9
+ ## Rule 1 — Call the right toolkit read; never re-implement transport
10
+
11
+ Dispatch on ref type: a `CmsExternalRef` (public `unauthenticatedUrl`) →
12
+ `getCmsContentByUrl(url)`, fetched AS-IS; a `CmsRef` (`contentKey`) →
13
+ `getCmsContentByKey`/`getCmsContentByKeys`, authenticated Connect. Do NOT reconstruct
14
+ a delivery URL, prepend the instance URL / `/services/data/vXX`, hand-remap a
15
+ `contentKey`, or route a CDN URL through Connect or vice versa; the package owns all
16
+ of that.
17
+
18
+ **Exception — standalone media on a foreign `url` ref is NOT fetched.** A media
19
+ `unauthenticatedUrl` (`sfdc_cms__{image,audio,video,document}`) is the ASSET itself,
20
+ not a delivery-JSON endpoint, so `MediaRenderer` sets `ref.url` directly onto the
21
+ element `src`/`href` and calls neither `getCmsContentByUrl` nor a resolver (Rule 2).
22
+ There is no fetched body, so alt text / document label ride on the ref (`altText` /
23
+ `title`, threaded from the search hand-off or asked at Ref Registration). This is
24
+ media-only: a `CmsExternalRef` for news/structured content is still fetched via
25
+ `getCmsContentByUrl`, and a `contentKey` media ref still fetches via
26
+ `getCmsContentByKey`.
27
+
28
+ When a `CmsRef` needs a channel, **write `public/content-metadata.json`** with
29
+ the resolved `channelId` and empty `contents: []` (create if absent; never overwrite a
30
+ present channel). Write only a KNOWN channel (catalog / prompt / user) — **never a
31
+ placeholder or fake `channelId`**: the toolkit fails open on a *missing* catalog, but a
32
+ present-but-wrong one breaks every `byKey` read. The deploy pipeline regenerates the
33
+ catalog per target org and fills the real `contents`. Endpoint + response contract +
34
+ catalog shape: `references/package-api.md`.
35
+
36
+ ## Rule 2 — Resolve media `src` by ref shape, then by medium
37
+
38
+ `MediaRenderer` forks first on **ref shape**, then (on the fetched path) by medium.
39
+
40
+ **Foreign `url` ref — direct, no fetch, no resolver.** A media `unauthenticatedUrl`
41
+ is the asset itself, so `ref.url` goes straight onto the element `src`/`href`. No
42
+ `getCmsContentByUrl`, no `resolveCmsImageUrl`/`resolveMediaUrl`, no body. Alt text /
43
+ document label come off the ref (`altText`/`title`). Emitting an `<img>` `src` from
44
+ the resolver here is wrong — the url needs no resolution.
45
+
46
+ **`contentKey` ref — fetch the body, then resolve by medium.** The `src` comes from
47
+ the fetched body's `sfdc_cms:media` field, and the resolution SPLITS by medium — each
48
+ side calls a toolkit SYNC resolver with ONLY its subject, no options object:
49
+
50
+ - **image** → `resolveCmsImageUrl(body['sfdc_cms:media'])` — the SAME resolver the
51
+ heuristic renderer uses for image fields; media reuses it, not a parallel copy.
52
+ - **audio / video / document** → `resolveMediaUrl(body['sfdc_cms:media'].url)` — the
53
+ toolkit's media resolver, given the body `url` string directly.
54
+
55
+ The toolkit resolves the relative Connect-API url on its own — a relative media url
56
+ works as-is and must NOT be hand-prefixed (no instance origin, no org origin, no
57
+ `{ instanceOrigin }` option). Pass no second argument to either resolver. There is no
58
+ local URL-prefixing helper; skill code never builds a media URL by hand. Media
59
+ dispatch is by `ref.cmsType` and ref shape, never by body shape, so audio/video/document
60
+ never enter the heuristic renderer's `isImageObject` path (SKILL.md §B1).
61
+
62
+ ## Rule 3 — Shared/deduped fetch state must survive React 19 StrictMode
63
+
64
+ The React hook `useCmsItem` dedupes through a module-level `inflight` map and must
65
+ survive mount→cleanup→remount: cleanup sets a local `cancelled` flag gating
66
+ `setState` and MUST NOT abort the shared request; shared requests get no abort signal;
67
+ `inflight` self-cleans via `.finally(delete)` at creation. The Angular
68
+ `CmsItemService` uses the same shared `cache`/`inflight` maps and a stale-ref guard in
69
+ `ngOnChanges`. Treat "works once, fails on remount" as a blocking bug.
70
+
71
+ ## Rule 4 — Decode RichText before injecting, through ONE path per framework
72
+
73
+ Delivery returns RichText entity-encoded (`"&lt;div&gt;…"`), so injecting it raw
74
+ renders literal tags. Decode with the toolkit's `decodeRichHtmlEntities` FIRST, then
75
+ inject at the single site:
76
+
77
+ - **React** — `richTextSanitizer(decodeRichHtmlEntities(value))` → raw-HTML injection
78
+ in the `RenderField` dispatcher inside `heuristicRenderer.tsx`. Delivery RichText is
79
+ TRUSTED, so the default `richTextSanitizer` is **pass-through**; apps add DOMPurify
80
+ via `setRichTextSanitizer` at entry. The default MUST NOT be escape-only: the value
81
+ is already decoded, so escaping undoes the decode (**never both decode AND escape**).
82
+ Per-type renderers MUST forward into `heuristicRenderer`, never inline their own
83
+ injection.
84
+ - **Angular** — `decodeRichHtmlEntities(value)` → `[innerHTML]` in
85
+ `cms-content.component.ts`. Angular's `DomSanitizer` sanitizes the binding
86
+ automatically; do NOT `bypassSecurityTrustHtml`.
87
+
88
+ Detection must catch the encoded form: match a tag-open (`<tag`/`</`/`<!`) OR `&lt;`
89
+ (`RICH_TEXT_RE`), not `value.includes('<')`. A bare `<` in prose (`"Q4 < projected"`)
90
+ must NOT match, or the browser's HTML parser silently drops everything after it.
91
+
92
+ ## Rule 5 — Type scalar fields as primitives, never `{ value }` wrappers
93
+
94
+ Delivery returns SCALAR fields (Text, RichText, Date, Number, URL) as PLAIN
95
+ primitives (`"body": "&lt;div&gt;…"`, `"title": "Berries"`), NOT `{ value: "…" }`.
96
+ Only images arrive as objects (`CmsImageField`). A prior scaffold assumed a uniform
97
+ `{ value: string }` and read `body.body.value`, always `undefined`, so RichText
98
+ rendered as nothing.
99
+
100
+ - **Generated `<Type>Body` uses primitives.** Text/RichText/URL → `string`;
101
+ Date/DateTime → `string`; Number → `number`; Boolean → `boolean`; Image →
102
+ `CmsImageField`. Never `{ value: T }`. Template: `assets/shared/cmsContentType.ts`.
103
+ - **Read fields directly.** `body.body`, never `body.body.value`. The renderer
104
+ dispatches on the primitive's shape; per-type renderers delegate and MUST NOT unwrap
105
+ `.value`.
106
+ - **Derive shapes from a real payload.** Confirm each field against an actual delivery
107
+ response: an inline sample in the prompt, a fetched `unauthenticatedUrl`, or the
108
+ schema. For a `contentKey` ref with no URL to fetch, an inline sample is the primary
109
+ source. Read field shapes from `contentBody`; the item `title` is at the envelope
110
+ ROOT, not a `contentBody` field. `references/schema-sync.md`; envelope contract in
111
+ `references/package-api.md`.
@@ -0,0 +1,87 @@
1
+ # Detail Pages
2
+
3
+ The Detail Page branch of Render Target (SKILL.md step 4). Reached when the prompt
4
+ asks for a route/page (e.g. *"create a detail page for X"*) OR when the user picks
5
+ "detail page" in answer to the step-4 render-target question. Never created
6
+ implicitly — either the prompt states it or the user chooses it.
7
+
8
+ For a same-type GROUP, this branch runs ONCE PER ITEM (default group target): N
9
+ pages, N routes, each targeting its own ref, with slugs deduped across the batch so
10
+ same-title items don't collide. The renderer + `<type>.ts` are shared (generated
11
+ once in step 2); only pages, routes, and refs multiply.
12
+
13
+ Paths below are under the detected framework's dir — `src/cms/react/` or
14
+ `src/cms/angular/`. Templates: React `assets/react/DetailPage.tsx`, Angular
15
+ `assets/angular/DetailPage.component.ts`.
16
+
17
+ ## Prerequisites (else HALT)
18
+
19
+ 1. `<framework>/types/{{type}}.ts` exists (markers intact).
20
+ 2. The renderer exists — React `react/types/{{Type}}Renderer.tsx`, Angular
21
+ `angular/types/{{Type}}Renderer.component.ts`.
22
+ 3. `{{REF_CONST}}` registered in `src/cms/shared/externalRefs.ts`.
23
+ 4. Router file has a marker-blocked route region (below).
24
+
25
+ ## Inputs (confirm all in ONE prompt before writing)
26
+
27
+ | Input | Default |
28
+ |-------|---------|
29
+ | `PageName` | `<TitlePascal>DetailPage` (React) / `<TitlePascal>Component` (Angular) |
30
+ | `urlSlug` | `<title-kebab>` |
31
+ | target `ref` | most-recent Embed or explicit pick |
32
+ | router file | detected (show, allow override) |
33
+
34
+ If the user overrides a value, re-show the final diff and confirm again.
35
+
36
+ ## Generation steps
37
+
38
+ 1. Read the renderer — absent → HALT "run Embed for `{{type}}` first".
39
+ 2. Read `shared/externalRefs.ts` — `{{REF_CONST}}` absent → HALT "run Embed first".
40
+ 3. Page file — React `src/pages/{{type}}/{{PageName}}.tsx` from
41
+ `assets/react/DetailPage.tsx`; Angular `src/pages/{{type}}/{{PageName}}.component.ts`
42
+ from `assets/angular/DetailPage.component.ts` (token sub). Not present → write;
43
+ present+differs → HALT; present+matches → no-op.
44
+ 4. Insert the route (below).
45
+ 5. Verify: report file path, route entry, slug. No typecheck/build.
46
+
47
+ ## Router marker block
48
+
49
+ Pasted once by hand, exactly once per file:
50
+
51
+ **React** — inside `<Routes>` (react-router v6) OR a `RouteObject[]` literal:
52
+
53
+ ```tsx
54
+ {/* <experience-cms-content-render:detail-routes-begin> */}
55
+ {/* <experience-cms-content-render:detail-routes-end> */}
56
+ ```
57
+
58
+ **Angular** — inside the `Routes` array literal (e.g. `app.routes.ts`):
59
+
60
+ ```ts
61
+ // <experience-cms-content-render:detail-routes-begin>
62
+ // <experience-cms-content-render:detail-routes-end>
63
+ ```
64
+
65
+ **Detection:** grep candidates for the begin/end pair — React (`App.tsx`,
66
+ `routes.tsx`, `router.tsx`, `main.tsx`), Angular (`app.routes.ts`, `app.config.ts`,
67
+ `app-routing.module.ts`). Exactly one → use it. Multiple → HALT (list them). Zero →
68
+ HALT with the snippet to paste.
69
+
70
+ ## Route insertion
71
+
72
+ Insert inside the block, above `:end`. Append-only, idempotent, collision-safe:
73
+
74
+ - React JSX: `<Route path="{{urlSlug}}" element={<{{PageName}} />} />`
75
+ - React data-router: `{ path: '{{urlSlug}}', element: <{{PageName}} /> },`
76
+ - Angular: `{ path: '{{urlSlug}}', component: {{PageName}} },` (lazy:
77
+ `{ path: '{{urlSlug}}', loadComponent: () => import('./pages/{{type}}/{{PageName}}.component').then(m => m.{{PageName}}) },`)
78
+
79
+ Ensure `{{PageName}}` is imported (path recomputed per project); skip if already
80
+ imported. If the slug already routes to a DIFFERENT component → HALT (slug
81
+ collision). If it routes to `{{PageName}}` already → no-op.
82
+
83
+ ## Non-goals
84
+
85
+ Static slugs only (dynamic `:id`/route params → future WI). Generated page is a
86
+ thin wrapper (one renderer, `layout="detail"`); customize in the renderer. No route
87
+ guards — user hand-wraps outside the marker block.
@@ -0,0 +1,127 @@
1
+ # Embed Recipes
2
+
3
+ ## Pasted delivery-URL detection (Identity, step 1)
4
+
5
+ The input carries a CMS delivery URL when it matches an Experience CDN delivery URL
6
+ `https://<host>.salesforce-experience.com/cms/delivery/<ver>/<channel>/contents/<id>`.
7
+ Match with this case-insensitive regex; the whitespace-free host segment lets a URL
8
+ embedded in surrounding prose still match:
9
+
10
+ ```text
11
+ https:\/\/[^\s/]+\.salesforce-experience\.com\/cms\/delivery\/[^\s]*\/contents\/[^\s]+
12
+ ```
13
+
14
+ On a match, that URL IS the `unauthenticatedUrl` → skip search, foreign ref. No match
15
+ → fall through to the contentKey / natural-language paths.
16
+
17
+ ---
18
+
19
+ Placement patterns for the in-place branch of Render Target (SKILL.md step 4).
20
+ Every recipe assumes the ref is registered and the renderer exists (earlier
21
+ pipeline steps), and that a placement target was stated in the prompt or answered
22
+ to the step-4 question.
23
+
24
+ Base snippet — the per-type renderer takes a `ref`:
25
+
26
+ **React** (`react/types/{{TypeRenderer}}.tsx`, selector-less component):
27
+
28
+ ```tsx
29
+ import { {{Type}}Renderer } from '../cms/react/types/{{TypeRenderer}}';
30
+ import { ref } from '../cms/shared/externalRefs';
31
+
32
+ <{{Type}}Renderer ref={ref('{{REF_CONST}}')} />
33
+ ```
34
+
35
+ **Angular** (`angular/types/{{Type}}Renderer.component.ts`, selector `cms-{{type}}`):
36
+ import `{{Type}}RendererComponent` into the host component's `imports`, then:
37
+
38
+ ```html
39
+ <cms-{{type}} [ref]="itemRef"></cms-{{type}}> <!-- itemRef = ref('{{REF_CONST}}') on the host class -->
40
+ ```
41
+
42
+ Idempotency (all recipes): if the same renderer for the same `{{REF_CONST}}`
43
+ already exists inside the anchor element, skip.
44
+
45
+ | Recipe | Prompt shape | Anchor | Extra prop |
46
+ |--------|--------------|--------|------------|
47
+ | Named component/page | "add X to `<Component>`" | JSX/template return: `cms:embed` placeholder comment, else append to top element's children | — |
48
+ | List/grid item | "add X as a card in the list" | static sibling list → append; **dynamic `.map`/`@for` list → HALT** (add to data source instead) | `layout="card"` |
49
+ | Sidebar | "put X in the sidebar" | first `<aside>`; none → HALT for a target | `layout="card"` |
50
+ | Hero | "show X as the hero" | top-level element or first `<header>` | `layout="detail"` |
51
+ | Custom layout | "use my `card-large` layout" | as named; layout must exist in `{{type}}Layouts` else HALT | `layout="card-large"` |
52
+ | Fields whitelist | "only show title and image" | as named | React `fields={['title','bannerImage']}` / Angular `[fields]="['title','bannerImage']"` |
53
+ | Slot override (React only) | "render the title as a link" | as named; **always HALTs to show diff first** (creates a component in parent scope) | `components={{ Title: … }}` |
54
+
55
+ Angular has no `components` slot prop — for a custom title/image treatment, style
56
+ via the class hooks (`references/styling-scopes.md`) or customize the renderer.
57
+
58
+ ## Ref-registration forms (Embed step 3)
59
+
60
+ Append inside the `external-refs-begin` marker block of `src/cms/shared/externalRefs.ts`;
61
+ emit the form matching the ref type and ensure the `satisfies` type is imported:
62
+
63
+ ```ts
64
+ // Foreign ref (unauthenticatedUrl):
65
+ EXT_FOOD_BERRIES: { name: 'EXT_FOOD_BERRIES', url: 'https://…/cms/delivery/…/contents/…?oid=…', cmsType: 'news' } satisfies CmsExternalRef<'news'>,
66
+ // uiBundle-space ref (contentKey — remapped at runtime via content-metadata.json):
67
+ EXT_LAUNCH_NEWS: { name: 'EXT_LAUNCH_NEWS', contentKey: 'M3ASD4KHASDB73', cmsType: 'news' } satisfies CmsRef<'news'>,
68
+ // Foreign MEDIA ref (unauthenticatedUrl IS the asset — not fetched; alt/title on the ref):
69
+ EXT_HERO_PHOTO: { name: 'EXT_HERO_PHOTO', url: 'https://…cdn….salesforce-experience.com/…/hero.jpg?oid=…', cmsType: 'image', altText: 'Team on launch day' } satisfies CmsExternalRef<'image'>,
70
+ ```
71
+
72
+ `cmsType` MUST equal the fqn DeveloperName and match the `<K>` in `satisfies` — this
73
+ discriminator is what makes a wrong-typed `ref` a compile error against the renderer;
74
+ omit it and the guard silently disappears. Store the identity verbatim: a foreign
75
+ `url` (never re-sign, strip `oid`, or bump version) or the `sourceContentKey` (the
76
+ toolkit owns source→target remap).
77
+
78
+ **Media on a foreign `url` ref carries `altText`/`title`.** The direct-URL media path
79
+ does not fetch a body, so the accessible name lives on the ref: `altText` (image alt /
80
+ audio-video aria-label) and `title` (document download-link label), sourced from the
81
+ search hand-off or the *Media alt text* ask (`references/interaction-model.md`).
82
+ `audio`/`video` need no `altText`. These fields are ignored on non-media refs and on
83
+ `contentKey` media refs (which read alt/title from the fetched body).
84
+
85
+ ### Channel resolution (CmsRef only)
86
+
87
+ A `CmsRef` MUST resolve a channel — the catalog is primary. Resolve in order, stop at
88
+ the first hit:
89
+
90
+ 1. **Existing catalog** — read `public/content-metadata.json`; a non-empty `channelId`
91
+ → use it, do NOT overwrite.
92
+ 2. **Prompt / identity** — a `channelId` accompanied the `contentKey` → write it into
93
+ `public/content-metadata.json`, creating the file if absent:
94
+ ```jsonc
95
+ { "channelId": "0apSG0000000ExampleChannel", "contents": [] }
96
+ ```
97
+ (`public/` is Vite's web root, so the toolkit's default `${BASE_URL}content-metadata.json`
98
+ resolves to `<appRoot>/public/content-metadata.json`. Empty `contents: []` is safe —
99
+ per-entry remap fails open; the deploy pipeline fills the real pairs.)
100
+ 3. **Neither → ASK** the user for the channelId (`references/interaction-model.md` →
101
+ *Channel ID*), then write it per step 2.
102
+ 4. **Still unresolved → HALT**: *"'{title}' is a contentKey with no resolvable channel;
103
+ rendering will fail. Provide the channelId or add it to public/content-metadata.json."*
104
+ Never write a placeholder/fake channel.
105
+
106
+ **Conflict:** a prompt-provided channel that DIFFERS from a non-empty channel already in
107
+ the catalog → ASK which wins with an option-select (`references/interaction-model.md` →
108
+ *Channel conflict*), never a silent overwrite. On **keep the catalog** → leave
109
+ `content-metadata.json` unchanged (no write). On **use the prompt channel** → overwrite
110
+ ONLY the `channelId` (preserve the existing `contents` array). Same/empty → use/write.
111
+ Idempotent: present channel → skip; absent → create; present-but-empty → fill.
112
+
113
+ `CMS_CHANNEL_ID_FALLBACK` in `externalRefs.ts` is a **cautioned, opt-in override** only
114
+ (the skill no longer auto-fills it; it is baked into source and does not travel per-org).
115
+ Precedence when the toolkit reads: explicit option > `CMS_CHANNEL_ID_FALLBACK` > catalog
116
+ `channelId` > the toolkit throws. Foreign refs carry their channel in the URL — no gate.
117
+
118
+ ## Anchor detection failure
119
+
120
+ No recipe applies (no top-level component, JSX/template return, list, or aside) →
121
+ HALT and report the file's markup structure so the user names a concrete anchor.
122
+
123
+ ## Non-goals
124
+
125
+ No Storybook, no test scaffolding, no wrapper components (`<Suspense>`,
126
+ `<CmsBoundary>`) — the renderer handles loading/error internally (React
127
+ `useCmsItem`, Angular `CmsItemService`).
@@ -0,0 +1,96 @@
1
+ # Failure Modes
2
+
3
+ ## Rich-text sanitizer plug (React only)
4
+
5
+ The RichText mechanics (decode-then-inject, why the default is pass-through) live in
6
+ Rule 4 / `references/heuristic-render-rules.md`. The one actionable extra: apps add
7
+ DOMPurify for defense-in-depth by registering a sanitizer once at entry — a runtime
8
+ plug, not a hard dep, so the choice is visible at the app boundary:
9
+
10
+ ```ts
11
+ import DOMPurify from 'dompurify';
12
+ import { setRichTextSanitizer } from './cms/react/heuristicRenderer';
13
+ setRichTextSanitizer(DOMPurify.sanitize);
14
+ ```
15
+
16
+ Angular has no such seam — `[innerHTML]` runs through `DomSanitizer` automatically.
17
+
18
+ ## Terminal stops (session ends — nothing scaffolded)
19
+
20
+ Distinct from the recoverable HALTs below: these END the session. See
21
+ `references/interaction-model.md` → *Terminal stops*.
22
+
23
+ | Trigger | What the skill does |
24
+ |---------|---------------------|
25
+ | **Init declined** — user answers "No" to the *Run Init?* option question | State Init was declined → skill HALTS and the session ends. Nothing installed or written; end the turn. Do not fall through to Embed. |
26
+ | **Zero search content** — experience-search-coordinate succeeds but returns zero content for the phrase | The item isn't authored in this uiBundle space yet. Stop and tell the user to generate the content there first (author + publish, e.g. via experience-cms-content-generate / experience-cms-content-type-generate), then re-run. Do not guess an identity or scaffold. |
27
+
28
+ ## HALT catalogue
29
+
30
+ | Trigger | Fix |
31
+ |---------|-----|
32
+ | Missing/malformed marker in managed file | User restores the marker line. Skill never auto-heals. |
33
+ | Ref name collision, different URL/key | User renames or removes the existing entry. |
34
+ | Init drift (scaffold differs from template) | User accepts (regenerate) or keeps their edit. |
35
+ | Foundation drift — some foundation files present, some missing | User restores the missing files or removes the partial set; the skill never Embeds onto a partial runtime. |
36
+ | Ambiguous framework — both `react` and `@angular/core` are deps | User names which framework to target. |
37
+ | Unknown framework — neither `react` nor `@angular/core` (or no `package.json`) | Not a supported React/Angular app; skill does not scaffold. |
38
+ | Router marker ambiguous / missing | User removes stale markers, or pastes the marker block. |
39
+ | Slug collision | User picks a different slug or removes the existing route. |
40
+ | Render target unanswered (step 4) — "in place" chosen but no placement named, or no anchor found | User names a component/page/region to render into. Skill never guesses a placement. |
41
+ | Detail-page prerequisite missing (renderer/ref) | User runs Embed for the type first. |
42
+ | Detail-page file exists with different content | User deletes or renames. |
43
+ | Content has neither `unauthenticatedUrl` nor `contentKey` | User pastes a public delivery URL or picks another item. (A `contentKey`-only item is supported — register a `CmsRef`.) |
44
+ | Multi-item selection when Embed needs one | User re-runs and picks a single item. |
45
+ | `CmsRef` with no resolvable channel — none in `public/content-metadata.json`, none in the prompt, and (bypass) no user to ask | User provides the channelId or adds it to `public/content-metadata.json`. The skill never invents or placeholders a channel; a `CmsRef` cannot render without one. |
46
+ | channelId conflict — a prompt-provided channel differs from a non-empty `channelId` already in `public/content-metadata.json` | ASK which wins as an option-select (`references/interaction-model.md` → *Channel conflict*): keep the catalog channel (no write) or use the prompt channel (overwrite only `channelId`, preserve `contents`). The skill overwrites the catalog only on confirmation, never silently. |
47
+
48
+ ## Runtime failures (renderer-level)
49
+
50
+ Surfaced via `useCmsItem`'s `error` state (React) / `CmsItemService.load`'s
51
+ returned `error` (Angular), and the renderer's `role="alert"` branch. The toolkit
52
+ raises typed errors — `CmsDeliveryError` / `CmsDeliveryNotFoundError` (public CDN)
53
+ and `ConnectApiError` / `CmsNotFoundError` (authenticated Connect) — which the
54
+ hook/service pass through unchanged.
55
+
56
+ | Symptom | Cause | User action |
57
+ |---------|-------|-------------|
58
+ | HTTP 404 on a foreign ref | Unpublished, truncated URL, OR URL rewritten (doubled version, wrong host — Rule 1) | Verify the `unauthenticatedUrl`; the toolkit's `getCmsContentByUrl` fetches it AS-IS. |
59
+ | HTTP 401 on a foreign ref | Foreign-ref URL not actually public | For public delivery it must be truly public; a non-public uiBundle item should be a `CmsRef` (`contentKey`) read via `getCmsContentByKey`, not a `CmsExternalRef`. |
60
+ | `CmsNotFoundError` on a `CmsRef` | `contentKey` not published in this org, or the catalog remapped to a stale `targetContentKey` | Verify the item is published; check `content-metadata.json` maps the source key to a live target (or omit the entry to fall back to the baked-in key). |
61
+ | `CmsNotFoundError` / Connect error on EVERY `CmsRef` | Channel could not be resolved — no `channelId` in `public/content-metadata.json`, empty `CMS_CHANNEL_ID_FALLBACK`, AND no explicit option | Add `channelId` to `public/content-metadata.json` (the primary, cross-org-safe seam); the cautioned `CMS_CHANNEL_ID_FALLBACK` in `externalRefs.ts` is an opt-in override only. Precedence: explicit option > fallback > catalog > the toolkit throws. |
62
+ | "No content found." / empty render, either transport | Body typed wrong — the toolkit returns the already-UNWRAPPED `contentBody` (envelope `title` lifted in), NOT `{ items: [...] }` or the raw envelope | Type the read as `getCmsContentBy*<TBody>` where `TBody` is the `contentBody` shape. Never re-implement unwrap or read `items[0]`. Contract: `references/package-api.md`. |
63
+ | Content renders but the TITLE is missing | `<Type>Body` read `contentBody.title`, but delivery returns `title` at the ENVELOPE ROOT | The toolkit lifts the root `title` into the returned body (when the body lacks one) so `pickTitle`/layouts see it. Do not emit `title` as a `<Type>Body` schema field. Contract: `references/package-api.md`. |
64
+ | Image silently missing, url IS present in the payload | The field was used directly as `src` instead of going through the resolver | Never use an image field as a `src`. Pass it to `resolveCmsImageUrl(field)` (React `resolveImageSrc`; Angular's `resolveImageSrc` method) — it handles both the top-level-`url` (CMS `imageReference`) and `source.ref`-string (foreign `url`) variants, with no options. Shape: `CmsImageField`; contract: `references/package-api.md`. |
65
+ | CMS image doesn't load | An image field was used directly as `src` instead of going through the resolver | Resolve via `resolveCmsImageUrl(field)` — it resolves a relative Connect-API url on its own and passes absolute urls through. Pass NO options; it is SYNC, so resolve inline (no async image component). |
66
+ | Standalone media (audio/video/document) 404s **on a `contentKey` ref** | The body `sfdc_cms:media.url` was used directly as the element `src` instead of going through the resolver | Resolve via `resolveMediaUrl(sfdc_cms:media.url)` (Rule 2) — the toolkit resolves a relative Connect-API url itself. Pass NO options and never hand-prefix the url. Images use `resolveCmsImageUrl` instead. (This is the CONTENTKEY path only — a foreign `url` media ref uses `ref.url` directly with no fetch and no resolver; see the next row.) |
67
+ | Standalone media on a foreign `url` ref renders blank / doesn't load | The renderer fetched the `unauthenticatedUrl` (or ran it through a resolver) instead of using it directly | A media `unauthenticatedUrl` is the ASSET itself — set `ref.url` straight onto the element `src`/`href`, NO `getCmsContentByUrl` and NO resolver (Rule 1/Rule 2). If the url genuinely 404s, verify it is the direct asset url (all query params intact) and the item is published. |
68
+ | Media image on a foreign `url` ref has no alt text / a document link reads "Download" | `altText`/`title` weren't threaded onto the `CmsExternalRef` (direct-URL path has no body to read them from) | Capture `altText`/`title` at Ref Registration from the search hand-off, or ASK the user (`references/interaction-model.md` → *Media alt text*). `audio`/`video` need no alt. |
69
+ | Public-CDN fetch fails in dev (either framework) | Something OTHER than CORS — the CMS delivery backend serves CORS headers, so a direct cross-origin fetch from `localhost` works with no proxy | No dev proxy is needed or scaffolded (the former proxy rule was removed). Check the `unauthenticatedUrl` is correct and reachable, and that the item is published; a genuine failure here is a 404/401 or a network error, not a CORS block. |
70
+ | RichText renders as literal `<tags>` | Entity-encoded HTML injected without decoding; OR (React) the default `richTextSanitizer` was made escape-only and re-escaped already-decoded HTML | Decode via the toolkit's `decodeRichHtmlEntities` before injecting; React keeps the default sanitizer pass-through; never both decode AND escape the same string (Rule 4). |
71
+ | Malformed body | Schema drift | Re-run Embed (Schema Sync detects drift). |
72
+ | Empty render | Field names don't match priority lists | Pass a `fields={[…]}` (React) / `[fields]="[…]"` (Angular) whitelist. |
73
+ | One card in a bulk list is missing / errors | A `getCmsContentByKeys` best-effort read returns a per-key `error` (e.g. `CmsNotFoundError`) for an unpublished/wrong-channel key | Expected — `getCmsContentByKeys` isolates per-key failures rather than throwing; render the `error` rows distinctly. `references/bulk-loading.md`. |
74
+ | A `unauthenticatedUrl` card won't batch | Foreign CDN ref has no `contentKey` and often a different channel — structurally can't join a bulk read | Expected: foreign refs degrade to their individual `getCmsContentByUrl` fetch; only `CmsRef` keys batch (`references/bulk-loading.md`). |
75
+
76
+ ## React 19 StrictMode
77
+
78
+ StrictMode intentionally mounts → cleans up → remounts. A hook that dedupes
79
+ through a shared `inflight` map (`useCmsItem`) must survive this. The classic
80
+ regression: cleanup aborts the SHARED in-flight request, the remount reuses the
81
+ now-dead promise, and every double-mount fails. Fix (baked into
82
+ `assets/react/useCmsItem.ts`, Rule 3): cleanup sets a `cancelled` flag and gates
83
+ `setState` (never aborts the shared request); shared requests get no abort
84
+ signal; `inflight` is self-cleaning via `.finally()` at creation. Treat "works
85
+ once, fails on remount" as a blocking bug. The Angular `CmsItemService`
86
+ (`assets/angular/cms-item.service.ts`) uses the same shared `cache`/`inflight`
87
+ maps and a stale-ref guard in `ngOnChanges`.
88
+
89
+ ## Not failure modes
90
+
91
+ - StrictMode double-mount → single network request (dedup + suppress-not-abort). By design.
92
+ - Undefined per-embed `layout="…"` → falls through to `detail`/`list`; not a HALT.
93
+ - Stale ref after source content removed → runtime 404; skill does not prune refs.
94
+ - `content-metadata.json` missing / 404 / invalid JSON / entry absent → the toolkit
95
+ fails open to the baked-in `contentKey` (and no catalog channel). A missing
96
+ catalog is the normal local-dev state, not an error.
@@ -0,0 +1,131 @@
1
+ # Heuristic Render Rules
2
+
3
+ Field-selection + value-shape rules for rendering an already-fetched, already-
4
+ unwrapped `contentBody` when there's no typed renderer. The SAME rules run in both
5
+ frameworks, in parallel implementations:
6
+
7
+ - **React** — `heuristicRenderer.tsx`: field selection (which fields, in what
8
+ order) plus the internal `RenderField` dispatcher (how one value renders), in one
9
+ file.
10
+ - **Angular** — one standalone `CmsContentComponent` (`cms-content`); `buildCells`
11
+ picks fields, `classify` maps each value to a `Cell` the `@switch` template
12
+ renders.
13
+
14
+ Neither fetches — a container (`useCmsItem` / `CmsItemService`) passes `body`,
15
+ `loading`, `error` in. Image-URL resolution is the toolkit's job
16
+ (`resolveCmsImageUrl`, SYNCHRONOUS), so images render as a plain `<img>` with no
17
+ async component in either framework.
18
+
19
+ ## Field selection
20
+
21
+ **Whitelist mode** — caller passes `fields` (React `fields={[…]}`, Angular
22
+ `[fields]="[…]"`): iterate in order, render one field each, nothing else.
23
+
24
+ **Heuristic mode** — no whitelist, pick by priority list:
25
+
26
+ ```text
27
+ IMAGE_PRIORITY = ['bannerImage','featuredImage','heroImage','coverImage','thumbnail','image']
28
+ EXCERPT_PRIORITY = ['excerpt','summary','description','subtitle']
29
+ ```
30
+
31
+ 1. Title — field named `title`, else first string key matching `/title/i`.
32
+ 2. Image — first `IMAGE_PRIORITY` field with an image shape.
33
+ 3. Excerpt — first `EXCERPT_PRIORITY` string.
34
+ 4. Overflow — `layout='detail'` renders remaining fields in order; `list` drops them.
35
+
36
+ Layout: `list` → `<h2>` title, no overflow; `detail` → `<h1>` title, overflow
37
+ shown. In React, slot overrides `components={{ Title, Image, RichText }}` win over
38
+ defaults (per-embed prop > type slot > default); Angular has no slot props (style
39
+ via class hooks + the container).
40
+
41
+ ## Value-shape decision table
42
+
43
+ React `RenderField(name, value, layout, resolveRef, …slots)` / Angular
44
+ `classify(name, value)` → `Cell`:
45
+
46
+ | Value shape | Rendering |
47
+ |-------------|-----------|
48
+ | `null`/`undefined` | skip |
49
+ | string matching `RICH_TEXT_RE` (`<tag`/`</`/`<!` OR `&lt;` OR `&#60;`/`&#x3c;`) | rich text — see below |
50
+ | `^\d{4}-\d{2}-\d{2}` AND parseable | `<time>` via `Intl.DateTimeFormat({dateStyle:'medium'})`; unparseable → fall through |
51
+ | `^https?://` | `<a target=_blank rel="noopener noreferrer">` |
52
+ | string, name `/title/i` | heading (h1 detail / h2 list); React TitleSlot wins |
53
+ | string (else) | `<p>` |
54
+ | number | `<span>` via `Intl.NumberFormat` |
55
+ | boolean | `<span class="badge">` Yes/No |
56
+ | array (React) | recurse each element as `${name}[${i}]` |
57
+ | object `.dateTime` | `<time>` dateStyle+timeStyle, optional `.timeZone` |
58
+ | object image-shaped | `resolveCmsImageUrl` → `src`, `field.altText ?? ''` → `alt` → `<img loading=lazy>` (React ImageSlot wins) |
59
+ | object `.ref.contentKey` | `resolveRef` → title, else contentKey |
60
+ | else | skip |
61
+
62
+ An object is "image-shaped" when it has a top-level string `url` OR a `source`
63
+ with a string `type` (`isImageObject`).
64
+
65
+ ## RichText — decode, then inject through ONE path
66
+
67
+ Delivery sends RichText entity-encoded (`&lt;div&gt;…`), so the match MUST include
68
+ `&lt;` — a `<`-only test never fires and tags render as literal text. The literal
69
+ side matches a tag-open (`<` followed by a letter, `/`, or `!`), NOT a bare `<`, so
70
+ plain prose like `"Q4 < projected"` stays a `<p>` instead of being dropped by the
71
+ browser's HTML parser. Decode with
72
+ the toolkit's `decodeRichHtmlEntities` BEFORE injecting; never HTML-escape a
73
+ decoded string (decode + escape cancel out). Injection is a single site per
74
+ framework:
75
+
76
+ - **React** — `richTextSanitizer(decodeRichHtmlEntities(value))` → `dangerouslySet
77
+ InnerHTML` in the `RenderField` dispatcher inside `heuristicRenderer.tsx`.
78
+ Delivery RichText is TRUSTED, so the default
79
+ sanitizer is pass-through; apps add DOMPurify via `setRichTextSanitizer` for
80
+ defense-in-depth (an escape-only default would undo the decode).
81
+ - **Angular** — `decodeRichHtmlEntities(value)` → `[innerHTML]` in
82
+ `cms-content.component.ts`. Angular's `DomSanitizer` sanitizes the binding
83
+ automatically — do NOT `bypassSecurityTrustHtml`.
84
+
85
+ Rule 4 in `SKILL.md`; symptoms in `references/failure-modes.md`.
86
+
87
+ ## Image URL resolution (toolkit-owned)
88
+
89
+ Never use an image field directly as a `src` — pass it to the toolkit's
90
+ `resolveCmsImageUrl(field)` (React helper `resolveImageSrc`; the Angular component's
91
+ private `resolveImageSrc`). It handles both variants and the references-bag fallback,
92
+ and resolves a relative CMS media url on its own; absolute/CDN urls pass through.
93
+ Pass NO options object — the resolver needs no instance origin. The call is
94
+ synchronous — resolve inline, render nothing when it returns `undefined`. Field shape
95
+ + the two variants: `references/package-api.md`.
96
+
97
+ For the `alt` attribute, read the field's authored `altText` (`resolveImageAlt`);
98
+ `resolveCmsImageUrl` returns only the `src` string and drops the field, so read
99
+ `altText` from the original field object, not the resolved URL. Fall back to `''`
100
+
101
+ ## Media types — a dedicated renderer, NOT the heuristic path
102
+
103
+ Standalone CMS media (`sfdc_cms__{image,audio,video,document}`) is a PREDEFINED type
104
+ parallel to `news`, and a single asset rather than a field bag — so it does NOT run
105
+ through this heuristic renderer at all. A media ref (`cmsType` ∈ `image`/`audio`/
106
+ `video`/`document`) binds to the shared **`MediaRenderer`** (React
107
+ `assets/react/MediaRenderer.tsx`; Angular `assets/angular/MediaRenderer.component.ts`),
108
+ which switches on `cmsType` to one element. It **forks first by ref shape** for how the
109
+ `src` is obtained (Rule 1/Rule 2):
110
+
111
+ - **Foreign `url` ref** — `ref.url` IS the asset, so it goes directly onto the element
112
+ `src`/`href`: NO fetch, NO resolver. `alt`/document-label come off the ref
113
+ (`ref.altText` / `ref.title`), since there is no body to read them from.
114
+ - **`contentKey` ref** — fetch the uniform `CmsMediaBody` (`sfdc_cms:media` + optional
115
+ `altText`) via `useCmsItem` / `CmsItemService`, then resolve the src by medium.
116
+
117
+ | cmsType | Element | Src — `url` ref | Src — `contentKey` ref |
118
+ |---|---|---|---|
119
+ | `image` | `<img src alt loading="lazy">` | `ref.url` | `resolveCmsImageUrl(sfdc_cms:media)` |
120
+ | `audio` | `<audio controls src>` (no `altText`) | `ref.url` | `resolveMediaUrl(url)` |
121
+ | `video` | `<video controls src>` | `ref.url` | `resolveMediaUrl(url)` |
122
+ | `document` | `<a href download>` — download link only, no inline preview | `ref.url` | `resolveMediaUrl(url)` |
123
+
124
+ Dispatch is by `cmsType` (known at ref-registration time) and ref shape, **never by body
125
+ shape** — so audio/video/document never enter this file's `isImageObject` branch and
126
+ can't be mis-rendered as an `<img>`. On the `contentKey` path, src resolution splits by
127
+ medium: image reuses the toolkit image resolver; audio/video/document pass the body `url`
128
+ to the toolkit's `resolveMediaUrl`. Both take only the subject and no options — the
129
+ toolkit resolves a relative Connect-API url itself (Rule 2,
130
+ `references/codegen-guardrails.md`). `mimeType` inside `source` is available for finer
131
+ intra-medium choices but is NOT the type discriminator.