import * as vue from 'vue'; import { ComputedRef, MaybeRefOrGetter, Ref } from 'vue'; import { U as UseSeoReturn, G as GlobalSeoConfig } from './vitepressTransform-CZ_IB3dq.js'; 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-CZ_IB3dq.js'; export { l as HeadLinkTag, H as HeadMetaTag, m as HeadScriptTag, o as HeadTagOverrides, I as InstallSeoHeadOptions, R as ResolvedHeadTags, q as SeoHeadClientLike, t as SeoHeadEntryLike, u as SeoHeadInput, v as SeoHeadPageRoute, w as SeoHeadPagesManifest, x as SeoHeadResolution, y as SeoHeadResolutionReason, p as SeoHeadRouteLike, S as SeoHeadRouterLike, b as buildHeadTags, k as buildSeoHeadRouteMap, f as escapeJsonLd, g as generateJsonLd, a as generateOpenGraphMeta, c as generateTwitterMeta, j as installSeoHead, h as isManagedLinkRel, i as isManagedMetaTag, n as normalizeSeoHeadPath, d as renderHeadTags, r as resolvePageSeo, s as spliceHeadHtml, e as stripManagedHeadTags } from './installSeoHead-CuVdVCis.js'; import { ImageContext, ResponsiveImageResult } from '@duffcloudservices/cms-core'; export { ImageContext, ResponsiveImageOptions, ResponsiveImageResult, ResponsiveSource, isCdnAssetUrl, resolveResponsiveImage } from '@duffcloudservices/cms-core'; /** * Types for .dcs/content.yaml structure */ /** * Root structure of .dcs/content.yaml */ interface DcsContentFile { /** Schema version */ version: number; /** ISO timestamp of last update */ lastUpdated: string; /** Email or identifier of who made the update */ updatedBy?: string; /** Global text content shared across all pages */ global?: Record; /** Page-specific text content keyed by page slug */ pages?: Record>; } /** * Configuration for useTextContent composable */ interface TextContentConfig { /** Page slug matching entry in content.yaml */ pageSlug: string; /** Default text values for all keys used by this page */ defaults: Record; /** Whether to fetch runtime overrides (default: true, but only if mode is 'runtime') */ fetchOnMount?: boolean; /** Custom cache key for deduplication */ cacheKey?: string; /** Cache TTL in milliseconds (default: 60000) */ cacheTtl?: number; } /** * Return type of useTextContent composable */ interface TextContentReturn { /** Get text by key with optional fallback */ t: (key: string, fallback?: string) => string; /** Get array of objects from indexed keys (e.g., items.1.title, items.2.title) */ getArray: (arrayKey: string) => Array & { _index: number; }>; /** All resolved texts (defaults merged with overrides) */ texts: vue.ComputedRef>; /** Raw overrides from build-time or API */ overrides: vue.Ref>; /** Loading state for runtime fetch */ isLoading: vue.Ref; /** Error message if fetch failed */ error: vue.Ref; /** Manually refresh overrides */ refresh: () => Promise; /** Check if a key has an override */ hasOverride: (key: string) => boolean; /** Whether build-time content was available */ hasBuildTimeContent: boolean; /** Current mode: 'commit' (build-time) or 'runtime' (API fetch) */ mode: 'commit' | 'runtime'; } /** * useTextContent Composable * * Provides text content management with build-time injection support and optional * runtime API overrides for DCS-managed customer sites. * * Content resolution order: * 1. Runtime API overrides (premium tier only, if mode is 'runtime') * 2. Build-time content from .dcs/content.yaml (injected via dcsContentPlugin) * 3. Hardcoded defaults passed to the composable * * @example * ```vue * * * * ``` */ /** * useTextContent composable for DCS-managed text content. * * @param config - Configuration object * @returns Text content helpers and state */ declare function useTextContent(config: TextContentConfig): TextContentReturn; /** * useSEO Composable * * Provides SEO configuration with build-time injection support from .dcs/seo.yaml. * Generates meta tags, Open Graph, Twitter Cards, and JSON-LD structured data. * * The actual tag resolution lives in the framework-agnostic `../seo/headTags` * module so that the build-time static-HTML emitter (`dcsSeoPlugin`) produces * byte-identical output. This composable is a thin Vue/unhead wrapper over it. * * THE HEAD-AUTHORITY CONTRACT (C-356). `.dcs/seo.yaml` is the ONLY writer of * the managed head fields. `applyHead()` RE-ASSERTS the baked head at runtime; * it never AUTHORS one, so it takes no arguments. Full text + reasoning: * `.docs/plans/dynamic-site-resolution/README.md` § "The head-authority * contract (C-356)". * * @example * ```vue * * ``` * * Need a different title? Change it in `.dcs/seo.yaml` (or in the portal SEO * editor, which writes it). A value hardcoded here is a SECOND writer, and a * second writer is a divergence by construction — whether or not today's two * values happen to agree. */ /** * useSEO composable for DCS-managed SEO configuration. * * @param pageSlug - Page slug matching entry in seo.yaml * @param pagePath - Optional page path for canonical URL generation * @returns SEO helpers and state */ declare function useSEO(pageSlug: string, pagePath?: string): UseSeoReturn; /** * Create a typed useSEO function with site-specific defaults. * Useful for creating a site-wide wrapper. * * @example * ```ts * // composables/useSiteSeo.ts * import { createSiteSEO } from '@duffcloudservices/cms' * * export const useSiteSeo = createSiteSEO({ * siteName: 'My Site', * siteUrl: 'https://example.com' * }) * ``` */ declare function createSiteSEO(_siteDefaults: Partial): (pageSlug: string, pagePath?: string) => UseSeoReturn; /** * Types for release notes API */ /** * Release note data from the API */ interface ReleaseNote { /** Semantic version (e.g., "1.2.0") */ version: string; /** Release title */ title: string; /** Brief summary for listings */ summary: string; /** Full markdown content */ notesMarkdown: string; /** Number of changes in release */ changeCount: number; /** ISO timestamp of release date */ releaseDate: string; } /** * Return type of useReleaseNotes composable */ interface ReleaseNotesReturn { /** The loaded release note */ releaseNote: vue.Ref; /** Loading state */ isLoading: vue.Ref; /** Error message */ error: vue.Ref; /** Refresh from API */ refresh: () => Promise; } /** * Return type of useSiteVersion composable */ interface SiteVersionReturn { /** Current site version */ version: vue.Ref; /** Loading state */ isLoading: vue.Ref; /** Full release notes link */ releaseNotesUrl: vue.ComputedRef; } /** * useReleaseNotes Composable * * Fetches and displays versioned release notes from the DCS Portal API. * Supports fetching specific versions or the latest release. * * @example * ```vue * * * * ``` */ /** * useReleaseNotes composable for fetching release notes from the DCS API. * * @param version - Semantic version (e.g., "1.2.0") or "latest" * @param options - Optional configuration * @returns Release notes data and state */ declare function useReleaseNotes(version: string, options?: { fetchOnMount?: boolean; }): ReleaseNotesReturn; /** * useSiteVersion Composable * * Gets the current site version for footer badges and version displays. * Fetches the latest release version from the DCS Portal API. * * @example * ```vue * * * * ``` */ /** * useSiteVersion composable for displaying the current site version. * * @param options - Optional configuration * @returns Site version data and computed URL */ declare function useSiteVersion(options?: { fetchOnMount?: boolean; }): SiteVersionReturn; /** * useMediaCarousel Composable * * Extracts media carousel items from text content keys following the pattern: * `{prefix}.{N}.url`, `{prefix}.{N}.type`, `{prefix}.{N}.alt` * * @example * ```vue * * * * ``` */ /** * Media carousel item representing an image, video, or embed */ interface MediaCarouselItem { /** URL to the image, video file, or embed URL */ url: string; /** * Type of media: 'image', 'video' (direct file), 'youtube', or 'instagram'. * Render `youtube`/`instagram` items through the click-to-load facade * `LiteMediaEmbed` (`@duffcloudservices/cms/lite-media-embed`) so the heavy * player iframe loads only on user interaction — never eagerly. */ type: 'image' | 'video' | 'youtube' | 'instagram'; /** Accessibility alt text */ alt?: string; /** * Whether this image has responsive CDN variants available. * Automatically set to `true` when the URL matches the DCS CDN asset pattern. * Components rendering the carousel should use `` when this is `true`. */ responsive?: boolean; } /** * Configuration for useMediaCarousel composable */ interface UseMediaCarouselConfig { /** Key prefix for carousel items (e.g., 'hero.media-carousel') */ prefix: string; /** The t() function from useTextContent */ t: (key: string, fallback?: string) => string; /** Default items to use if no content keys are found */ defaults?: MediaCarouselItem[]; /** Maximum number of items to look for (default: 10) */ maxItems?: number; } /** * Return type for useMediaCarousel composable */ interface UseMediaCarouselReturn { /** Computed array of media carousel items */ items: ComputedRef; /** Whether any items were found from content keys */ hasItems: ComputedRef; /** Number of items in the carousel */ count: ComputedRef; } /** * Extract media carousel items from text content keys. * * Looks for keys in the format: * - `{prefix}.{N}.url` - Required URL for the media * - `{prefix}.{N}.type` - Type: 'image' or 'video' (defaults to 'image') * - `{prefix}.{N}.alt` - Alt text for accessibility * * Items are sorted by index (0, 1, etc.) and only included if they have a valid URL. * * @param config - Configuration object * @returns Media carousel helpers and state */ declare function useMediaCarousel(config: UseMediaCarouselConfig): UseMediaCarouselReturn; /** * Vue 3 composable that resolves responsive image variants for DCS CDN-hosted assets. * * Wraps the framework-agnostic `resolveResponsiveImage` from `@duffcloudservices/cms-core` * with reactive Vue refs so it can be used directly in ` * * * ``` */ interface UseResponsiveImageOptions { /** Source URL — can be a reactive ref, getter, or plain string. */ src: MaybeRefOrGetter; /** Alt text — can be a reactive ref, getter, or plain string. */ alt: MaybeRefOrGetter; /** Sizing context — determines which variants to include. */ context?: MaybeRefOrGetter; /** Optional `sizes` attribute override. */ sizes?: MaybeRefOrGetter; /** Skip variant resolution and use the original URL only. */ original?: MaybeRefOrGetter; /** Intrinsic width — emitted as a layout-shift hint when paired with `height`. */ width?: MaybeRefOrGetter; /** Intrinsic height — emitted as a layout-shift hint when paired with `width`. */ height?: MaybeRefOrGetter; } /** * Reactively resolves responsive image metadata for a DCS CDN URL. * * The returned object is a computed ref that recomputes whenever any * of the input refs change. Spread `imgProps` onto an `` or * combine with `sources` inside a `` element. */ declare function useResponsiveImage(options: UseResponsiveImageOptions): ResponsiveImageResult; /** * Composable for reading curated review selections from DCS content. * Reviews are stored in content.yaml by the visual editor's ReviewPickerSheet. */ interface ReviewItem { id: string; platform: 'google' | 'meta' | string; rating: number; authorName: string; authorPhotoUrl?: string; text?: string; date?: string; replyText?: string; locationName?: string; sourceLocationName?: string; sourceUrl?: string; } interface UseReviewContentConfig { /** The section key matching the data-dcs-reviews attribute value */ sectionKey: string; /** Page slug for page-specific content lookup (defaults to current page) */ pageSlug?: string; /** Fallback reviews when no content is available */ defaults?: ReviewItem[]; } interface UseReviewContentReturn { /** The curated review items from content */ reviews: ComputedRef; /** Whether any reviews are available */ hasReviews: ComputedRef; /** Number of reviews */ count: ComputedRef; } declare function useReviewContent(config: UseReviewContentConfig): UseReviewContentReturn; /** How a clicked element was classified. */ type ConversionInteractionType = 'booking' | 'phone' | 'email' | 'form_submit' | 'social' | 'external' | 'internal' | 'button'; /** * The interaction types that ARE the money moment — the ones an owner report counts. * * Everything else (`social`, `external`, `internal`, `button`) is navigation telemetry and * is still captured, but it must never be summed into "conversions". Keeping the set here, * rather than in each report's query, means one edit changes every consumer. */ declare const CONVERSION_INTERACTION_TYPES: readonly ConversionInteractionType[]; /** Whether an interaction type counts as a conversion. */ declare function isConversionType(type: ConversionInteractionType): boolean; /** A captured conversion event, in App Insights `trackEvent` shape. */ interface ConversionEvent { /** Event name — `site_interaction` by default (see {@link ConversionTrackingOptions.eventName}). */ name: string; /** Flat string properties; App Insights `customDimensions`. */ properties: { interaction_type: ConversionInteractionType; /** * `'true'` when {@link isConversionType} holds. A string, not a boolean, because App * Insights `customDimensions` and GA4 event params are both string maps — so the * owner-report query is one predicate (`is_conversion == "true"`) instead of an * interaction-type IN-list that every new report has to remember to keep in sync. */ is_conversion: string; /** Visible label / aria-label of the clicked element, truncated. */ label: string; /** * Destination, query string and fragment stripped, and REDACTED for contact schemes: * a `tel:` / `sms:` / `mailto:` href becomes `tel:#` — never the raw number or * address. See {@link redactHref}. Empty for buttons and form submits. */ href: string; /** URL scheme of the destination including the colon (`tel:`, `https:`), or `''`. */ href_scheme: string; /** Host of the destination, or `''` for buttons, form submits and contact schemes. */ href_host: string; /** * Short digest of the contact target (the phone number / email address), or `''` for * everything else. Lets a report say "CTA A got 12 taps, CTA B got 3" without ever * storing the contact string itself. */ href_hash: string; /** Path of the page the click happened on. */ page_path: string; /** Host of the page the click happened on. */ host: string; /** Schema version, so a report can tell old rows from new ones. */ capture_version: string; }; } /** A telemetry transport. Typically `(e) => telemetry.trackEvent(e)`. */ type ConversionSink = (event: ConversionEvent) => void; interface ConversionTrackingOptions { /** * Where to send events. Optional on purpose — omit it when the telemetry SDK is * deferred, and call {@link attachConversionSink} once it has loaded. Events captured * in the meantime are buffered, not dropped. */ sink?: ConversionSink; /** * Fire-and-forget transports that receive every event AS WELL AS `sink`, and that do NOT * count as "a sink exists" for buffering purposes. * * This distinction is load-bearing. GA4's `gtag` is a mirror: the deploy injects it as a * synchronous inline snippet, so it is either there when the click happens or the hit is * genuinely unavailable — there is nothing to wait for. App Insights is a `sink`: it boots * on an idle callback minutes later, which is exactly what the buffer exists to survive. * Treating GA4 as a `sink` would have satisfied the "do we have somewhere to send this?" * test on every site and quietly disabled the deferred-SDK interlock. */ mirrors?: ConversionSink[]; /** * Custom event name. Defaults to `site_interaction` — the name Just Posh has been * emitting since 2026-04, so its history stays one continuous series. Pass * `'dcs_conversion'` on a site with no existing history if you prefer the canonical name. */ eventName?: string; /** * Extra hostnames to treat as booking destinations, on top of {@link DEFAULT_BOOKING_HOSTS}. * Matched on host suffix, so `vagaro.com` also matches `www.vagaro.com`. */ bookingHosts?: string[]; /** * Same-origin paths that mean "booking" (e.g. a self-hosted `/book`). Matched as a * prefix on the pathname. */ bookingPaths?: string[]; /** Extra social hostnames on top of {@link DEFAULT_SOCIAL_HOSTS}. */ socialHosts?: string[]; /** Max events held while no sink is attached. Default 50 — bounded so a bot cannot grow it. */ bufferLimit?: number; /** Drop untrusted (script-dispatched) clicks. Default `false`. */ requireTrusted?: boolean; /** Document to bind to. Defaults to the ambient `document`. Injected in tests. */ target?: Document; /** * Also capture managed-form submissions as `form_submit`. Default `true`. * * A form submit is a conversion on every DCS site that has a form, and it is the one * affordance a click listener alone cannot see honestly: clicking "Send" on a form that * then fails validation is not a lead. So the `submit` event — not the click — is * authoritative, and clicks on submit controls are deliberately dropped to keep the two * from counting the same action twice. */ captureFormSubmits?: boolean; /** * Honour the visitor's Do Not Track signal. Default `true`. * * With DNT on, `start()` binds nothing at all — no listener, no buffer, no event. This * is a measurement rail, not a consent platform: if a site ever grows a real consent * banner, gate {@link installConversionCapture} on it rather than weakening this. */ respectDoNotTrack?: boolean; /** * Bind a second delegated listener even though one is already live on this document. * * Off by default and it should stay off. Two delegated listeners on one document count * every click twice — the same defect class C-288 measured as 52% duplicate page views * on a live customer site, which the portal then reported to the owner as traffic. If * you are reaching for this, you almost certainly want `stop()` on the existing tracker. */ force?: boolean; } /** * Schema version stamped on every captured event. Bump on a breaking property change. * * `2` (C-326): `href` is redacted for `tel:`/`sms:`/`mailto:`, and `is_conversion`, * `href_scheme` + `href_hash` were added. Version `1` rows carry raw contact hrefs and no * conversion flag, so a report spanning the boundary must branch on this. */ declare const CONVERSION_CAPTURE_VERSION = "2"; /** * Booking/scheduling vendors seen across the DCS fleet plus the common SMB schedulers. * Host-suffix matched. Add per-site extras via `bookingHosts` rather than editing this. */ declare const DEFAULT_BOOKING_HOSTS: readonly string[]; /** Social destinations. Host-suffix matched. */ declare const DEFAULT_SOCIAL_HOSTS: readonly string[]; /** * FNV-1a (32-bit), base36. Synchronous on purpose: this runs inside a click handler, where * `crypto.subtle` — the only real hash a browser offers — is async and would force the * event to be built after the navigation has already started. * * BE HONEST ABOUT WHAT THIS IS. It is not anonymisation. A site publishes two or three * phone numbers, so anybody holding the site could brute-force the digest back in * milliseconds. What it buys is real but narrow: the contact string never lands in an * analytics store (GA4 forbids PII in event params outright), a support screenshot of the * events table cannot leak a customer's mailbox, and the value is still stable enough to * answer "which CTA did they tap". Do not describe it as anything more than that. */ declare function hashTarget(value: string): string; /** The scheme of an href, including the colon (`tel:`, `https:`), or `''`. */ declare function hrefScheme(href: string, pageHost: string): string; /** * Split an href into the parts that are safe to emit. * * For `tel:` / `sms:` / `mailto:` the target is a person's or business's contact string, so * it is replaced by `#` and the digest is also surfaced on its own. For * everything else the href passes through {@link sanitizeUrl} unchanged — a booking URL's * path is the useful part and its query (which CAN carry a name or email) is already gone. */ declare function redactHref(href: string, pageHost: string): { href: string; scheme: string; hash: string; }; /** * Whether this element is the control that submits a form. * * Clicks on these are dropped so the `submit` event can be the single source of truth — * see {@link ConversionTrackingOptions.captureFormSubmits}. Note the HTML default: a * `