@duffcloudservices/cms 0.12.0 → 0.13.1
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 +13 -1
- package/dist/editor/editorBridge.js +75 -5
- package/dist/editor/editorBridge.js.map +1 -1
- package/dist/headHonesty-OzxvLuwd.d.ts +222 -0
- package/dist/index.d.ts +365 -22
- package/dist/index.js +421 -21
- package/dist/index.js.map +1 -1
- package/dist/installSeoHead-kWQwObez.d.ts +627 -0
- package/dist/plugins/index.d.ts +90 -6
- package/dist/plugins/index.js +530 -49
- 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 +17 -6
- package/src/components/DcsCallButton.test.ts +58 -0
- package/src/components/DcsCallButton.vue +19 -4
- package/src/components/LiteMediaEmbed.vue +3 -3
- package/src/components/ManagedImage.test.ts +34 -0
- package/src/components/ManagedImage.vue +5 -0
- package/src/components/PreviewRibbon.vue +25 -6
- package/src/components/raw-source-imports.test.ts +44 -0
- 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/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
|
@@ -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 & 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 };
|
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
|
/**
|
|
@@ -477,22 +482,347 @@ interface UseReviewContentReturn {
|
|
|
477
482
|
}
|
|
478
483
|
declare function useReviewContent(config: UseReviewContentConfig): UseReviewContentReturn;
|
|
479
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;
|
|
480
790
|
/**
|
|
481
|
-
*
|
|
791
|
+
* Whether this document is being rendered inside a frame.
|
|
482
792
|
*
|
|
483
|
-
*
|
|
484
|
-
*
|
|
485
|
-
*
|
|
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.
|
|
486
810
|
*
|
|
487
|
-
*
|
|
488
|
-
*
|
|
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.
|
|
489
821
|
*
|
|
490
|
-
*
|
|
491
|
-
*
|
|
492
|
-
* `console.error`/`console.warn` on every signed-out page load is exactly the
|
|
493
|
-
* console-noise this fixes. A genuine signed-in visitor is returned as a
|
|
494
|
-
* 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.
|
|
495
824
|
*/
|
|
825
|
+
declare function resetConversionCapture(): void;
|
|
496
826
|
|
|
497
827
|
/** A signed-in site visitor. */
|
|
498
828
|
interface SiteVisitor {
|
|
@@ -507,6 +837,14 @@ interface SiteVisitorSessionResult {
|
|
|
507
837
|
visitor: SiteVisitor | null;
|
|
508
838
|
/** Convenience flag — `true` iff a visitor is present. */
|
|
509
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;
|
|
510
848
|
}
|
|
511
849
|
interface FetchSiteVisitorSessionOptions {
|
|
512
850
|
/** API base (default `/api/v1`, same-origin via Front Door). */
|
|
@@ -531,6 +869,11 @@ interface UseSiteVisitorSessionReturn {
|
|
|
531
869
|
isAuthenticated: ComputedRef<boolean>;
|
|
532
870
|
/** `true` while a probe is in flight. */
|
|
533
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>;
|
|
534
877
|
/** Re-run the probe. */
|
|
535
878
|
refresh: () => Promise<void>;
|
|
536
879
|
}
|
|
@@ -540,4 +883,4 @@ interface UseSiteVisitorSessionReturn {
|
|
|
540
883
|
*/
|
|
541
884
|
declare function useSiteVisitorSession(options?: UseSiteVisitorSessionOptions): UseSiteVisitorSessionReturn;
|
|
542
885
|
|
|
543
|
-
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 };
|