import { Plugin, HtmlTagDescriptor } from 'vite'; import { D as DcsRobotsOptions, Y as SeoHonestyBlock, Z as PageRouteEntry, H as SeoConfiguration, E as ContentConfig } from '../vitepressTransform-CZ_IB3dq.js'; export { C as CreateSeoTransformPageDataOptions, R as ResolvedPageOverrides, S as SeoPageContext, r as SeoPageTypeRule, t as VitePressHeadConfig, V as VitePressPageData, b as buildVitePressSeoHead, c as createSeoTransformPageData, d as defaultRelativePathToRoute } from '../vitepressTransform-CZ_IB3dq.js'; import { H as HonestyMode } from '../headHonesty-OzxvLuwd.js'; import { Component, Plugin as Plugin$1 } from 'vue'; import MarkdownIt from 'markdown-it'; /** * DCS Content Plugin for Vite * * Reads `.dcs/content.yaml` at build time and injects content * as `__DCS_CONTENT__` global variable for use by useTextContent. * * @example * ```typescript * // vite.config.ts * import { dcsContentPlugin } from '@duffcloudservices/cms/plugins' * * export default defineConfig({ * plugins: [ * dcsContentPlugin({ debug: true }) * ] * }) * ``` * * For VitePress: * ```typescript * // .vitepress/config.ts * import { defineConfig } from 'vitepress' * import { dcsContentPlugin } from '@duffcloudservices/cms/plugins' * * export default defineConfig({ * vite: { * plugins: [ * dcsContentPlugin() * ] * } * }) * ``` */ interface DcsContentPluginOptions { /** Path to content.yaml relative to project root (default: '.dcs/content.yaml') */ contentPath?: string; /** Enable debug logging */ debug?: boolean; } /** * Vite plugin that injects .dcs/content.yaml at build time. * * @param options - Plugin configuration * @returns Vite plugin */ declare function dcsContentPlugin(options?: DcsContentPluginOptions): Plugin; /** * DCS SEO Plugin for Vite * * Two responsibilities, both driven by `.dcs/seo.yaml`: * * 1. **Build-time define** (always on): reads `.dcs/seo.yaml` and injects it as * the `__DCS_SEO__` global for the runtime `useSEO` composable. * * 2. **Static `` emitter** (opt-in via `emitStaticHtml: true`, default * OFF): after the bundle is written, reads the SPA's built `index.html`, * and for every route in `.dcs/pages.yaml` writes a per-route * `dist//index.html` whose `` carries the resolved title, meta, * canonical, Open Graph, Twitter, and JSON-LD tags. This gives a Vue SPA * per-route static SEO **without** vite-ssg. * * VitePress sites already bake SEO via their own config, so they leave this * option OFF and are completely unaffected. * * @example Runtime define only (default — safe for VitePress) * ```typescript * // vite.config.ts * import { dcsSeoPlugin } from '@duffcloudservices/cms/plugins' * * export default defineConfig({ * plugins: [dcsSeoPlugin({ debug: true })] * }) * ``` * * @example Vue SPA with per-route static emission * ```typescript * // vite.config.ts * import { dcsSeoPlugin } from '@duffcloudservices/cms/plugins' * * export default defineConfig({ * plugins: [ * dcsSeoPlugin({ * emitStaticHtml: true, // turn the emitter ON * pagesPath: '.dcs/pages.yaml', // route manifest (default) * noindex: ['account', 'projects'], // robots: noindex,nofollow * exclude: ['/preview'], // skip these routes entirely * }) * ] * }) * ``` */ interface DcsSeoPluginOptions { /** Path to seo.yaml relative to project root (default: '.dcs/seo.yaml') */ seoPath?: string; /** Enable debug logging */ debug?: boolean; /** * Opt-in: emit per-route static `` (meta + JSON-LD) into the built * `dist/` at the end of the build. Default `false` — VitePress sites and any * site that bakes its own SEO are unaffected when this is off. */ emitStaticHtml?: boolean; /** * Build-time BODY prerender: after the per-route `` is emitted, render * each indexable route's real DOM from the just-built SPA (headless Chromium * via the site's `playwright`) and splice it into the mount container so non-JS * AI crawlers see the page's body prose — not just `
`. * * Defaults to whatever `emitStaticHtml` is, so every SPA already emitting * per-route HTML gains crawler-visible bodies on a cms bump with no per-site * edit. Set `false` here to keep head-only emission. It is ALSO overridable * per-site (no code change / no cms republish) via `.dcs/seo.yaml` * `prerenderBody: false`. noindex/auth-gated routes are never body-prerendered; * a route that crashes at render time fails the build loud rather than shipping * a broken body. Requires `playwright` in the site (already the fleet default); * absent ⇒ graceful no-op (head + JSON-LD still emitted). */ prerenderBody?: boolean; /** * Path to the route manifest relative to project root, used only when * `emitStaticHtml` is true. Default `'.dcs/pages.yaml'`. */ pagesPath?: string; /** * Path to `content.yaml` relative to project root, used only when * `emitStaticHtml` is true. Threads REAL reviews into the honest * Review/aggregateRating on the per-route LocalBusiness node. Default * `'.dcs/content.yaml'`. Missing/unparseable ⇒ no reviews emitted. */ contentPath?: string; /** * Routes to skip entirely (no per-route HTML written). Matched against the * route `path` (e.g. `'/preview'`) OR the route `slug` (e.g. `'account'`). */ exclude?: string[]; /** * Routes that should receive `robots: noindex, nofollow`. Matched against the * route `path` OR `slug`. The home route (`/`) still overwrites * `dist/index.html`. * * This is a PREDICATE OVER `pages.yaml`, not a standalone directive about a * URL. An entry that matches no page is a **build error** * (`NoindexOrphanError`, C-416) — with no matching page nothing is emitted for * it, so the entry would silently do nothing while the URL stayed * `index, follow`. Matched raw, so `'/account/'` does not match a `'/account'` * route; matched against the full manifest, so an entry that is also in * `exclude` is fine. */ noindex?: string[]; /** * Opt-in: emit the site-wide static files `dist/sitemap.xml`, * `dist/robots.txt`, and `dist/llms.txt` after the per-route HTML loop. * * Defaults to whatever `emitStaticHtml` is — so every site already calling * `dcsSeoPlugin({ emitStaticHtml: true })` gets sitemap/robots/llms for free * with zero per-site edits. Set explicitly to `false` to keep emitting * per-route HTML without the site files. The whole block is wrapped in the * same try/catch + existsSync guards, so it can never break a build. */ emitSiteFiles?: boolean; /** * Canonical production origin, e.g. `https://ironoakcontractors.com`. Used as * the sitemap/llms base and the absolute `Sitemap:` line in robots.txt. When * absent, falls back to `seo.yaml`'s `global.siteUrl`; if neither is known and * no per-page canonicals exist, the sitemap + llms.txt no-op (robots still * emits). Never guessed. */ siteUrl?: string; /** robots.txt override hooks (disallow/allow/extra/aiBots/force). */ robots?: DcsRobotsOptions; /** * Preview / staging gate. When `true`, robots.txt becomes `Disallow: /` (no * `Sitemap:` line) and neither sitemap.xml nor llms.txt is emitted (privacy). * Detected ONLY via this explicit option — never guessed. */ preview?: boolean; /** Emit `dist/llms.txt` (default `true`; always off in preview mode). */ llms?: boolean; /** * P1 — assert that each route's BAKED ``/description equals what the * app leaves in the `<head>` after it mounts. Rides the prerender browser, so * the marginal cost is one `page.evaluate()` per route. * * Env override: `DCS_SEO_HEAD_HONESTY=error|warn|off`. * seo.yaml override: `headHonesty: warn` or `headHonesty: { mode, allow }`. */ headHonesty?: boolean | HonestyMode | SeoHonestyBlock; /** * P2 — assert that every URL the factory publishes (JSON-LD logo/image IRIs, * `og:image`, icon links, sitemap `<loc>`s, llms.txt links) resolves to what it * promises. Same-origin assets are proven against `dist/` with no network; * cross-origin assets are probed under a cache + time budget and only a * DEFINITIVE wrong answer fails the build. * * Env override: `DCS_SEO_URL_HONESTY=error|warn|off`, * `DCS_SEO_URL_HONESTY_NETWORK=off` to skip the cross-origin probes. */ urlHonesty?: boolean | HonestyMode | SeoHonestyBlock; /** * P12 — hoist `<meta charset>` to the top of `<head>` and assert it lands * inside the spec's 1024-byte encoding-sniffing window. * * Env override: `DCS_SEO_CHARSET_BUDGET=error|warn|off`. */ charsetBudget?: boolean | HonestyMode | SeoHonestyBlock; } /** * Every key a `noindex` entry can legitimately match. * * The plugin's contract for `noindex` is a PREDICATE OVER THE ROUTE MANIFEST, * never a directive about a bare URL. Every consumer asks the question * route-first and identically — * `noindexSet.has(route.path) || (route.slug && noindexSet.has(route.slug))` — * in {@link emitStaticSeoHtml}, in the sitemap/llms `isRouteIndexable` * predicate, and in the baked `__DCS_PAGES__` manifest the runtime re-assert * reads. So the set of entries that can ever do anything is exactly the union * of the manifest's paths and slugs, and this function is the exact inverse of * that same relation rather than a second, drifting implementation of it. */ declare function collectNoindexMatchKeys(routes: PageRouteEntry[]): Set<string>; /** * `noindex` entries that match no route in the manifest — i.e. entries that * are, today, silent no-ops. * * Matched with the SAME raw string equality the consumers use, deliberately: * a `'/account/'` entry against a `'/account'` route does not match in * `emitStaticSeoHtml`, so it must not "match" here either, or the check would * bless the very entry the emitter is about to ignore. */ declare function findOrphanNoindexEntries(noindex: string[], routes: PageRouteEntry[]): string[]; /** Thrown when a `noindex` entry names nothing in `pages.yaml` (C-416). */ declare class NoindexOrphanError extends Error { readonly orphans: string[]; readonly knownKeys: string[]; constructor(orphans: string[], knownKeys: string[]); } /** * Core site-file emitter, separated from Vite so it is unit-testable. * * Writes the site-wide static files into `outDir`, reusing the SAME routes + * seoConfig already loaded by the per-route HTML loop (no second manifest read): * - `sitemap.xml` (skipped in preview, or when no absolute base is derivable) * - `robots.txt` (always; preview = `Disallow: /`; don't-clobber unless force) * - `llms.txt` (skipped in preview, or when nothing indexable) * * Returns the list of relative filenames written. Pure aside from `fs` writes — * deterministic + idempotent. Never throws (the caller also wraps in try/catch). */ declare function emitSiteFiles(params: { outDir: string; projectRoot: string; pagesPath: string; routes: PageRouteEntry[]; seoConfig: SeoConfiguration | undefined; siteUrl?: string; exclude?: string[]; noindex?: string[]; preview?: boolean; robots?: DcsRobotsOptions; llms?: boolean; debug?: boolean; }): string[]; /** * Core emitter, separated from Vite so it is unit-testable. * * For each route: build the head tags from the shared resolver, splice them * into the shell HTML, and write `dist/<path>/index.html`. Returns the number * of files written. Pure aside from `fs` writes — deterministic + idempotent. */ declare function emitStaticSeoHtml(params: { outDir: string; shellHtml: string; routes: PageRouteEntry[]; seoConfig: SeoConfiguration | undefined; /** * Parsed `.dcs/content.yaml` — threads REAL review items (honesty-gated) into * the per-route LocalBusiness node. Optional; omit ⇒ no Review/aggregateRating. */ contentConfig?: ContentConfig; exclude?: string[]; noindex?: string[]; /** `pages.yaml` top-level `excluded:` globs — routes matching are NOT emitted. */ excludedGlobs?: string[]; debug?: boolean; }): number; /** * Vite plugin that injects .dcs/seo.yaml at build time and, optionally, emits * per-route static `<head>` HTML for SPA SEO. * * @param options - Plugin configuration * @returns Vite plugin */ declare function dcsSeoPlugin(options?: DcsSeoPluginOptions): Plugin; /** * DCS Editor Plugin for Vite * * Injects the editor bridge script into customer sites when running * in portal preview mode (inside the visual editor iframe). * * The bridge enables: * - Inline text editing via contenteditable * - Section hover highlights and AI ✨ buttons * - postMessage communication with the portal * * This plugin should only be active in development/preview mode. * It's safe to include in production builds — it does nothing unless * the page detects it's inside an iframe. * * @example * ```typescript * // vite.config.ts * import { dcsEditorPlugin } from '@duffcloudservices/cms/plugins' * * export default defineConfig({ * plugins: [ * dcsEditorPlugin() * ] * }) * ``` */ interface DcsEditorPluginOptions { /** Enable debug logging */ debug?: boolean; } /** * Vite plugin that injects the editor bridge script for portal preview integration. * * The bridge auto-initializes only when the page detects it's running inside an iframe, * so it's safe to include in all builds — it's a no-op in standalone browsing. * * Uses a virtual module (`/__dcs-editor-bridge.js`) served through Vite's dev server * so that bare module specifiers (like `@duffcloudservices/cms/editor`) are properly * resolved through Vite's module graph rather than hitting the browser's native ESM * resolver, which can't handle bare specifiers. */ declare function dcsEditorPlugin(options?: DcsEditorPluginOptions): Plugin; /** * DCS Preview Plugin for Vue * * Registers a supplied ribbon component as a global `DcsPreviewRibbon` * component. The ribbon handles its own visibility — it only renders on * `preview.duffcloudservices.com` and hides everywhere else (localhost, * production domains, and inside the visual page editor iframe). * * Why does the caller pass the component in? Because this file is compiled * by tsup (esbuild) which has no `.vue` SFC loader. Keeping the raw `.vue` * import out of the compiled plugins bundle avoids the build error while * still letting consumer code (which *does* run through Vite) resolve the * SFC at dev/build time. * * @example VitePress theme/index.ts * ```typescript * import { dcsPreviewPlugin } from '@duffcloudservices/cms/plugins' * import PreviewRibbon from '@duffcloudservices/cms/components' * * export default { * Layout, * enhanceApp({ app }) { * app.use(dcsPreviewPlugin(PreviewRibbon)) * } * } * ``` */ interface DcsPreviewPluginOptions { /** * Override the version string displayed on the ribbon. * If omitted, the ribbon auto-detects from VITE_SITE_VERSION or the API. */ version?: string | null; } /** * Creates and returns the DCS Preview plugin. * * When installed, it registers the supplied ribbon component as a global * `DcsPreviewRibbon` component. Add `<DcsPreviewRibbon />` to your root * Layout, or use the `dcsEditorPlugin` Vite plugin which injects it via * `transformIndexHtml`. * * @param ribbonComponent - The PreviewRibbon SFC (imported by the consumer) * @param options - Optional configuration */ declare function dcsPreviewPlugin(ribbonComponent: Component, options?: DcsPreviewPluginOptions): Plugin$1; /** * DCS CDN Image Plugin for Vite * * Rewrites local static image references (e.g. `/images/staff/photo.jpg`) * to CDN URLs at build time using the `.dcs/cdn-image-map.json` mapping file * generated by the `image-migrate adopt` CLI command. * * For raster images with WebP variants, `<img>` elements are transformed into * responsive `<picture>` elements with `srcset` for optimised delivery. * SVGs receive a simple URL swap with no variant handling. * * **In development mode this plugin is a no-op** — local `/images/` paths * continue to work via Vite's static asset serving so hot-reload is unaffected. * * The plugin handles two in-pipeline replacement vectors: * * 1. **Module transform** (`transform` hook) — rewrites `<img>` tags and * string literals in Vue SFCs, TS, JS, CSS, MD, and HTML modules. * 2. **Chunk rendering** (`renderChunk` hook) — rewrites image paths in * final rendered JS/CSS chunks AFTER Vite's `define` substitution. * This catches data injected by `dcsContentPlugin` via `__DCS_CONTENT__` * (from `.dcs/content.yaml`) which bypasses the `transform` hook. * * A third vector — **post-build file processing** — is handled by the * companion `dcsCdnBuildEnd` hook, which must be registered separately in * VitePress config. VitePress generates HTML *after* both Vite builds * complete, so Vite plugin hooks (`closeBundle`, `transformIndexHtml`) * cannot catch SSR-rendered `<head>` tags or static `public/` files like * `service-worker.js`. * * @example * ```ts * // .vitepress/config.ts * import { dcsCdnImagePlugin, dcsCdnBuildEnd } from '@duffcloudservices/cms/plugins' * * export default defineConfig({ * vite: { * plugins: [ * dcsCdnImagePlugin() * ] * }, * buildEnd: dcsCdnBuildEnd() * }) * ``` */ interface DcsCdnImagePluginOptions { /** Path to cdn-image-map.json relative to project root (default: '.dcs/cdn-image-map.json') */ mapPath?: string; /** * Patterns to match for replacement. Each must be a **leading-slash path prefix** * that appears in source code (e.g. `/images/`). The `localPath` field in the * mapping file is compared *without* a leading slash. * * Default: `['/images/']` */ pathPrefixes?: string[]; /** * Default `sizes` attribute for responsive `<picture>` elements. * Override per-context via the `data-sizes` attribute on the original `<img>`. * * Default: `'(max-width: 1024px) 100vw, 1024px'` */ defaultSizes?: string; /** Enable debug logging */ debug?: boolean; } /** * Vite plugin that rewrites static `/images/` paths to CDN URLs at build time. */ declare function dcsCdnImagePlugin(options?: DcsCdnImagePluginOptions): Plugin; interface DcsCdnBuildEndOptions { /** Path to cdn-image-map.json relative to project root (default: '.dcs/cdn-image-map.json') */ mapPath?: string; /** * Patterns to match for replacement. * Default: `['/images/']` */ pathPrefixes?: string[]; /** Enable debug logging */ debug?: boolean; /** * File extensions to process in the output directory. * Default: `['.html', '.js']` */ extensions?: string[]; } /** * VitePress `buildEnd` hook factory that post-processes generated HTML and * static files in the output directory to replace remaining `/images/` paths * with CDN URLs. * * VitePress generates HTML **after** both Vite builds complete, so Vite * plugin hooks (`closeBundle`, `transformIndexHtml`) cannot catch * SSR-rendered `<head>` tags (favicons, OG images). This hook runs after all * HTML files are written to disk. * * Also rewrites static files (e.g. `service-worker.js`) copied from * `public/` that are not part of Vite's module pipeline. * * @example * ```ts * // .vitepress/config.ts * import { dcsCdnBuildEnd } from '@duffcloudservices/cms/plugins' * * export default defineConfig({ * buildEnd: dcsCdnBuildEnd({ debug: true }) * }) * ``` */ declare function dcsCdnBuildEnd(options?: DcsCdnBuildEndOptions): (siteConfig: { root: string; outDir: string; }) => Promise<void>; /** * markdown-it plugin that transforms standard `![alt](url)` image syntax * into responsive `<picture>` elements when the URL matches the DCS CDN * asset pattern. * * Non-CDN images are rendered with the default image renderer (plain `<img>`). * * @example * ```ts * // .vitepress/config.ts * import { responsiveImagePlugin } from '@duffcloudservices/cms/plugins' * * export default defineConfig({ * markdown: { * config: (md) => { * md.use(responsiveImagePlugin) * }, * }, * }) * ``` * * Input markdown: * ```md * ![Physical therapy session](https://files.duffcloudservices.com/kept/assets/blog/abc-123.jpg) * ``` * * Rendered HTML: * ```html * <picture> * <source srcset="...abc-123-sm.webp 640w, ...abc-123-md.webp 1024w, ...abc-123-lg.webp 1920w" * type="image/webp" * sizes="(max-width: 1024px) 100vw, 1024px" /> * <img src="...abc-123-md.webp" alt="Physical therapy session" loading="lazy" decoding="async" /> * </picture> * ``` */ declare function responsiveImagePlugin(md: MarkdownIt): void; /** * Build-time responsive-variant lint for DCS customer sites. * * The 2026-07-03 flagship-excellence review (P7) found desktop pages shipping * un-suffixed **full-size** CDN images (e.g. a 321.9 KB hero `47af1e8f….webp`) * instead of a `-md`/`-lg` responsive step. That happens when an image reaches * the output HTML through a path that does NOT run the `<img>` → `<picture>` * transform — content injected via `__DCS_CONTENT__` and then swapped to the * base `cdnUrl` by `dcsCdnBuildEnd`, or a hand-authored `<img src>` pointing at * a base CDN asset URL. * * A DCS CDN asset URL has the shape * `https://files.<host>/[content/]<slug>/assets/[<dir>/]<uuid>.<ext>` * Responsive variants insert a `-sm`/`-md`/`-lg` suffix before the extension * (`…<uuid>-md.webp`). Those suffixes contain non-hex letters, so a URL that * still matches the *base* pattern below is, by definition, an un-suffixed * full-size asset. This lint scans the built HTML's `<img src>` and * `<source srcset>` attributes for such URLs and reports (or fails on) them. * * It is intentionally a **static-output** lint (VitePress generates HTML after * both Vite builds finish, so a Vite plugin hook cannot see the SSG output) and * is registered via VitePress `buildEnd`. * * @example * ```ts * // .vitepress/config.ts * import { dcsCdnBuildEnd, dcsResponsiveImageLint, chainBuildEnd } from '@duffcloudservices/cms/plugins' * * export default defineConfig({ * // run the CDN rewrite first, then lint the result * buildEnd: chainBuildEnd(dcsCdnBuildEnd(), dcsResponsiveImageLint()), * }) * ``` */ interface ResponsiveImageViolation { /** The un-suffixed full-size CDN URL that should have been a responsive variant. */ url: string; /** Which element referenced it. */ tag: 'img' | 'source'; } /** True when `url` is a base (un-suffixed, full-size) DCS CDN asset URL. */ declare function isFullSizeCdnUrl(url: string): boolean; /** * Scan an HTML string and return every `<img src>` / `<source srcset>` reference * that resolves to a base (un-suffixed) full-size DCS CDN asset. `srcset` * candidates are split on commas and only the URL portion (before the width / * density descriptor) is tested. `onerror` and other attributes are ignored — * only the actual `src` / `srcset` sources are inspected. */ declare function findFullSizeCdnImages(html: string): ResponsiveImageViolation[]; interface DcsResponsiveImageLintOptions { /** * Throw (fail the build) when full-size images are found. Default `false` * (warn only) so the guard can land ahead of the per-site cleanup; flip to * `true` in CI once a site's output is clean to prevent regressions. */ failOnViolation?: boolean; /** File extensions to scan in the output directory. Default `['.html']`. */ extensions?: string[]; /** Enable per-file debug logging. */ debug?: boolean; } interface ResponsiveImageLintReport { filesScanned: number; violations: Array<ResponsiveImageViolation & { file: string; }>; } /** * VitePress `buildEnd` factory that lints the generated HTML for un-suffixed * full-size CDN images. Returns the report (also useful for tests) and either * warns or throws depending on `failOnViolation`. */ declare function dcsResponsiveImageLint(options?: DcsResponsiveImageLintOptions): (siteConfig: { outDir: string; }) => Promise<ResponsiveImageLintReport>; /** * Compose multiple VitePress `buildEnd` hooks into one (VitePress accepts a * single `buildEnd`). Hooks run sequentially in the order given, so a rewrite * hook (`dcsCdnBuildEnd`) can precede a lint (`dcsResponsiveImageLint`). */ declare function chainBuildEnd<T>(...hooks: Array<((siteConfig: T) => unknown | Promise<unknown>) | undefined | null>): (siteConfig: T) => Promise<void>; /** * DCS motion tokens — the customer-site copy of the shared motion vocabulary. * * Injected as a constant `<style>` into every DCS site's `<head>` by * `dcsContentPlugin` (see its `transformIndexHtml`). This is the fleet-wide * seam: every site (vue-spa and VitePress) already wires `dcsContentPlugin`, * so a cms version bump ships these tokens to the whole fleet with zero * per-site source edits. * * Source of truth is the platform copy at * `packages/dcs-ui/src/styles/motion.css` and the `dcs-product-motion` skill — * keep the values here in lockstep. Values ONLY: reduced-motion stays a * per-component responsibility so no blanket `!important` rule leaks into a * site's isolated `.<slug>-page` design subtree. */ declare const DCS_MOTION_TOKENS_CSS = ":root {\n --motion-duration-stagger: 40ms;\n --motion-duration-micro: 80ms;\n --motion-duration-quick: 150ms;\n --motion-duration-fast: 250ms;\n --motion-duration-medium: 350ms;\n --motion-duration-slow: 400ms;\n --motion-duration-very-slow: 500ms;\n --motion-ease-out: cubic-bezier(0.22, 1, 0.36, 1);\n --motion-ease-in-out: cubic-bezier(0.65, 0, 0.35, 1);\n --motion-ease-linear: linear;\n --motion-ease-bounce: cubic-bezier(0.34, 1.36, 0.64, 1);\n --motion-ease-bounce-strong: cubic-bezier(0.34, 3.85, 0.64, 1);\n --motion-distance-xs: 4px;\n --motion-distance-sm: 8px;\n --motion-distance-md: 12px;\n --motion-distance-lg: 24px;\n --motion-scale-lg: 0.96;\n --motion-scale-md: 0.97;\n --motion-scale-sm: 0.98;\n --motion-scale-xs: 0.99;\n --motion-blur-sm: 2px;\n --motion-blur-md: 3px;\n --motion-blur-lg: 8px;\n}"; /** * The `<head>` tag descriptor for the motion-token block. * * `injectTo: 'head'`, NOT `'head-prepend'` — C-414. Vite's `head-prepend` * inserts immediately after the `<head>` open tag, i.e. AHEAD of the site's own * `<meta charset>`. This block is 858 bytes, so it pushed the encoding * declaration to byte 910 of the HTML spec's 1024-byte encoding-sniffing window * on every site in the fleet (measured on Iron Oak and Bryan's 2026-07-27, and * re-measured identical here), leaving ~90 bytes of headroom before every DCS * site silently mojibakes. * * `'head'` appends to the END of the `<head>` the site authored — which is * still ABOVE Vite's own build-time `<link rel=stylesheet>` / entry `<script>`, * because those are injected AFTER this `order: 'pre'` hook's tags. So the * per-site `:root` override still wins the cascade (that was the original * reason for `head-prepend`) AND the charset lands at byte ~46 with ~954 bytes * of headroom. Measured, not assumed — see `verify-honesty-rails.mjs` P12. * * If you ever move this back to `head-prepend`, the P12 rail in * `scripts/verify-honesty-rails.mjs` will fail: it asserts the charset comes * FIRST straight out of `dcsContentPlugin`, with no `dcsSeoPlugin` hoist. */ declare function dcsMotionTokensStyleTag(): HtmlTagDescriptor; /** * DCS AI-assistant widget loader — the cms head-inject seam for Phase 3 of the * Managed Site AI Agents plan (.docs/archive/plans/managed-site-ai-agents/plan.md). * * This rides the same always-on `dcsContentPlugin.transformIndexHtml` seam that * ships the motion tokens: every DCS site already wires `dcsContentPlugin`, so a * cms version bump delivers this loader to the whole fleet with ZERO per-site * source edits, and the portal per-site toggle turns the widget on/off at * runtime with no rebuild. * * What gets injected is a tiny (<2 KB) inline `<script>` — NOT the widget. On * idle it fetches the public agent config for this site; only if the agent is * enabled (HTTP 200) does it publish `window.__DCS_AGENT_BOOTSTRAP__` and inject * the real widget bundle from the CDN. The loader is **fail-silent**: a disabled * agent (404), a dead backend, a blocked request, or a malformed response all * result in no mount and no console error, so the assistant can never break a * customer page. * * Build-only: the loader is injected into the production build output (the fleet * delivery), not into `vite dev`. In local dev the config fetch would hit the * prod API from a non-whitelisted `localhost` origin and log an uncatchable CORS * error for every site developer — so, unlike the constant motion-token block, * this network-touching loader stays out of the dev server. */ /** DCS API origin the loader calls (matches the cms runtime composables). */ declare const DEFAULT_AGENT_API_BASE_URL = "https://portal.duffcloudservices.com"; /** * Versioned CDN URL of the widget bundle. Both `cdn.` (branding CDN, CorsAllowAll) * and the already-live `files.` route serve the same `sgdcs` blob origin with an * identical `/agent-widget/v1/...` layout, so this is a plain classic-script * include that needs no CORS on the bundle host. Defaults to `files.` because the * `cdn.` Front Door route is still provisioning-gated (enableCdnDomain=false) — * flip the default only after that domain is live, or pass cdnScriptUrl. */ declare const DEFAULT_AGENT_WIDGET_SCRIPT_URL = "https://files.duffcloudservices.com/agent-widget/v1/dcs-agent-widget.js"; interface DcsAgentWidgetLoaderOptions { /** Owning site slug — baked into the loader so it can build the config URL. */ siteSlug: string; /** DCS API base (default: the shared portal origin). */ apiBaseUrl?: string; /** Full URL of the CDN widget bundle. */ cdnScriptUrl?: string; } /** * Builds the inline loader script body. Slug + URLs are HTML-safe-encoded (see * encodeForInlineScript) so no value can break out of the string literal OR the * `<script>` element. Kept intentionally compact to stay under the ~2 KB perf * budget (G4). */ declare function buildAgentWidgetLoaderScript(options: DcsAgentWidgetLoaderOptions): string; /** * Reads `site_slug` from the site's `.dcs/site.yaml` (the same identity file the * announcements/provisioning path uses). Returns null when no site.yaml exists * or it carries no slug — the caller then injects nothing (silence for non-DCS * builds). */ declare function readSiteSlug(projectRoot: string): string | null; interface DcsAgentWidgetTagOptions { /** Vite project root used to resolve the site slug. */ projectRoot: string; /** Override the DCS API base (default: process.env.VITE_API_BASE_URL, else the shared origin). */ apiBaseUrl?: string; /** Override the CDN bundle URL. */ cdnScriptUrl?: string; } /** * The `<head>` tag descriptor for the loader, or `null` when this build is not a * DCS-managed site (no resolvable slug) — in which case nothing is injected. */ declare function dcsAgentWidgetLoaderTag(options: DcsAgentWidgetTagOptions): HtmlTagDescriptor | null; export { DCS_MOTION_TOKENS_CSS, DEFAULT_AGENT_API_BASE_URL, DEFAULT_AGENT_WIDGET_SCRIPT_URL, type DcsAgentWidgetLoaderOptions, type DcsAgentWidgetTagOptions, type DcsCdnBuildEndOptions, type DcsCdnImagePluginOptions, type DcsContentPluginOptions, type DcsEditorPluginOptions, type DcsPreviewPluginOptions, type DcsResponsiveImageLintOptions, type DcsSeoPluginOptions, NoindexOrphanError, type ResponsiveImageLintReport, type ResponsiveImageViolation, buildAgentWidgetLoaderScript, chainBuildEnd, collectNoindexMatchKeys, dcsAgentWidgetLoaderTag, dcsCdnBuildEnd, dcsCdnImagePlugin, dcsContentPlugin, dcsEditorPlugin, dcsMotionTokensStyleTag, dcsPreviewPlugin, dcsResponsiveImageLint, dcsSeoPlugin, emitSiteFiles, emitStaticSeoHtml, findFullSizeCdnImages, findOrphanNoindexEntries, isFullSizeCdnUrl, readSiteSlug, responsiveImagePlugin };