@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
@@ -1,6 +1,7 @@
1
1
  import { Plugin, HtmlTagDescriptor } from 'vite';
2
- import { D as DcsRobotsOptions, Y as PageRouteEntry, H as SeoConfiguration, E as ContentConfig } from '../vitepressTransform-DfmABXmK.js';
3
- export { C as CreateSeoTransformPageDataOptions, R as ResolvedPageOverrides, S as SeoPageContext, r as SeoPageTypeRule, t as VitePressHeadConfig, V as VitePressPageData, b as buildVitePressSeoHead, c as createSeoTransformPageData, d as defaultRelativePathToRoute } from '../vitepressTransform-DfmABXmK.js';
2
+ import { D as DcsRobotsOptions, Y as SeoHonestyBlock, Z as PageRouteEntry, H as SeoConfiguration, E as ContentConfig } from '../vitepressTransform-JG_zlaux.js';
3
+ export { C as CreateSeoTransformPageDataOptions, R as ResolvedPageOverrides, S as SeoPageContext, r as SeoPageTypeRule, t as VitePressHeadConfig, V as VitePressPageData, b as buildVitePressSeoHead, c as createSeoTransformPageData, d as defaultRelativePathToRoute } from '../vitepressTransform-JG_zlaux.js';
4
+ import { H as HonestyMode } from '../headHonesty-OzxvLuwd.js';
4
5
  import { Component, Plugin as Plugin$1 } from 'vue';
5
6
  import MarkdownIt from 'markdown-it';
6
7
 
@@ -146,6 +147,14 @@ interface DcsSeoPluginOptions {
146
147
  * Routes that should receive `robots: noindex, nofollow`. Matched against the
147
148
  * route `path` OR `slug`. The home route (`/`) still overwrites
148
149
  * `dist/index.html`.
150
+ *
151
+ * This is a PREDICATE OVER `pages.yaml`, not a standalone directive about a
152
+ * URL. An entry that matches no page is a **build error**
153
+ * (`NoindexOrphanError`, C-416) — with no matching page nothing is emitted for
154
+ * it, so the entry would silently do nothing while the URL stayed
155
+ * `index, follow`. Matched raw, so `'/account/'` does not match a `'/account'`
156
+ * route; matched against the full manifest, so an entry that is also in
157
+ * `exclude` is fine.
149
158
  */
150
159
  noindex?: string[];
151
160
  /**
@@ -177,6 +186,63 @@ interface DcsSeoPluginOptions {
177
186
  preview?: boolean;
178
187
  /** Emit `dist/llms.txt` (default `true`; always off in preview mode). */
179
188
  llms?: boolean;
189
+ /**
190
+ * P1 — assert that each route's BAKED `<title>`/description equals what the
191
+ * app leaves in the `<head>` after it mounts. Rides the prerender browser, so
192
+ * the marginal cost is one `page.evaluate()` per route.
193
+ *
194
+ * Env override: `DCS_SEO_HEAD_HONESTY=error|warn|off`.
195
+ * seo.yaml override: `headHonesty: warn` or `headHonesty: { mode, allow }`.
196
+ */
197
+ headHonesty?: boolean | HonestyMode | SeoHonestyBlock;
198
+ /**
199
+ * P2 — assert that every URL the factory publishes (JSON-LD logo/image IRIs,
200
+ * `og:image`, icon links, sitemap `<loc>`s, llms.txt links) resolves to what it
201
+ * promises. Same-origin assets are proven against `dist/` with no network;
202
+ * cross-origin assets are probed under a cache + time budget and only a
203
+ * DEFINITIVE wrong answer fails the build.
204
+ *
205
+ * Env override: `DCS_SEO_URL_HONESTY=error|warn|off`,
206
+ * `DCS_SEO_URL_HONESTY_NETWORK=off` to skip the cross-origin probes.
207
+ */
208
+ urlHonesty?: boolean | HonestyMode | SeoHonestyBlock;
209
+ /**
210
+ * P12 — hoist `<meta charset>` to the top of `<head>` and assert it lands
211
+ * inside the spec's 1024-byte encoding-sniffing window.
212
+ *
213
+ * Env override: `DCS_SEO_CHARSET_BUDGET=error|warn|off`.
214
+ */
215
+ charsetBudget?: boolean | HonestyMode | SeoHonestyBlock;
216
+ }
217
+ /**
218
+ * Every key a `noindex` entry can legitimately match.
219
+ *
220
+ * The plugin's contract for `noindex` is a PREDICATE OVER THE ROUTE MANIFEST,
221
+ * never a directive about a bare URL. Every consumer asks the question
222
+ * route-first and identically —
223
+ * `noindexSet.has(route.path) || (route.slug && noindexSet.has(route.slug))` —
224
+ * in {@link emitStaticSeoHtml}, in the sitemap/llms `isRouteIndexable`
225
+ * predicate, and in the baked `__DCS_PAGES__` manifest the runtime re-assert
226
+ * reads. So the set of entries that can ever do anything is exactly the union
227
+ * of the manifest's paths and slugs, and this function is the exact inverse of
228
+ * that same relation rather than a second, drifting implementation of it.
229
+ */
230
+ declare function collectNoindexMatchKeys(routes: PageRouteEntry[]): Set<string>;
231
+ /**
232
+ * `noindex` entries that match no route in the manifest — i.e. entries that
233
+ * are, today, silent no-ops.
234
+ *
235
+ * Matched with the SAME raw string equality the consumers use, deliberately:
236
+ * a `'/account/'` entry against a `'/account'` route does not match in
237
+ * `emitStaticSeoHtml`, so it must not "match" here either, or the check would
238
+ * bless the very entry the emitter is about to ignore.
239
+ */
240
+ declare function findOrphanNoindexEntries(noindex: string[], routes: PageRouteEntry[]): string[];
241
+ /** Thrown when a `noindex` entry names nothing in `pages.yaml` (C-416). */
242
+ declare class NoindexOrphanError extends Error {
243
+ readonly orphans: string[];
244
+ readonly knownKeys: string[];
245
+ constructor(orphans: string[], knownKeys: string[]);
180
246
  }
181
247
  /**
182
248
  * Core site-file emitter, separated from Vite so it is unit-testable.
@@ -483,6 +549,89 @@ declare function dcsCdnBuildEnd(options?: DcsCdnBuildEndOptions): (siteConfig: {
483
549
 
484
550
  declare function responsiveImagePlugin(md: MarkdownIt): void;
485
551
 
552
+ /**
553
+ * Build-time responsive-variant lint for DCS customer sites.
554
+ *
555
+ * The 2026-07-03 flagship-excellence review (P7) found desktop pages shipping
556
+ * un-suffixed **full-size** CDN images (e.g. a 321.9 KB hero `47af1e8f….webp`)
557
+ * instead of a `-md`/`-lg` responsive step. That happens when an image reaches
558
+ * the output HTML through a path that does NOT run the `<img>` → `<picture>`
559
+ * transform — content injected via `__DCS_CONTENT__` and then swapped to the
560
+ * base `cdnUrl` by `dcsCdnBuildEnd`, or a hand-authored `<img src>` pointing at
561
+ * a base CDN asset URL.
562
+ *
563
+ * A DCS CDN asset URL has the shape
564
+ * `https://files.<host>/[content/]<slug>/assets/[<dir>/]<uuid>.<ext>`
565
+ * Responsive variants insert a `-sm`/`-md`/`-lg` suffix before the extension
566
+ * (`…<uuid>-md.webp`). Those suffixes contain non-hex letters, so a URL that
567
+ * still matches the *base* pattern below is, by definition, an un-suffixed
568
+ * full-size asset. This lint scans the built HTML's `<img src>` and
569
+ * `<source srcset>` attributes for such URLs and reports (or fails on) them.
570
+ *
571
+ * It is intentionally a **static-output** lint (VitePress generates HTML after
572
+ * both Vite builds finish, so a Vite plugin hook cannot see the SSG output) and
573
+ * is registered via VitePress `buildEnd`.
574
+ *
575
+ * @example
576
+ * ```ts
577
+ * // .vitepress/config.ts
578
+ * import { dcsCdnBuildEnd, dcsResponsiveImageLint, chainBuildEnd } from '@duffcloudservices/cms/plugins'
579
+ *
580
+ * export default defineConfig({
581
+ * // run the CDN rewrite first, then lint the result
582
+ * buildEnd: chainBuildEnd(dcsCdnBuildEnd(), dcsResponsiveImageLint()),
583
+ * })
584
+ * ```
585
+ */
586
+ interface ResponsiveImageViolation {
587
+ /** The un-suffixed full-size CDN URL that should have been a responsive variant. */
588
+ url: string;
589
+ /** Which element referenced it. */
590
+ tag: 'img' | 'source';
591
+ }
592
+ /** True when `url` is a base (un-suffixed, full-size) DCS CDN asset URL. */
593
+ declare function isFullSizeCdnUrl(url: string): boolean;
594
+ /**
595
+ * Scan an HTML string and return every `<img src>` / `<source srcset>` reference
596
+ * that resolves to a base (un-suffixed) full-size DCS CDN asset. `srcset`
597
+ * candidates are split on commas and only the URL portion (before the width /
598
+ * density descriptor) is tested. `onerror` and other attributes are ignored —
599
+ * only the actual `src` / `srcset` sources are inspected.
600
+ */
601
+ declare function findFullSizeCdnImages(html: string): ResponsiveImageViolation[];
602
+ interface DcsResponsiveImageLintOptions {
603
+ /**
604
+ * Throw (fail the build) when full-size images are found. Default `false`
605
+ * (warn only) so the guard can land ahead of the per-site cleanup; flip to
606
+ * `true` in CI once a site's output is clean to prevent regressions.
607
+ */
608
+ failOnViolation?: boolean;
609
+ /** File extensions to scan in the output directory. Default `['.html']`. */
610
+ extensions?: string[];
611
+ /** Enable per-file debug logging. */
612
+ debug?: boolean;
613
+ }
614
+ interface ResponsiveImageLintReport {
615
+ filesScanned: number;
616
+ violations: Array<ResponsiveImageViolation & {
617
+ file: string;
618
+ }>;
619
+ }
620
+ /**
621
+ * VitePress `buildEnd` factory that lints the generated HTML for un-suffixed
622
+ * full-size CDN images. Returns the report (also useful for tests) and either
623
+ * warns or throws depending on `failOnViolation`.
624
+ */
625
+ declare function dcsResponsiveImageLint(options?: DcsResponsiveImageLintOptions): (siteConfig: {
626
+ outDir: string;
627
+ }) => Promise<ResponsiveImageLintReport>;
628
+ /**
629
+ * Compose multiple VitePress `buildEnd` hooks into one (VitePress accepts a
630
+ * single `buildEnd`). Hooks run sequentially in the order given, so a rewrite
631
+ * hook (`dcsCdnBuildEnd`) can precede a lint (`dcsResponsiveImageLint`).
632
+ */
633
+ declare function chainBuildEnd<T>(...hooks: Array<((siteConfig: T) => unknown | Promise<unknown>) | undefined | null>): (siteConfig: T) => Promise<void>;
634
+
486
635
  /**
487
636
  * DCS motion tokens — the customer-site copy of the shared motion vocabulary.
488
637
  *
@@ -500,14 +649,32 @@ declare function responsiveImagePlugin(md: MarkdownIt): void;
500
649
  */
501
650
  declare const DCS_MOTION_TOKENS_CSS = ":root {\n --motion-duration-stagger: 40ms;\n --motion-duration-micro: 80ms;\n --motion-duration-quick: 150ms;\n --motion-duration-fast: 250ms;\n --motion-duration-medium: 350ms;\n --motion-duration-slow: 400ms;\n --motion-duration-very-slow: 500ms;\n --motion-ease-out: cubic-bezier(0.22, 1, 0.36, 1);\n --motion-ease-in-out: cubic-bezier(0.65, 0, 0.35, 1);\n --motion-ease-linear: linear;\n --motion-ease-bounce: cubic-bezier(0.34, 1.36, 0.64, 1);\n --motion-ease-bounce-strong: cubic-bezier(0.34, 3.85, 0.64, 1);\n --motion-distance-xs: 4px;\n --motion-distance-sm: 8px;\n --motion-distance-md: 12px;\n --motion-distance-lg: 24px;\n --motion-scale-lg: 0.96;\n --motion-scale-md: 0.97;\n --motion-scale-sm: 0.98;\n --motion-scale-xs: 0.99;\n --motion-blur-sm: 2px;\n --motion-blur-md: 3px;\n --motion-blur-lg: 8px;\n}";
502
651
  /**
503
- * The `<head>` tag descriptor for the motion-token block. `head-prepend` keeps
504
- * it above the site's own bundled CSS so a per-site `:root` override still wins.
652
+ * The `<head>` tag descriptor for the motion-token block.
653
+ *
654
+ * `injectTo: 'head'`, NOT `'head-prepend'` — C-414. Vite's `head-prepend`
655
+ * inserts immediately after the `<head>` open tag, i.e. AHEAD of the site's own
656
+ * `<meta charset>`. This block is 858 bytes, so it pushed the encoding
657
+ * declaration to byte 910 of the HTML spec's 1024-byte encoding-sniffing window
658
+ * on every site in the fleet (measured on Iron Oak and Bryan's 2026-07-27, and
659
+ * re-measured identical here), leaving ~90 bytes of headroom before every DCS
660
+ * site silently mojibakes.
661
+ *
662
+ * `'head'` appends to the END of the `<head>` the site authored — which is
663
+ * still ABOVE Vite's own build-time `<link rel=stylesheet>` / entry `<script>`,
664
+ * because those are injected AFTER this `order: 'pre'` hook's tags. So the
665
+ * per-site `:root` override still wins the cascade (that was the original
666
+ * reason for `head-prepend`) AND the charset lands at byte ~46 with ~954 bytes
667
+ * of headroom. Measured, not assumed — see `verify-honesty-rails.mjs` P12.
668
+ *
669
+ * If you ever move this back to `head-prepend`, the P12 rail in
670
+ * `scripts/verify-honesty-rails.mjs` will fail: it asserts the charset comes
671
+ * FIRST straight out of `dcsContentPlugin`, with no `dcsSeoPlugin` hoist.
505
672
  */
506
673
  declare function dcsMotionTokensStyleTag(): HtmlTagDescriptor;
507
674
 
508
675
  /**
509
676
  * DCS AI-assistant widget loader — the cms head-inject seam for Phase 3 of the
510
- * Managed Site AI Agents plan (.docs/plans/managed-site-ai-agents/index.html).
677
+ * Managed Site AI Agents plan (.docs/archive/plans/managed-site-ai-agents/plan.md).
511
678
  *
512
679
  * This rides the same always-on `dcsContentPlugin.transformIndexHtml` seam that
513
680
  * ships the motion tokens: every DCS site already wires `dcsContentPlugin`, so a
@@ -577,4 +744,4 @@ interface DcsAgentWidgetTagOptions {
577
744
  */
578
745
  declare function dcsAgentWidgetLoaderTag(options: DcsAgentWidgetTagOptions): HtmlTagDescriptor | null;
579
746
 
580
- export { DCS_MOTION_TOKENS_CSS, DEFAULT_AGENT_API_BASE_URL, DEFAULT_AGENT_WIDGET_SCRIPT_URL, type DcsAgentWidgetLoaderOptions, type DcsAgentWidgetTagOptions, type DcsCdnBuildEndOptions, type DcsCdnImagePluginOptions, type DcsContentPluginOptions, type DcsEditorPluginOptions, type DcsPreviewPluginOptions, type DcsSeoPluginOptions, buildAgentWidgetLoaderScript, dcsAgentWidgetLoaderTag, dcsCdnBuildEnd, dcsCdnImagePlugin, dcsContentPlugin, dcsEditorPlugin, dcsMotionTokensStyleTag, dcsPreviewPlugin, dcsSeoPlugin, emitSiteFiles, emitStaticSeoHtml, readSiteSlug, responsiveImagePlugin };
747
+ export { DCS_MOTION_TOKENS_CSS, DEFAULT_AGENT_API_BASE_URL, DEFAULT_AGENT_WIDGET_SCRIPT_URL, type DcsAgentWidgetLoaderOptions, type DcsAgentWidgetTagOptions, type DcsCdnBuildEndOptions, type DcsCdnImagePluginOptions, type DcsContentPluginOptions, type DcsEditorPluginOptions, type DcsPreviewPluginOptions, type DcsResponsiveImageLintOptions, type DcsSeoPluginOptions, NoindexOrphanError, type ResponsiveImageLintReport, type ResponsiveImageViolation, buildAgentWidgetLoaderScript, chainBuildEnd, collectNoindexMatchKeys, dcsAgentWidgetLoaderTag, dcsCdnBuildEnd, dcsCdnImagePlugin, dcsContentPlugin, dcsEditorPlugin, dcsMotionTokensStyleTag, dcsPreviewPlugin, dcsResponsiveImageLint, dcsSeoPlugin, emitSiteFiles, emitStaticSeoHtml, findFullSizeCdnImages, findOrphanNoindexEntries, isFullSizeCdnUrl, readSiteSlug, responsiveImagePlugin };