@duffcloudservices/cms 0.11.0 → 0.13.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 (41) hide show
  1. package/README.md +244 -8
  2. package/dist/chunk-A5F4C72F.js +500 -0
  3. package/dist/chunk-A5F4C72F.js.map +1 -0
  4. package/dist/{chunk-F3EIWEZD.js → chunk-HVSF23P7.js} +971 -73
  5. package/dist/chunk-HVSF23P7.js.map +1 -0
  6. package/dist/editor/editorBridge.d.ts +53 -1
  7. package/dist/editor/editorBridge.js +141 -5
  8. package/dist/editor/editorBridge.js.map +1 -1
  9. package/dist/headHonesty-OzxvLuwd.d.ts +222 -0
  10. package/dist/index.d.ts +369 -22
  11. package/dist/index.js +424 -22
  12. package/dist/index.js.map +1 -1
  13. package/dist/installSeoHead-kWQwObez.d.ts +627 -0
  14. package/dist/plugins/index.d.ts +173 -6
  15. package/dist/plugins/index.js +628 -54
  16. package/dist/plugins/index.js.map +1 -1
  17. package/dist/seo/index.d.ts +763 -4
  18. package/dist/seo/index.js +2 -2
  19. package/dist/{vitepressTransform-DfmABXmK.d.ts → vitepressTransform-JG_zlaux.d.ts} +99 -6
  20. package/package.json +26 -16
  21. package/src/components/DcsCallButton.test.ts +58 -0
  22. package/src/components/DcsCallButton.vue +19 -4
  23. package/src/components/DcsReviewShowcase.vue +5 -1
  24. package/src/components/LiteMediaEmbed.vue +3 -3
  25. package/src/components/ManagedImage.test.ts +94 -0
  26. package/src/components/ManagedImage.vue +58 -6
  27. package/src/components/PreviewRibbon.vue +4 -1
  28. package/src/composables/useConversionTracking.test.ts +492 -0
  29. package/src/composables/useConversionTracking.ts +770 -0
  30. package/src/composables/useReleaseNotes.ts +7 -1
  31. package/src/composables/useResponsiveImage.ts +6 -0
  32. package/src/composables/useSEO.applyHead.test.ts +150 -0
  33. package/src/composables/useSEO.ts +63 -17
  34. package/src/composables/useSiteVersion.ts +4 -1
  35. package/src/composables/useSiteVisitorSession.test.ts +56 -0
  36. package/src/composables/useSiteVisitorSession.ts +39 -3
  37. package/src/composables/useTextContent.ts +9 -1
  38. package/dist/chunk-DAYLLSEE.js +0 -3
  39. package/dist/chunk-DAYLLSEE.js.map +0 -1
  40. package/dist/chunk-F3EIWEZD.js.map +0 -1
  41. package/dist/spliceHeadHtml-CsBEucGy.d.ts +0 -254
@@ -0,0 +1,222 @@
1
+ /**
2
+ * P1 — baked-vs-rendered `<head>` honesty rail.
3
+ *
4
+ * ## The defect this exists for
5
+ *
6
+ * `.dcs/seo.yaml` is the owner-approved, portal-managed source of truth for a
7
+ * page's `<title>` and description, and {@link buildHeadTags} bakes it into the
8
+ * per-route static HTML. But the SAME resolver is also reachable at runtime via
9
+ * `useSEO().applyHead(overrides)`, and an `overrides.title` is used VERBATIM —
10
+ * it does not go through `titleTemplate`. So a view that hardcodes
11
+ * `applyHead({ title: 'Our Services' })` silently replaces the approved title
12
+ * the instant the app boots.
13
+ *
14
+ * Measured on live production (2026-07-27, before the site-local fix):
15
+ *
16
+ * ```
17
+ * route baked (non-JS crawler) runtime (Google + humans)
18
+ * / Rochester Hills Handyman, Repairs & Carpentry | Iron Oak Iron Oak Contractors | Crafted Repairs …
19
+ * /services Handyman, Carpentry & Home Repairs in SE Michigan | … Our Services
20
+ * ```
21
+ *
22
+ * Two crawlers, two different sites. Nothing failed. Nothing logged. The
23
+ * approved SEO decision (C-147 / Q-141=B) was live for AI crawlers and reverted
24
+ * for Google — for the whole time since it shipped.
25
+ *
26
+ * ## What the rail asserts
27
+ *
28
+ * After the build, for every route the emitter wrote, the prerender browser is
29
+ * already loading the page. This rail reads `document.title` and the effective
30
+ * `meta[name=description]` from that SAME render and compares them to the values
31
+ * baked into the file on disk. Any divergence names the route and BOTH values.
32
+ *
33
+ * ## Why the pass condition is stable (not a moving target)
34
+ *
35
+ * A site whose views call `applyHead()` with no title/description overrides
36
+ * produces byte-identical tags at runtime and at build time — same
37
+ * {@link buildHeadTags}, same `seo.yaml` (the plugin bakes it into
38
+ * `__DCS_SEO__`). Zero divergence is therefore the *structural* outcome of
39
+ * seo.yaml being the only writer, not a threshold someone has to keep tuning.
40
+ *
41
+ * ## Escape hatches (visible by construction)
42
+ *
43
+ * `mode` can be lowered to `warn`/`off` and individual routes can be listed in
44
+ * `allow`, but both live in `.dcs/seo.yaml` (portal-owned, in git, reviewable)
45
+ * or in an env var that prints in the build log — there is no silent way to
46
+ * switch this off. Every allowed route is still logged.
47
+ */
48
+ /** How a divergence is reported. */
49
+ type HonestyMode = 'error' | 'warn' | 'off';
50
+ /** One route's baked and rendered head values. */
51
+ interface HeadObservation {
52
+ /** Route path as it appears in `pages.yaml` (e.g. `/services`). */
53
+ route: string;
54
+ /** `<title>` as written into the emitted file (still HTML-escaped). */
55
+ bakedTitle: string | null;
56
+ /** `document.title` after the app mounted. */
57
+ runtimeTitle: string | null;
58
+ /** `meta[name=description]@content` as written into the emitted file. */
59
+ bakedDescription: string | null;
60
+ /** The EFFECTIVE `meta[name=description]` in the rendered DOM (the last one). */
61
+ runtimeDescription: string | null;
62
+ /** How many `meta[name=description]` tags the rendered DOM carried. */
63
+ runtimeDescriptionCount?: number;
64
+ }
65
+ /** A single baked≠rendered divergence. */
66
+ interface HeadHonestyViolation {
67
+ route: string;
68
+ field: 'title' | 'description';
69
+ baked: string | null;
70
+ runtime: string | null;
71
+ }
72
+ /** Configuration for the rail. */
73
+ interface HeadHonestyOptions {
74
+ /** `error` (default) fails the build, `warn` logs, `off` skips entirely. */
75
+ mode?: HonestyMode;
76
+ /** Also compare the meta description (default `true`). */
77
+ checkDescription?: boolean;
78
+ /**
79
+ * Route paths exempted from the comparison. An exemption is still LOGGED, so
80
+ * "who turned this off for which route" is answerable from the build output.
81
+ */
82
+ allow?: string[];
83
+ }
84
+ /**
85
+ * Decode the HTML character references the emitter can produce.
86
+ *
87
+ * This is NOT a general-purpose entity decoder — it covers the set
88
+ * `spliceHeadHtml`'s `escapeAttr` emits (`& " < >`) plus numeric references and
89
+ * the handful of named ones that appear in real CMS copy. Anything unrecognised
90
+ * is left verbatim, which is the safe direction: an unknown entity can only ever
91
+ * cause a *reported* divergence to be investigated, never a real one to be
92
+ * silently swallowed.
93
+ */
94
+ declare function decodeHtmlEntities(value: string): string;
95
+ /**
96
+ * Put a baked (HTML-source) value and a rendered (DOM) value on the same
97
+ * footing before comparing them.
98
+ *
99
+ * The three transforms exist because of three MEASURED false-positive
100
+ * mechanisms, not from caution:
101
+ * - **entity decode** — the baked side carries `Repairs &amp; Carpentry`
102
+ * while `document.title` yields `Repairs & Carpentry`. Identical strings.
103
+ * - **whitespace collapse** — `document.title` is specified to strip and
104
+ * collapse ASCII whitespace; the emitted `<title>` keeps the author's.
105
+ * - **Unicode NFC** — a composed `é` and a decomposed `é` render identically
106
+ * and mean the same thing; only the byte sequence differs.
107
+ *
108
+ * Deliberately NOT normalised: case, punctuation, and typographic look-alikes
109
+ * (`-` vs `—`, `'` vs `’`). Those are real content differences and a rail that
110
+ * hides them is the vacuous-green shape this whole class of work exists to kill.
111
+ */
112
+ declare function normalizeHeadText(value: string | null | undefined): string | null;
113
+ /** The baked `<title>` / description of an emitted document. */
114
+ interface BakedHead {
115
+ title: string | null;
116
+ description: string | null;
117
+ }
118
+ /**
119
+ * Read the baked `<title>` and `meta[name=description]` out of an emitted file.
120
+ *
121
+ * Regex-based on purpose — it mirrors `spliceHeadHtml`, runs over the emitter's
122
+ * own deterministic output, and keeps a heavy HTML parser out of the build.
123
+ */
124
+ declare function extractBakedHead(html: string): BakedHead;
125
+ /**
126
+ * Compare every observation and return the divergences.
127
+ *
128
+ * A `null` on either side is meaningful and reported: a runtime that DELETES the
129
+ * baked description is the same class of silent undoing as one that rewrites it.
130
+ * The one exception is a route where BOTH sides are absent — nothing was claimed,
131
+ * so nothing was undone.
132
+ */
133
+ declare function findHeadHonestyViolations(observations: HeadObservation[], options?: HeadHonestyOptions): HeadHonestyViolation[];
134
+ /**
135
+ * Render the full report. Every violation names the ROUTE and BOTH values, so
136
+ * the operator never has to reproduce the divergence to act on it — which is the
137
+ * difference between a rail people fix and a rail people mute.
138
+ */
139
+ declare function formatHeadHonestyReport(violations: HeadHonestyViolation[]): string;
140
+ /** Thrown in `error` mode so the build goes red on a divergence. */
141
+ declare class HeadHonestyError extends Error {
142
+ readonly violations: HeadHonestyViolation[];
143
+ constructor(violations: HeadHonestyViolation[]);
144
+ }
145
+ /** Inputs that can set the rail's severity, in increasing precedence. */
146
+ interface HonestyModeInput {
147
+ /** Compiled-in default. */
148
+ fallback?: HonestyMode;
149
+ /** The plugin option in `vite.config.ts`. */
150
+ option?: HonestyMode | boolean;
151
+ /** The per-site escape hatch in `.dcs/seo.yaml`. */
152
+ seoYaml?: HonestyMode | boolean;
153
+ /** The env override (e.g. `DCS_SEO_HEAD_HONESTY=warn`) — highest precedence. */
154
+ env?: string | undefined;
155
+ }
156
+ /**
157
+ * Resolve the effective severity.
158
+ *
159
+ * Precedence is env > seo.yaml > plugin option > default, i.e. the MOST
160
+ * operator-visible signal wins. An unrecognised value is ignored rather than
161
+ * treated as "off" — a typo'd escape hatch must never silently disable a gate.
162
+ */
163
+ declare function resolveHonestyMode(input: HonestyModeInput): HonestyMode;
164
+ /**
165
+ * What the render pass was able to observe, so the rail can prove it RAN.
166
+ *
167
+ * ## The defect this exists for
168
+ *
169
+ * C-334 shipped P1 with an environment-dependent silent skip: when
170
+ * `createPlaywrightRenderer` found no playwright/browser it warned and returned
171
+ * no observations, and the caller then *skipped the assert entirely* — so an
172
+ * `error`-severity rail let the build pass having checked nothing. That is the
173
+ * same shape as C-322 (the snapshot rail was vacuous for months because `gh` was
174
+ * missing on the runner) and the exact class this campaign exists to close.
175
+ *
176
+ * A rail is only a gate if "it did not run" is itself a failure. In `error`
177
+ * mode the three non-execution shapes below are hard failures; in `warn` mode
178
+ * they are warnings that name exactly what was not checked.
179
+ */
180
+ interface HeadHonestyExecution {
181
+ /** Did the build have a headless renderer at all? */
182
+ rendererAvailable: boolean;
183
+ /** Routes that reached the renderer (the denominator). */
184
+ eligibleRoutes: string[];
185
+ /** Routes that produced a baked-vs-rendered observation (the numerator). */
186
+ observedRoutes: string[];
187
+ /** Routes that never reached the renderer, with why. */
188
+ filteredRoutes: Array<{
189
+ route: string;
190
+ reason: string;
191
+ }>;
192
+ /** Every route in the manifest, for the "0 of N" arithmetic. */
193
+ totalRoutes: number;
194
+ }
195
+ /** Why the rail did not run (empty ⇒ it ran). */
196
+ declare function findHeadHonestyExecutionFaults(ex: HeadHonestyExecution): string[];
197
+ /** Render the "this rail did not run" report — it names what went unchecked. */
198
+ declare function formatHeadHonestyExecutionReport(ex: HeadHonestyExecution, faults: string[]): string;
199
+ /** Thrown in `error` mode when the P1 rail could not run. */
200
+ declare class HeadHonestyNotRunError extends Error {
201
+ readonly execution: HeadHonestyExecution;
202
+ readonly faults: string[];
203
+ constructor(execution: HeadHonestyExecution, faults: string[]);
204
+ }
205
+ /**
206
+ * Assert the rail RAN. Throws {@link HeadHonestyNotRunError} in `error` mode,
207
+ * warns in `warn` mode. Returns `true` when the comparison is worth running
208
+ * (i.e. there is at least one observation to compare).
209
+ */
210
+ declare function assertHeadHonestyExecuted(execution: HeadHonestyExecution, options: {
211
+ mode: HonestyMode;
212
+ }): boolean;
213
+ /**
214
+ * Apply the rail: throw in `error` mode, log in `warn` mode, do nothing in
215
+ * `off`. Returns the violations so callers can report counts.
216
+ */
217
+ declare function assertHeadHonesty(observations: HeadObservation[], options: HeadHonestyOptions & {
218
+ mode: HonestyMode;
219
+ debug?: boolean;
220
+ }): HeadHonestyViolation[];
221
+
222
+ export { type BakedHead as B, type HonestyMode as H, type HeadObservation as a, assertHeadHonesty as b, formatHeadHonestyReport as c, decodeHtmlEntities as d, extractBakedHead as e, findHeadHonestyViolations as f, HeadHonestyError as g, assertHeadHonestyExecuted as h, findHeadHonestyExecutionFaults as i, formatHeadHonestyExecutionReport as j, HeadHonestyNotRunError as k, type HeadHonestyExecution as l, type HonestyModeInput as m, normalizeHeadText as n, type HeadHonestyViolation as o, type HeadHonestyOptions as p, resolveHonestyMode as r };