@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.
- package/README.md +244 -8
- package/dist/chunk-A5F4C72F.js +500 -0
- package/dist/chunk-A5F4C72F.js.map +1 -0
- package/dist/{chunk-F3EIWEZD.js → chunk-HVSF23P7.js} +971 -73
- package/dist/chunk-HVSF23P7.js.map +1 -0
- package/dist/editor/editorBridge.d.ts +53 -1
- package/dist/editor/editorBridge.js +141 -5
- package/dist/editor/editorBridge.js.map +1 -1
- package/dist/headHonesty-OzxvLuwd.d.ts +222 -0
- package/dist/index.d.ts +369 -22
- package/dist/index.js +424 -22
- package/dist/index.js.map +1 -1
- package/dist/installSeoHead-kWQwObez.d.ts +627 -0
- package/dist/plugins/index.d.ts +173 -6
- package/dist/plugins/index.js +628 -54
- package/dist/plugins/index.js.map +1 -1
- package/dist/seo/index.d.ts +763 -4
- package/dist/seo/index.js +2 -2
- package/dist/{vitepressTransform-DfmABXmK.d.ts → vitepressTransform-JG_zlaux.d.ts} +99 -6
- package/package.json +26 -16
- package/src/components/DcsCallButton.test.ts +58 -0
- package/src/components/DcsCallButton.vue +19 -4
- package/src/components/DcsReviewShowcase.vue +5 -1
- package/src/components/LiteMediaEmbed.vue +3 -3
- package/src/components/ManagedImage.test.ts +94 -0
- package/src/components/ManagedImage.vue +58 -6
- package/src/components/PreviewRibbon.vue +4 -1
- package/src/composables/useConversionTracking.test.ts +492 -0
- package/src/composables/useConversionTracking.ts +770 -0
- package/src/composables/useReleaseNotes.ts +7 -1
- package/src/composables/useResponsiveImage.ts +6 -0
- package/src/composables/useSEO.applyHead.test.ts +150 -0
- package/src/composables/useSEO.ts +63 -17
- package/src/composables/useSiteVersion.ts +4 -1
- package/src/composables/useSiteVisitorSession.test.ts +56 -0
- package/src/composables/useSiteVisitorSession.ts +39 -3
- package/src/composables/useTextContent.ts +9 -1
- package/dist/chunk-DAYLLSEE.js +0 -3
- package/dist/chunk-DAYLLSEE.js.map +0 -1
- package/dist/chunk-F3EIWEZD.js.map +0 -1
- package/dist/spliceHeadHtml-CsBEucGy.d.ts +0 -254
package/dist/index.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import * as vue from 'vue';
|
|
2
2
|
import { ComputedRef, MaybeRefOrGetter, Ref } from 'vue';
|
|
3
|
-
import { U as UseSeoReturn, G as GlobalSeoConfig } from './vitepressTransform-
|
|
4
|
-
export { A as AI_BOTS, y as BlogMeta, x as BreadcrumbCrumb, v as BuildLlmsParams, u as BuildRobotsParams, B as BuildSitemapParams, E as ContentConfig, C as CreateSeoTransformPageDataOptions, D as DcsRobotsOptions, F as FaqSource, X as HeadOverrides, P as PageSeoConfig, R as ResolvedPageOverrides, T as ResolvedPageSeo, z as ReviewSource, w as SchemaObject, O as SeoAlternateConfig, I as SeoAuthorConfig, H as SeoConfiguration, K as SeoImagesConfig, L as SeoOpenGraphConfig, S as SeoPageContext, r as SeoPageTypeRule, N as SeoSchemaConfig, J as SeoSocialConfig, M as SeoTwitterConfig, Q as SeoVerificationConfig, W as UseSeoConfig, t as VitePressHeadConfig, V as VitePressPageData, q as absolutizeUrl, j as breadcrumbTrailFromRoute, k as buildBlogPosting, h as buildBreadcrumbList, l as buildFaqPage, g as buildGlobalGraph, f as buildLlmsTxt, m as buildReviewSchemaParts, e as buildRobotsTxt, a as buildSitemapXml, b as buildVitePressSeoHead, c as createSeoTransformPageData, d as defaultRelativePathToRoute, o as filterRealFaq, n as filterRealReviews, p as findReviewItemsForPage, i as isRouteIndexable, s as slugToTitle } from './vitepressTransform-
|
|
5
|
-
export {
|
|
3
|
+
import { U as UseSeoReturn, G as GlobalSeoConfig } from './vitepressTransform-JG_zlaux.js';
|
|
4
|
+
export { A as AI_BOTS, y as BlogMeta, x as BreadcrumbCrumb, v as BuildLlmsParams, u as BuildRobotsParams, B as BuildSitemapParams, E as ContentConfig, C as CreateSeoTransformPageDataOptions, D as DcsRobotsOptions, F as FaqSource, X as HeadOverrides, P as PageSeoConfig, R as ResolvedPageOverrides, T as ResolvedPageSeo, z as ReviewSource, w as SchemaObject, O as SeoAlternateConfig, I as SeoAuthorConfig, H as SeoConfiguration, K as SeoImagesConfig, L as SeoOpenGraphConfig, S as SeoPageContext, r as SeoPageTypeRule, N as SeoSchemaConfig, J as SeoSocialConfig, M as SeoTwitterConfig, Q as SeoVerificationConfig, W as UseSeoConfig, t as VitePressHeadConfig, V as VitePressPageData, q as absolutizeUrl, j as breadcrumbTrailFromRoute, k as buildBlogPosting, h as buildBreadcrumbList, l as buildFaqPage, g as buildGlobalGraph, f as buildLlmsTxt, m as buildReviewSchemaParts, e as buildRobotsTxt, a as buildSitemapXml, b as buildVitePressSeoHead, c as createSeoTransformPageData, d as defaultRelativePathToRoute, o as filterRealFaq, n as filterRealReviews, p as findReviewItemsForPage, i as isRouteIndexable, s as slugToTitle } from './vitepressTransform-JG_zlaux.js';
|
|
5
|
+
export { j as HeadLinkTag, H as HeadMetaTag, k as HeadScriptTag, l as HeadTagOverrides, I as InstallSeoHeadOptions, R as ResolvedHeadTags, o as SeoHeadClientLike, p as SeoHeadEntryLike, q as SeoHeadInput, t as SeoHeadPageRoute, u as SeoHeadPagesManifest, v as SeoHeadResolution, w as SeoHeadResolutionReason, m as SeoHeadRouteLike, S as SeoHeadRouterLike, b as buildHeadTags, h as buildSeoHeadRouteMap, f as escapeJsonLd, g as generateJsonLd, a as generateOpenGraphMeta, c as generateTwitterMeta, i as installSeoHead, n as normalizeSeoHeadPath, d as renderHeadTags, r as resolvePageSeo, s as spliceHeadHtml, e as stripManagedHeadTags } from './installSeoHead-kWQwObez.js';
|
|
6
6
|
import { ImageContext, ResponsiveImageResult } from '@duffcloudservices/cms-core';
|
|
7
7
|
export { ImageContext, ResponsiveImageOptions, ResponsiveImageResult, ResponsiveSource, isCdnAssetUrl, resolveResponsiveImage } from '@duffcloudservices/cms-core';
|
|
8
8
|
|
|
@@ -119,6 +119,12 @@ declare function useTextContent(config: TextContentConfig): TextContentReturn;
|
|
|
119
119
|
* module so that the build-time static-HTML emitter (`dcsSeoPlugin`) produces
|
|
120
120
|
* byte-identical output. This composable is a thin Vue/unhead wrapper over it.
|
|
121
121
|
*
|
|
122
|
+
* THE HEAD-AUTHORITY CONTRACT (C-356). `.dcs/seo.yaml` is the ONLY writer of
|
|
123
|
+
* the managed head fields. `applyHead()` RE-ASSERTS the baked head at runtime;
|
|
124
|
+
* it never AUTHORS one, so it takes no arguments. Full text + reasoning:
|
|
125
|
+
* `.docs/plans/dynamic-site-resolution/README.md` § "The head-authority
|
|
126
|
+
* contract (C-356)".
|
|
127
|
+
*
|
|
122
128
|
* @example
|
|
123
129
|
* ```vue
|
|
124
130
|
* <script setup lang="ts">
|
|
@@ -126,16 +132,15 @@ declare function useTextContent(config: TextContentConfig): TextContentReturn;
|
|
|
126
132
|
*
|
|
127
133
|
* const { applyHead, getSchema, config } = useSEO('home')
|
|
128
134
|
*
|
|
129
|
-
* //
|
|
135
|
+
* // Re-assert the baked head for this route. No arguments — ever.
|
|
130
136
|
* applyHead()
|
|
131
|
-
*
|
|
132
|
-
* // Or customize before applying
|
|
133
|
-
* applyHead({
|
|
134
|
-
* title: 'Custom Override Title',
|
|
135
|
-
* schemas: [...getSchema(), customSchema]
|
|
136
|
-
* })
|
|
137
137
|
* </script>
|
|
138
138
|
* ```
|
|
139
|
+
*
|
|
140
|
+
* Need a different title? Change it in `.dcs/seo.yaml` (or in the portal SEO
|
|
141
|
+
* editor, which writes it). A value hardcoded here is a SECOND writer, and a
|
|
142
|
+
* second writer is a divergence by construction — whether or not today's two
|
|
143
|
+
* values happen to agree.
|
|
139
144
|
*/
|
|
140
145
|
|
|
141
146
|
/**
|
|
@@ -427,6 +432,10 @@ interface UseResponsiveImageOptions {
|
|
|
427
432
|
sizes?: MaybeRefOrGetter<string | undefined>;
|
|
428
433
|
/** Skip variant resolution and use the original URL only. */
|
|
429
434
|
original?: MaybeRefOrGetter<boolean | undefined>;
|
|
435
|
+
/** Intrinsic width — emitted as a layout-shift hint when paired with `height`. */
|
|
436
|
+
width?: MaybeRefOrGetter<number | undefined>;
|
|
437
|
+
/** Intrinsic height — emitted as a layout-shift hint when paired with `width`. */
|
|
438
|
+
height?: MaybeRefOrGetter<number | undefined>;
|
|
430
439
|
}
|
|
431
440
|
/**
|
|
432
441
|
* Reactively resolves responsive image metadata for a DCS CDN URL.
|
|
@@ -473,22 +482,347 @@ interface UseReviewContentReturn {
|
|
|
473
482
|
}
|
|
474
483
|
declare function useReviewContent(config: UseReviewContentConfig): UseReviewContentReturn;
|
|
475
484
|
|
|
485
|
+
/** How a clicked element was classified. */
|
|
486
|
+
type ConversionInteractionType = 'booking' | 'phone' | 'email' | 'form_submit' | 'social' | 'external' | 'internal' | 'button';
|
|
487
|
+
/**
|
|
488
|
+
* The interaction types that ARE the money moment — the ones an owner report counts.
|
|
489
|
+
*
|
|
490
|
+
* Everything else (`social`, `external`, `internal`, `button`) is navigation telemetry and
|
|
491
|
+
* is still captured, but it must never be summed into "conversions". Keeping the set here,
|
|
492
|
+
* rather than in each report's query, means one edit changes every consumer.
|
|
493
|
+
*/
|
|
494
|
+
declare const CONVERSION_INTERACTION_TYPES: readonly ConversionInteractionType[];
|
|
495
|
+
/** Whether an interaction type counts as a conversion. */
|
|
496
|
+
declare function isConversionType(type: ConversionInteractionType): boolean;
|
|
497
|
+
/** A captured conversion event, in App Insights `trackEvent` shape. */
|
|
498
|
+
interface ConversionEvent {
|
|
499
|
+
/** Event name — `site_interaction` by default (see {@link ConversionTrackingOptions.eventName}). */
|
|
500
|
+
name: string;
|
|
501
|
+
/** Flat string properties; App Insights `customDimensions`. */
|
|
502
|
+
properties: {
|
|
503
|
+
interaction_type: ConversionInteractionType;
|
|
504
|
+
/**
|
|
505
|
+
* `'true'` when {@link isConversionType} holds. A string, not a boolean, because App
|
|
506
|
+
* Insights `customDimensions` and GA4 event params are both string maps — so the
|
|
507
|
+
* owner-report query is one predicate (`is_conversion == "true"`) instead of an
|
|
508
|
+
* interaction-type IN-list that every new report has to remember to keep in sync.
|
|
509
|
+
*/
|
|
510
|
+
is_conversion: string;
|
|
511
|
+
/** Visible label / aria-label of the clicked element, truncated. */
|
|
512
|
+
label: string;
|
|
513
|
+
/**
|
|
514
|
+
* Destination, query string and fragment stripped, and REDACTED for contact schemes:
|
|
515
|
+
* a `tel:` / `sms:` / `mailto:` href becomes `tel:#<digest>` — never the raw number or
|
|
516
|
+
* address. See {@link redactHref}. Empty for buttons and form submits.
|
|
517
|
+
*/
|
|
518
|
+
href: string;
|
|
519
|
+
/** URL scheme of the destination including the colon (`tel:`, `https:`), or `''`. */
|
|
520
|
+
href_scheme: string;
|
|
521
|
+
/** Host of the destination, or `''` for buttons, form submits and contact schemes. */
|
|
522
|
+
href_host: string;
|
|
523
|
+
/**
|
|
524
|
+
* Short digest of the contact target (the phone number / email address), or `''` for
|
|
525
|
+
* everything else. Lets a report say "CTA A got 12 taps, CTA B got 3" without ever
|
|
526
|
+
* storing the contact string itself.
|
|
527
|
+
*/
|
|
528
|
+
href_hash: string;
|
|
529
|
+
/** Path of the page the click happened on. */
|
|
530
|
+
page_path: string;
|
|
531
|
+
/** Host of the page the click happened on. */
|
|
532
|
+
host: string;
|
|
533
|
+
/** Schema version, so a report can tell old rows from new ones. */
|
|
534
|
+
capture_version: string;
|
|
535
|
+
};
|
|
536
|
+
}
|
|
537
|
+
/** A telemetry transport. Typically `(e) => telemetry.trackEvent(e)`. */
|
|
538
|
+
type ConversionSink = (event: ConversionEvent) => void;
|
|
539
|
+
interface ConversionTrackingOptions {
|
|
540
|
+
/**
|
|
541
|
+
* Where to send events. Optional on purpose — omit it when the telemetry SDK is
|
|
542
|
+
* deferred, and call {@link attachConversionSink} once it has loaded. Events captured
|
|
543
|
+
* in the meantime are buffered, not dropped.
|
|
544
|
+
*/
|
|
545
|
+
sink?: ConversionSink;
|
|
546
|
+
/**
|
|
547
|
+
* Fire-and-forget transports that receive every event AS WELL AS `sink`, and that do NOT
|
|
548
|
+
* count as "a sink exists" for buffering purposes.
|
|
549
|
+
*
|
|
550
|
+
* This distinction is load-bearing. GA4's `gtag` is a mirror: the deploy injects it as a
|
|
551
|
+
* synchronous inline snippet, so it is either there when the click happens or the hit is
|
|
552
|
+
* genuinely unavailable — there is nothing to wait for. App Insights is a `sink`: it boots
|
|
553
|
+
* on an idle callback minutes later, which is exactly what the buffer exists to survive.
|
|
554
|
+
* Treating GA4 as a `sink` would have satisfied the "do we have somewhere to send this?"
|
|
555
|
+
* test on every site and quietly disabled the deferred-SDK interlock.
|
|
556
|
+
*/
|
|
557
|
+
mirrors?: ConversionSink[];
|
|
558
|
+
/**
|
|
559
|
+
* Custom event name. Defaults to `site_interaction` — the name Just Posh has been
|
|
560
|
+
* emitting since 2026-04, so its history stays one continuous series. Pass
|
|
561
|
+
* `'dcs_conversion'` on a site with no existing history if you prefer the canonical name.
|
|
562
|
+
*/
|
|
563
|
+
eventName?: string;
|
|
564
|
+
/**
|
|
565
|
+
* Extra hostnames to treat as booking destinations, on top of {@link DEFAULT_BOOKING_HOSTS}.
|
|
566
|
+
* Matched on host suffix, so `vagaro.com` also matches `www.vagaro.com`.
|
|
567
|
+
*/
|
|
568
|
+
bookingHosts?: string[];
|
|
569
|
+
/**
|
|
570
|
+
* Same-origin paths that mean "booking" (e.g. a self-hosted `/book`). Matched as a
|
|
571
|
+
* prefix on the pathname.
|
|
572
|
+
*/
|
|
573
|
+
bookingPaths?: string[];
|
|
574
|
+
/** Extra social hostnames on top of {@link DEFAULT_SOCIAL_HOSTS}. */
|
|
575
|
+
socialHosts?: string[];
|
|
576
|
+
/** Max events held while no sink is attached. Default 50 — bounded so a bot cannot grow it. */
|
|
577
|
+
bufferLimit?: number;
|
|
578
|
+
/** Drop untrusted (script-dispatched) clicks. Default `false`. */
|
|
579
|
+
requireTrusted?: boolean;
|
|
580
|
+
/** Document to bind to. Defaults to the ambient `document`. Injected in tests. */
|
|
581
|
+
target?: Document;
|
|
582
|
+
/**
|
|
583
|
+
* Also capture managed-form submissions as `form_submit`. Default `true`.
|
|
584
|
+
*
|
|
585
|
+
* A form submit is a conversion on every DCS site that has a form, and it is the one
|
|
586
|
+
* affordance a click listener alone cannot see honestly: clicking "Send" on a form that
|
|
587
|
+
* then fails validation is not a lead. So the `submit` event — not the click — is
|
|
588
|
+
* authoritative, and clicks on submit controls are deliberately dropped to keep the two
|
|
589
|
+
* from counting the same action twice.
|
|
590
|
+
*/
|
|
591
|
+
captureFormSubmits?: boolean;
|
|
592
|
+
/**
|
|
593
|
+
* Honour the visitor's Do Not Track signal. Default `true`.
|
|
594
|
+
*
|
|
595
|
+
* With DNT on, `start()` binds nothing at all — no listener, no buffer, no event. This
|
|
596
|
+
* is a measurement rail, not a consent platform: if a site ever grows a real consent
|
|
597
|
+
* banner, gate {@link installConversionCapture} on it rather than weakening this.
|
|
598
|
+
*/
|
|
599
|
+
respectDoNotTrack?: boolean;
|
|
600
|
+
/**
|
|
601
|
+
* Bind a second delegated listener even though one is already live on this document.
|
|
602
|
+
*
|
|
603
|
+
* Off by default and it should stay off. Two delegated listeners on one document count
|
|
604
|
+
* every click twice — the same defect class C-288 measured as 52% duplicate page views
|
|
605
|
+
* on a live customer site, which the portal then reported to the owner as traffic. If
|
|
606
|
+
* you are reaching for this, you almost certainly want `stop()` on the existing tracker.
|
|
607
|
+
*/
|
|
608
|
+
force?: boolean;
|
|
609
|
+
}
|
|
610
|
+
/**
|
|
611
|
+
* Schema version stamped on every captured event. Bump on a breaking property change.
|
|
612
|
+
*
|
|
613
|
+
* `2` (C-326): `href` is redacted for `tel:`/`sms:`/`mailto:`, and `is_conversion`,
|
|
614
|
+
* `href_scheme` + `href_hash` were added. Version `1` rows carry raw contact hrefs and no
|
|
615
|
+
* conversion flag, so a report spanning the boundary must branch on this.
|
|
616
|
+
*/
|
|
617
|
+
declare const CONVERSION_CAPTURE_VERSION = "2";
|
|
618
|
+
/**
|
|
619
|
+
* Booking/scheduling vendors seen across the DCS fleet plus the common SMB schedulers.
|
|
620
|
+
* Host-suffix matched. Add per-site extras via `bookingHosts` rather than editing this.
|
|
621
|
+
*/
|
|
622
|
+
declare const DEFAULT_BOOKING_HOSTS: readonly string[];
|
|
623
|
+
/** Social destinations. Host-suffix matched. */
|
|
624
|
+
declare const DEFAULT_SOCIAL_HOSTS: readonly string[];
|
|
625
|
+
/**
|
|
626
|
+
* FNV-1a (32-bit), base36. Synchronous on purpose: this runs inside a click handler, where
|
|
627
|
+
* `crypto.subtle` — the only real hash a browser offers — is async and would force the
|
|
628
|
+
* event to be built after the navigation has already started.
|
|
629
|
+
*
|
|
630
|
+
* BE HONEST ABOUT WHAT THIS IS. It is not anonymisation. A site publishes two or three
|
|
631
|
+
* phone numbers, so anybody holding the site could brute-force the digest back in
|
|
632
|
+
* milliseconds. What it buys is real but narrow: the contact string never lands in an
|
|
633
|
+
* analytics store (GA4 forbids PII in event params outright), a support screenshot of the
|
|
634
|
+
* events table cannot leak a customer's mailbox, and the value is still stable enough to
|
|
635
|
+
* answer "which CTA did they tap". Do not describe it as anything more than that.
|
|
636
|
+
*/
|
|
637
|
+
declare function hashTarget(value: string): string;
|
|
638
|
+
/** The scheme of an href, including the colon (`tel:`, `https:`), or `''`. */
|
|
639
|
+
declare function hrefScheme(href: string, pageHost: string): string;
|
|
640
|
+
/**
|
|
641
|
+
* Split an href into the parts that are safe to emit.
|
|
642
|
+
*
|
|
643
|
+
* For `tel:` / `sms:` / `mailto:` the target is a person's or business's contact string, so
|
|
644
|
+
* it is replaced by `<scheme>#<digest>` and the digest is also surfaced on its own. For
|
|
645
|
+
* everything else the href passes through {@link sanitizeUrl} unchanged — a booking URL's
|
|
646
|
+
* path is the useful part and its query (which CAN carry a name or email) is already gone.
|
|
647
|
+
*/
|
|
648
|
+
declare function redactHref(href: string, pageHost: string): {
|
|
649
|
+
href: string;
|
|
650
|
+
scheme: string;
|
|
651
|
+
hash: string;
|
|
652
|
+
};
|
|
653
|
+
/**
|
|
654
|
+
* Whether this element is the control that submits a form.
|
|
655
|
+
*
|
|
656
|
+
* Clicks on these are dropped so the `submit` event can be the single source of truth —
|
|
657
|
+
* see {@link ConversionTrackingOptions.captureFormSubmits}. Note the HTML default: a
|
|
658
|
+
* `<button>` inside a form with no `type` IS a submit button, which is exactly how every
|
|
659
|
+
* `<DcsForm>` renders its action.
|
|
660
|
+
*/
|
|
661
|
+
declare function isSubmitControl(el: Element): boolean;
|
|
662
|
+
/** Read the visitor's Do Not Track signal across the three places browsers have put it. */
|
|
663
|
+
declare function doNotTrackEnabled(): boolean;
|
|
664
|
+
/**
|
|
665
|
+
* Classify a link.
|
|
666
|
+
*
|
|
667
|
+
* ORDER IS LOAD-BEARING and is asserted by tests. Protocol wins first (a `tel:` href has
|
|
668
|
+
* no host to match on), then booking — *before* the same-origin and social checks — so a
|
|
669
|
+
* self-hosted `/book` path and a booking vendor that also runs a social profile are both
|
|
670
|
+
* attributed to revenue rather than to `internal` / `social`.
|
|
671
|
+
*/
|
|
672
|
+
declare function classifyHref(href: string, opts: {
|
|
673
|
+
pageHost: string;
|
|
674
|
+
bookingHosts: readonly string[];
|
|
675
|
+
bookingPaths: readonly string[];
|
|
676
|
+
socialHosts: readonly string[];
|
|
677
|
+
}): ConversionInteractionType;
|
|
678
|
+
/** The object returned by {@link createConversionTracker}. */
|
|
679
|
+
interface ConversionTracker {
|
|
680
|
+
/** Bind the capture-phase click listener. Idempotent. */
|
|
681
|
+
start(): void;
|
|
682
|
+
/** Unbind and clear the buffer. Idempotent. */
|
|
683
|
+
stop(): void;
|
|
684
|
+
/** Attach (or replace) the sink and immediately drain anything buffered. */
|
|
685
|
+
attachSink(sink: ConversionSink): void;
|
|
686
|
+
/** Events currently held because no sink is attached. Read-only copy. */
|
|
687
|
+
buffered(): ConversionEvent[];
|
|
688
|
+
/** Capture a click target directly. Exposed for tests and manual instrumentation. */
|
|
689
|
+
capture(element: Element): ConversionEvent | null;
|
|
690
|
+
}
|
|
691
|
+
/**
|
|
692
|
+
* Attach a sink to every live tracker and drain their buffers.
|
|
693
|
+
*
|
|
694
|
+
* Call this from a deferred/lazy App Insights loader once the SDK is ready:
|
|
695
|
+
*
|
|
696
|
+
* ```ts
|
|
697
|
+
* const { ApplicationInsights } = await import('@microsoft/applicationinsights-web')
|
|
698
|
+
* const ai = new ApplicationInsights({ config })
|
|
699
|
+
* ai.loadAppInsights()
|
|
700
|
+
* attachConversionSink((e) => ai.trackEvent({ name: e.name, properties: e.properties }))
|
|
701
|
+
* ```
|
|
702
|
+
*
|
|
703
|
+
* @returns how many trackers received the sink.
|
|
704
|
+
*/
|
|
705
|
+
declare function attachConversionSink(sink: ConversionSink): number;
|
|
706
|
+
/**
|
|
707
|
+
* Framework-free conversion tracker. Vue callers usually want
|
|
708
|
+
* {@link useConversionTracking} instead.
|
|
709
|
+
*/
|
|
710
|
+
declare function createConversionTracker(options?: ConversionTrackingOptions): ConversionTracker;
|
|
711
|
+
/**
|
|
712
|
+
* Vue composable: start conversion capture for the lifetime of the calling component.
|
|
713
|
+
*
|
|
714
|
+
* YOU PROBABLY DO NOT NEED THIS ANY MORE. Since C-326, importing `@duffcloudservices/cms`
|
|
715
|
+
* auto-installs a tracker (`installConversionCapture`), so every fleet site captures
|
|
716
|
+
* conversions with no site-repo code at all. Calling this on top of that is a no-op with a
|
|
717
|
+
* loud console warning — `start()` refuses to bind a second listener to a document that
|
|
718
|
+
* already has one, because two listeners double every conversion count.
|
|
719
|
+
*
|
|
720
|
+
* It remains exported for the cases the auto-install deliberately does not cover: a site
|
|
721
|
+
* that needs per-site `bookingHosts`/`bookingPaths`, or a non-cms consumer wiring capture
|
|
722
|
+
* by hand. In the first case, prefer configuring the auto-installer:
|
|
723
|
+
*
|
|
724
|
+
* ```ts
|
|
725
|
+
* import { installConversionCapture } from '@duffcloudservices/cms'
|
|
726
|
+
* installConversionCapture({ bookingHosts: ['stridethera.com'], bookingPaths: ['/book'] })
|
|
727
|
+
* ```
|
|
728
|
+
*
|
|
729
|
+
* Returns the tracker so a caller can attach a sink or inspect the buffer. Safe on the
|
|
730
|
+
* server: with no `document`, `start()` is a no-op.
|
|
731
|
+
*/
|
|
732
|
+
declare function useConversionTracking(options?: ConversionTrackingOptions): ConversionTracker;
|
|
733
|
+
|
|
734
|
+
/**
|
|
735
|
+
* Fleet-wide conversion capture, installed without asking the site.
|
|
736
|
+
*
|
|
737
|
+
* WHY THIS FILE EXISTS
|
|
738
|
+
* --------------------
|
|
739
|
+
* `useConversionTracking` shipped in @duffcloudservices/cms, was exported from the package
|
|
740
|
+
* index, was documented with an example, and was adopted by ZERO of the eleven fleet sites.
|
|
741
|
+
* Two independent reviews on 2026-07-26 measured what that cost (C-326):
|
|
742
|
+
*
|
|
743
|
+
* - Bryan's Handyman Solutions exists to generate phone calls. It ships five `tel:` CTAs
|
|
744
|
+
* — hero, header, mobile icon, sticky call bar, footer — and every one of them fires
|
|
745
|
+
* nothing. `trackEvent` appears in the bundle exactly once: as the composable's own
|
|
746
|
+
* unused export. Its GA4 property was never created either, so the site has no
|
|
747
|
+
* measurement of any kind for the only action it is built to produce.
|
|
748
|
+
* - Kim Duff Homes, a flagship account, has 6 pageViews in App Insights across 365 days.
|
|
749
|
+
*
|
|
750
|
+
* The lesson is not "write better docs". A capability the site must remember to switch on
|
|
751
|
+
* is a capability that is off. So capture is a SIDE EFFECT of importing the package every
|
|
752
|
+
* fleet site already depends on, and it finds its own transport.
|
|
753
|
+
*
|
|
754
|
+
* HOW A SITE'S EVENTS GET OUT — three sinks, in the order they are usually available
|
|
755
|
+
* ---------------------------------------------------------------------------------
|
|
756
|
+
* 1. GA4 (`window.gtag`). The deploy workflow injects the gtag snippet into every built
|
|
757
|
+
* HTML file when `.dcs/site.yaml` carries a `google_analytics_id`. That means a site
|
|
758
|
+
* with GA4 configured captures conversions with LITERALLY no code change — which is
|
|
759
|
+
* precisely why the empty-`google_analytics_id` launch gate is the other half of C-326.
|
|
760
|
+
* 2. `window.__dcsConversionAttach(sink)` — a one-line, import-free contract published by
|
|
761
|
+
* this module for any telemetry transport that boots later. `@duffcloudservices/telemetry`
|
|
762
|
+
* calls it automatically inside `initialize()`; a site with a bespoke App Insights boot
|
|
763
|
+
* adds the single line. Events captured before that arrive are buffered, not lost.
|
|
764
|
+
* 3. `attachConversionSink(sink)` — the direct API, for callers holding the import.
|
|
765
|
+
*
|
|
766
|
+
* WHERE IT DELIBERATELY DOES NOTHING
|
|
767
|
+
* ----------------------------------
|
|
768
|
+
* - No `document` (SSR, VitePress build, the Playwright prerender pass).
|
|
769
|
+
* - Inside an iframe — the DCS visual editor and preview surfaces frame the site, and
|
|
770
|
+
* an editor clicking around a customer's page is not a lead.
|
|
771
|
+
* - Do Not Track is on.
|
|
772
|
+
* - The page opted out via `window.__dcsConversionOptOut = true` or
|
|
773
|
+
* `<html data-dcs-analytics="off">`.
|
|
774
|
+
*
|
|
775
|
+
* In every one of those cases it binds no listener and buffers nothing.
|
|
776
|
+
*/
|
|
777
|
+
|
|
778
|
+
/** Options for {@link installConversionCapture}. */
|
|
779
|
+
interface InstallConversionCaptureOptions extends Omit<ConversionTrackingOptions, 'sink' | 'mirrors' | 'force'> {
|
|
780
|
+
/**
|
|
781
|
+
* Emit through GA4's `window.gtag` when it is present. Default `true`.
|
|
782
|
+
*
|
|
783
|
+
* Looked up at emit time, not install time: the gtag snippet is `async`, so on a fast
|
|
784
|
+
* connection the site's JS can install this before Google's script has defined `gtag`.
|
|
785
|
+
*/
|
|
786
|
+
gtag?: boolean;
|
|
787
|
+
}
|
|
788
|
+
/** Whether the page has explicitly asked not to be measured. */
|
|
789
|
+
declare function conversionOptedOut(): boolean;
|
|
476
790
|
/**
|
|
477
|
-
*
|
|
791
|
+
* Whether this document is being rendered inside a frame.
|
|
478
792
|
*
|
|
479
|
-
*
|
|
480
|
-
*
|
|
481
|
-
*
|
|
793
|
+
* The DCS portal embeds customer sites in an iframe for the visual editor and preview
|
|
794
|
+
* (`dcsEditorPlugin` keys off the same `window.parent !== window` test). Counting an
|
|
795
|
+
* editor's clicks as customer conversions would inflate exactly the number the owner
|
|
796
|
+
* report is supposed to make trustworthy.
|
|
797
|
+
*/
|
|
798
|
+
declare function inFrame(): boolean;
|
|
799
|
+
/** Reason the installer declined, or `null` when it installed. */
|
|
800
|
+
type ConversionInstallSkipReason = 'no-document' | 'already-installed' | 'in-frame' | 'do-not-track' | 'opted-out';
|
|
801
|
+
/** Result of an {@link installConversionCapture} call. */
|
|
802
|
+
interface ConversionInstallResult {
|
|
803
|
+
installed: boolean;
|
|
804
|
+
reason: ConversionInstallSkipReason | null;
|
|
805
|
+
tracker: ConversionTracker | null;
|
|
806
|
+
}
|
|
807
|
+
/**
|
|
808
|
+
* Install fleet conversion capture. Idempotent; safe to call from anywhere, any number of
|
|
809
|
+
* times, on the server or in a frame.
|
|
482
810
|
*
|
|
483
|
-
*
|
|
484
|
-
*
|
|
811
|
+
* Called for its side effect from the package index, so a site gets this by importing
|
|
812
|
+
* `@duffcloudservices/cms` at all. Call it directly only to pass per-site configuration
|
|
813
|
+
* (extra `bookingHosts`, a self-hosted `bookingPaths`) — do that BEFORE the first click,
|
|
814
|
+
* i.e. at app entry, since the first call wins.
|
|
815
|
+
*/
|
|
816
|
+
declare function installConversionCapture(options?: InstallConversionCaptureOptions): ConversionInstallResult;
|
|
817
|
+
/** The installed tracker, or `null`. Exposed for tests and diagnostics. */
|
|
818
|
+
declare function installedConversionTracker(): ConversionTracker | null;
|
|
819
|
+
/**
|
|
820
|
+
* Tear the auto-installed tracker down and clear the global flags.
|
|
485
821
|
*
|
|
486
|
-
*
|
|
487
|
-
*
|
|
488
|
-
* `console.error`/`console.warn` on every signed-out page load is exactly the
|
|
489
|
-
* console-noise this fixes. A genuine signed-in visitor is returned as a
|
|
490
|
-
* normalized `SiteVisitor`; anything else resolves to `null`.
|
|
822
|
+
* Exists for tests and for a site that genuinely needs to re-install with different
|
|
823
|
+
* options. Not part of the normal runtime path.
|
|
491
824
|
*/
|
|
825
|
+
declare function resetConversionCapture(): void;
|
|
492
826
|
|
|
493
827
|
/** A signed-in site visitor. */
|
|
494
828
|
interface SiteVisitor {
|
|
@@ -503,6 +837,14 @@ interface SiteVisitorSessionResult {
|
|
|
503
837
|
visitor: SiteVisitor | null;
|
|
504
838
|
/** Convenience flag — `true` iff a visitor is present. */
|
|
505
839
|
authenticated: boolean;
|
|
840
|
+
/**
|
|
841
|
+
* `true` when the probe did not reach the platform API at all — the response carried a
|
|
842
|
+
* non-JSON body (C-298 layer 2), i.e. this host does not route `/api/v1/*` to the API.
|
|
843
|
+
* Distinct from an ordinary signed-out visit: a site can render "sign-in temporarily
|
|
844
|
+
* unavailable" instead of a login button that cannot possibly work. Absent/`false` on
|
|
845
|
+
* every normal path, so existing consumers are unaffected.
|
|
846
|
+
*/
|
|
847
|
+
apiUnreachable?: boolean;
|
|
506
848
|
}
|
|
507
849
|
interface FetchSiteVisitorSessionOptions {
|
|
508
850
|
/** API base (default `/api/v1`, same-origin via Front Door). */
|
|
@@ -527,6 +869,11 @@ interface UseSiteVisitorSessionReturn {
|
|
|
527
869
|
isAuthenticated: ComputedRef<boolean>;
|
|
528
870
|
/** `true` while a probe is in flight. */
|
|
529
871
|
isLoading: Ref<boolean>;
|
|
872
|
+
/**
|
|
873
|
+
* `true` when the last probe did not reach the platform API (non-JSON body — C-298
|
|
874
|
+
* layer 2). Render "sign-in unavailable" rather than a login button that cannot work.
|
|
875
|
+
*/
|
|
876
|
+
apiUnreachable: Ref<boolean>;
|
|
530
877
|
/** Re-run the probe. */
|
|
531
878
|
refresh: () => Promise<void>;
|
|
532
879
|
}
|
|
@@ -536,4 +883,4 @@ interface UseSiteVisitorSessionReturn {
|
|
|
536
883
|
*/
|
|
537
884
|
declare function useSiteVisitorSession(options?: UseSiteVisitorSessionOptions): UseSiteVisitorSessionReturn;
|
|
538
885
|
|
|
539
|
-
export { type DcsContentFile, type FetchSiteVisitorSessionOptions, GlobalSeoConfig, type MediaCarouselItem, type ReleaseNote, type ReleaseNotesReturn, type ReviewItem, type SiteVersionReturn, type SiteVisitor, type SiteVisitorSessionResult, type TextContentConfig, type TextContentReturn, type UseMediaCarouselConfig, type UseMediaCarouselReturn, type UseReviewContentConfig, type UseReviewContentReturn, UseSeoReturn, type UseSiteVisitorSessionOptions, type UseSiteVisitorSessionReturn, createSiteSEO, fetchSiteVisitorSession, useMediaCarousel, useReleaseNotes, useResponsiveImage, useReviewContent, useSEO, useSiteVersion, useSiteVisitorSession, useTextContent };
|
|
886
|
+
export { CONVERSION_CAPTURE_VERSION, CONVERSION_INTERACTION_TYPES, type ConversionEvent, type ConversionInstallResult, type ConversionInstallSkipReason, type ConversionInteractionType, type ConversionSink, type ConversionTracker, type ConversionTrackingOptions, DEFAULT_BOOKING_HOSTS, DEFAULT_SOCIAL_HOSTS, type DcsContentFile, type FetchSiteVisitorSessionOptions, GlobalSeoConfig, type InstallConversionCaptureOptions, type MediaCarouselItem, type ReleaseNote, type ReleaseNotesReturn, type ReviewItem, type SiteVersionReturn, type SiteVisitor, type SiteVisitorSessionResult, type TextContentConfig, type TextContentReturn, type UseMediaCarouselConfig, type UseMediaCarouselReturn, type UseReviewContentConfig, type UseReviewContentReturn, UseSeoReturn, type UseSiteVisitorSessionOptions, type UseSiteVisitorSessionReturn, attachConversionSink, classifyHref, conversionOptedOut, createConversionTracker, createSiteSEO, doNotTrackEnabled, fetchSiteVisitorSession, hashTarget, hrefScheme, inFrame, installConversionCapture, installedConversionTracker, isConversionType, isSubmitControl, redactHref, resetConversionCapture, useConversionTracking, useMediaCarousel, useReleaseNotes, useResponsiveImage, useReviewContent, useSEO, useSiteVersion, useSiteVisitorSession, useTextContent };
|