@duffcloudservices/cms 0.13.0 → 0.13.2
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/dist/{chunk-HVSF23P7.js → chunk-64BUBTW7.js} +148 -24
- package/dist/chunk-64BUBTW7.js.map +1 -0
- package/dist/{chunk-A5F4C72F.js → chunk-OGAJX4KM.js} +2 -2
- package/dist/{chunk-A5F4C72F.js.map → chunk-OGAJX4KM.js.map} +1 -1
- package/dist/index.d.ts +130 -5
- package/dist/index.js +48 -17
- package/dist/index.js.map +1 -1
- package/dist/{installSeoHead-kWQwObez.d.ts → installSeoHead-EE0Z7UmK.d.ts} +60 -9
- package/dist/plugins/index.d.ts +2 -2
- package/dist/plugins/index.js +2 -2
- package/dist/seo/index.d.ts +139 -4
- package/dist/seo/index.js +2 -2
- package/dist/{vitepressTransform-JG_zlaux.d.ts → vitepressTransform-Ds5Hff8t.d.ts} +29 -5
- package/package.json +2 -2
- package/src/components/PreviewRibbon.vue +25 -9
- package/src/components/ResponsiveImage.test.ts +45 -0
- package/src/components/ResponsiveImage.vue +12 -0
- package/src/components/raw-source-imports.test.ts +44 -0
- package/src/composables/anonymousSessionGate.test.ts +243 -0
- package/src/composables/useSiteVisitorSession.ts +100 -20
- package/dist/chunk-HVSF23P7.js.map +0 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { H as SeoConfiguration, x as BreadcrumbCrumb, z as ReviewSource, F as FaqSource, y as BlogMeta, T as ResolvedPageSeo, N as SeoSchemaConfig, G as GlobalSeoConfig, L as SeoOpenGraphConfig, M as SeoTwitterConfig } from './vitepressTransform-
|
|
1
|
+
import { H as SeoConfiguration, x as BreadcrumbCrumb, z as ReviewSource, F as FaqSource, y as BlogMeta, T as ResolvedPageSeo, N as SeoSchemaConfig, G as GlobalSeoConfig, L as SeoOpenGraphConfig, M as SeoTwitterConfig } from './vitepressTransform-Ds5Hff8t.js';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Framework-agnostic SEO head-tag resolution.
|
|
@@ -229,6 +229,31 @@ declare function buildHeadTags(pageSlug: string, pagePath: string | undefined, s
|
|
|
229
229
|
* VitePress `transformPageData` head emit) share one guard.
|
|
230
230
|
*/
|
|
231
231
|
declare function escapeJsonLd(json: string): string;
|
|
232
|
+
/**
|
|
233
|
+
* IS THIS META TAG OURS? — the SINGLE definition of the managed meta set.
|
|
234
|
+
*
|
|
235
|
+
* WHY THIS IS EXPORTED AND WHY IT MUST STAY THE ONLY COPY (C-603 / Q-453=A).
|
|
236
|
+
* `installSeoHead`'s DOM sweep removes the baked managed tags on the first
|
|
237
|
+
* matched in-app navigation, and "managed" there has to mean EXACTLY what it
|
|
238
|
+
* means here — this function is the build-time emitter's own answer, and the
|
|
239
|
+
* runtime sweep is its DOM twin. The named failure mode of the sweep-once
|
|
240
|
+
* design is precisely these two lists drifting: a tag the emitter starts baking
|
|
241
|
+
* later would strand again, quietly, because the sweep would not recognise it.
|
|
242
|
+
* A shared predicate makes that drift unrepresentable rather than merely
|
|
243
|
+
* discouraged.
|
|
244
|
+
*
|
|
245
|
+
* Both arguments are accepted (rather than one "identity" string) because a
|
|
246
|
+
* `<meta>` is keyed by `name` OR `property` and the two namespaces overlap:
|
|
247
|
+
* `og:*` is a property, `twitter:*` is a name.
|
|
248
|
+
*/
|
|
249
|
+
declare function isManagedMetaTag(name: string | null | undefined, property: string | null | undefined): boolean;
|
|
250
|
+
/**
|
|
251
|
+
* IS THIS LINK OURS? — the SINGLE definition of the managed link set.
|
|
252
|
+
*
|
|
253
|
+
* `canonical` and `alternate` only. Stylesheets, preloads, icons and manifests
|
|
254
|
+
* belong to the app shell and are never touched by either sink.
|
|
255
|
+
*/
|
|
256
|
+
declare function isManagedLinkRel(rel: string | null | undefined): boolean;
|
|
232
257
|
/**
|
|
233
258
|
* Render the resolved head tags to a deterministic HTML fragment.
|
|
234
259
|
* Order: title, meta (in the order produced by buildHeadTags), link, script.
|
|
@@ -325,7 +350,12 @@ declare function spliceHeadHtml(html: string, tags: ResolvedHeadTags): string;
|
|
|
325
350
|
* structurally (`name` unset, `matched` empty, path `/`) and skips at most
|
|
326
351
|
* ONE write, ever.
|
|
327
352
|
*
|
|
328
|
-
*
|
|
353
|
+
* S6 THE BAKED MANAGED TAGS ARE SWEPT ONCE, ON THE FIRST MATCH. See DEFECT
|
|
354
|
+
* F-A immediately below and `sweepBakedHeadTags.ts` for the four refusals
|
|
355
|
+
* that make the sweep safe (JSON-LD and `<title>` unreachable; never
|
|
356
|
+
* sweep a tag the runtime is about to declare; snapshot at install time).
|
|
357
|
+
*
|
|
358
|
+
* MEASURED DEFECT F-A — **CLOSED 2026-08-11 by SWEEP ONCE** (Q-453 = A, C-603).
|
|
329
359
|
* The production-shaped fixture (`installSeoHead.integration.test.ts`) put a
|
|
330
360
|
* REAL Unhead client (both 1.11.20 and 2.0.19) on a REAL baked document and
|
|
331
361
|
* counted `document.head`. Two facts came out of it:
|
|
@@ -352,12 +382,23 @@ declare function spliceHeadHtml(html: string, tags: ResolvedHeadTags): string;
|
|
|
352
382
|
* was written by a PREVIOUS re-assert, Unhead owns it and drops it correctly,
|
|
353
383
|
* which is all a recording fake can ever model.
|
|
354
384
|
*
|
|
355
|
-
*
|
|
356
|
-
*
|
|
357
|
-
*
|
|
358
|
-
*
|
|
359
|
-
*
|
|
360
|
-
*
|
|
385
|
+
* THE FIX, DECIDED BY THE OWNER RATHER THAN INVENTED HERE. Q-453 chose A —
|
|
386
|
+
* SWEEP ONCE — over "always emit" (which structurally cannot fix hreflang or
|
|
387
|
+
* `article:*`, and forks the shared `buildHeadTags` path) and "stop baking
|
|
388
|
+
* optional tags" (the only crawler-VISIBLE option, and it changes them by
|
|
389
|
+
* deletion). On the first MATCHED in-app navigation this module now removes
|
|
390
|
+
* the baked managed tags the incoming input does not re-declare, so the
|
|
391
|
+
* runtime owns the managed head from then on. The sweep lives in
|
|
392
|
+
* `sweepBakedHeadTags.ts` and reuses the EMITTER'S OWN managed-set predicates
|
|
393
|
+
* (`isManagedMetaTag` / `isManagedLinkRel` in `spliceHeadHtml.ts`) rather
|
|
394
|
+
* than a second list, because the named residual of option A was those two
|
|
395
|
+
* lists drifting apart.
|
|
396
|
+
*
|
|
397
|
+
* Crawler-visible bytes are UNCHANGED: the sweep is reachable only from a
|
|
398
|
+
* matched in-app navigation, and R5 already refuses to write for the fetched
|
|
399
|
+
* document. The fixture's `DEFECT F-A` assertions (ITEM 2a / 2d, plus the
|
|
400
|
+
* third instance in ITEM 8a) are flipped to the swept expectation, which was
|
|
401
|
+
* the acceptance test this docstring named.
|
|
361
402
|
*
|
|
362
403
|
* WHAT IT RE-ASSERTS, AND WHAT IT DELIBERATELY DOES NOT.
|
|
363
404
|
* - `title`, `meta`, `link` — the managed fields P1 (`headHonesty.ts`) grades
|
|
@@ -508,6 +549,16 @@ interface SeoHeadResolution {
|
|
|
508
549
|
reason: SeoHeadResolutionReason;
|
|
509
550
|
/** The resolved `<title>`, or `null` when nothing was written. */
|
|
510
551
|
title: string | null;
|
|
552
|
+
/**
|
|
553
|
+
* How many BAKED managed tags this navigation swept out of the DOM (S6).
|
|
554
|
+
*
|
|
555
|
+
* Non-zero on AT MOST ONE navigation per session — the first match, which
|
|
556
|
+
* spends the sweep latch — and `0` on every other, including every
|
|
557
|
+
* no-match / failed / initial resolution. Reported for the same reason
|
|
558
|
+
* `no-match` is: "it swept nothing" and "it never swept" must not look the
|
|
559
|
+
* same from the outside.
|
|
560
|
+
*/
|
|
561
|
+
sweptBakedTags: number;
|
|
511
562
|
}
|
|
512
563
|
interface InstallSeoHeadOptions {
|
|
513
564
|
/**
|
|
@@ -624,4 +675,4 @@ declare function buildSeoHeadRouteMap(routes: readonly SeoHeadPageRoute[]): Map<
|
|
|
624
675
|
*/
|
|
625
676
|
declare function installSeoHead(router: SeoHeadRouterLike, options: InstallSeoHeadOptions): unknown;
|
|
626
677
|
|
|
627
|
-
export { type SeoHeadRouteHasNoMeta as
|
|
678
|
+
export { formatDuplicateNormalizedPaths as A, SEO_HEAD_ROUTE_HAS_NO_META as B, type SeoHeadRouteHasNoMeta as C, type HeadMetaTag as H, type InstallSeoHeadOptions as I, type ResolvedHeadTags as R, type SeoHeadRouterLike as S, generateOpenGraphMeta as a, buildHeadTags as b, generateTwitterMeta as c, renderHeadTags as d, stripManagedHeadTags as e, escapeJsonLd as f, generateJsonLd as g, isManagedLinkRel as h, isManagedMetaTag as i, installSeoHead as j, buildSeoHeadRouteMap as k, type HeadLinkTag as l, type HeadScriptTag as m, normalizeSeoHeadPath as n, type HeadTagOverrides as o, type SeoHeadRouteLike as p, type SeoHeadClientLike as q, resolvePageSeo as r, spliceHeadHtml as s, type SeoHeadEntryLike as t, type SeoHeadInput as u, type SeoHeadPageRoute as v, type SeoHeadPagesManifest as w, type SeoHeadResolution as x, type SeoHeadResolutionReason as y, findDuplicateNormalizedPaths as z };
|
package/dist/plugins/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Plugin, HtmlTagDescriptor } from 'vite';
|
|
2
|
-
import { D as DcsRobotsOptions, Y as SeoHonestyBlock, Z as PageRouteEntry, H as SeoConfiguration, E as ContentConfig } from '../vitepressTransform-
|
|
3
|
-
export { C as CreateSeoTransformPageDataOptions, R as ResolvedPageOverrides, S as SeoPageContext, r as SeoPageTypeRule, t as VitePressHeadConfig, V as VitePressPageData, b as buildVitePressSeoHead, c as createSeoTransformPageData, d as defaultRelativePathToRoute } from '../vitepressTransform-
|
|
2
|
+
import { D as DcsRobotsOptions, Y as SeoHonestyBlock, Z as PageRouteEntry, H as SeoConfiguration, E as ContentConfig } from '../vitepressTransform-Ds5Hff8t.js';
|
|
3
|
+
export { C as CreateSeoTransformPageDataOptions, R as ResolvedPageOverrides, S as SeoPageContext, r as SeoPageTypeRule, t as VitePressHeadConfig, V as VitePressPageData, b as buildVitePressSeoHead, c as createSeoTransformPageData, d as defaultRelativePathToRoute } from '../vitepressTransform-Ds5Hff8t.js';
|
|
4
4
|
import { H as HonestyMode } from '../headHonesty-OzxvLuwd.js';
|
|
5
5
|
import { Component, Plugin as Plugin$1 } from 'vue';
|
|
6
6
|
import MarkdownIt from 'markdown-it';
|
package/dist/plugins/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { buildSitemapXml, isHandAuthoredRobotsAcceptable, buildRobotsTxt, buildLlmsTxt, routeExclusionReason, breadcrumbTrailFromRoute, findReviewItemsForPage, findBusinessLicense, buildHeadTags, spliceHeadHtml, routeToOutputFile, loadPagesManifest, hoistCharsetMeta, checkCharsetBudget, isBodyPrerenderEnabled, resolveHonestyMode, assertHeadHonestyExecuted, findHeadHonestyViolations, assertHeadHonesty, isRouteEmitted, findDuplicateNormalizedPaths, formatDuplicateNormalizedPaths, prerenderBodies, CharsetBudgetError, formatCharsetHeadroomWarning, dedupeUrls, collectEmittedUrlsFromFiles, classifyUrl, auditSameOriginAssets, checkRemoteAssets, auditCheckedTotal, EmittedUrlNotRunError, formatEmittedUrlAudit, formatEmittedUrlReport, EmittedUrlError } from '../chunk-
|
|
2
|
-
export { buildVitePressSeoHead, createSeoTransformPageData, defaultRelativePathToRoute } from '../chunk-
|
|
1
|
+
import { buildSitemapXml, isHandAuthoredRobotsAcceptable, buildRobotsTxt, buildLlmsTxt, routeExclusionReason, breadcrumbTrailFromRoute, findReviewItemsForPage, findBusinessLicense, buildHeadTags, spliceHeadHtml, routeToOutputFile, loadPagesManifest, hoistCharsetMeta, checkCharsetBudget, isBodyPrerenderEnabled, resolveHonestyMode, assertHeadHonestyExecuted, findHeadHonestyViolations, assertHeadHonesty, isRouteEmitted, findDuplicateNormalizedPaths, formatDuplicateNormalizedPaths, prerenderBodies, CharsetBudgetError, formatCharsetHeadroomWarning, dedupeUrls, collectEmittedUrlsFromFiles, classifyUrl, auditSameOriginAssets, checkRemoteAssets, auditCheckedTotal, EmittedUrlNotRunError, formatEmittedUrlAudit, formatEmittedUrlReport, EmittedUrlError } from '../chunk-64BUBTW7.js';
|
|
2
|
+
export { buildVitePressSeoHead, createSeoTransformPageData, defaultRelativePathToRoute } from '../chunk-64BUBTW7.js';
|
|
3
3
|
import fs5 from 'fs';
|
|
4
4
|
import path5 from 'path';
|
|
5
5
|
import yaml4 from 'js-yaml';
|
package/dist/seo/index.d.ts
CHANGED
|
@@ -1,10 +1,145 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
import { H as HeadMetaTag, l as HeadLinkTag } from '../installSeoHead-EE0Z7UmK.js';
|
|
2
|
+
export { m as HeadScriptTag, o as HeadTagOverrides, I as InstallSeoHeadOptions, R as ResolvedHeadTags, B as SEO_HEAD_ROUTE_HAS_NO_META, q as SeoHeadClientLike, t as SeoHeadEntryLike, u as SeoHeadInput, v as SeoHeadPageRoute, w as SeoHeadPagesManifest, x as SeoHeadResolution, y as SeoHeadResolutionReason, C as SeoHeadRouteHasNoMeta, p as SeoHeadRouteLike, S as SeoHeadRouterLike, b as buildHeadTags, k as buildSeoHeadRouteMap, f as escapeJsonLd, z as findDuplicateNormalizedPaths, A as formatDuplicateNormalizedPaths, 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-EE0Z7UmK.js';
|
|
3
|
+
import { Z as PageRouteEntry } from '../vitepressTransform-Ds5Hff8t.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, ad as NormalisedFaq, ac as NormalisedReview, R as ResolvedPageOverrides, ae as ReviewSchemaParts, z as ReviewSource, a2 as RouteEmissionRules, a3 as RouteExclusionReason, w as SchemaObject, S as SeoPageContext, r as SeoPageTypeRule, t as VitePressHeadConfig, V as VitePressPageData, q as absolutizeUrl, j as breadcrumbTrailFromRoute, k as buildBlogPosting, h as buildBreadcrumbList, l as buildFaqPage, g as buildGlobalGraph, a4 as buildHasCredential, f as buildLlmsTxt, m as buildReviewSchemaParts, e as buildRobotsTxt, a as buildSitemapXml, b as buildVitePressSeoHead, c as createSeoTransformPageData, d as defaultRelativePathToRoute, a9 as deriveSameAs, o as filterRealFaq, n as filterRealReviews, a8 as findBusinessLicense, a6 as findGlobalSchemasOfType, a5 as findLocalBusinessSchema, p as findReviewItemsForPage, a7 as graphAbsorbs, aa as graphIds, ab as isLocalBusinessType, a0 as isRouteEmitted, i as isRouteIndexable, _ as loadPagesManifest, $ as parsePagesManifest, a1 as routeExclusionReason, s as slugToTitle } from '../vitepressTransform-Ds5Hff8t.js';
|
|
4
5
|
import { a as HeadObservation } from '../headHonesty-OzxvLuwd.js';
|
|
5
6
|
export { B as BakedHead, g as HeadHonestyError, l as HeadHonestyExecution, k as HeadHonestyNotRunError, p as HeadHonestyOptions, o as HeadHonestyViolation, H as HonestyMode, m as HonestyModeInput, b as assertHeadHonesty, h as assertHeadHonestyExecuted, d as decodeHtmlEntities, e as extractBakedHead, i as findHeadHonestyExecutionFaults, f as findHeadHonestyViolations, j as formatHeadHonestyExecutionReport, c as formatHeadHonestyReport, n as normalizeHeadText, r as resolveHonestyMode } from '../headHonesty-OzxvLuwd.js';
|
|
6
7
|
import 'vue';
|
|
7
8
|
|
|
9
|
+
/**
|
|
10
|
+
* THE SWEEP-ONCE HALF OF `installSeoHead` — the fix for DEFECT F-A
|
|
11
|
+
* (Q-453 = A, decided 2026-08-11; C-603).
|
|
12
|
+
*
|
|
13
|
+
* THE DEFECT, IN ONE SENTENCE. Unhead removes only the side effects it OWNS.
|
|
14
|
+
* The baked managed tags were written into the served HTML by the build-time
|
|
15
|
+
* emitter before any JavaScript ran, so Unhead never owned them and never
|
|
16
|
+
* removes them — which means a managed tag present in the BAKED head and ABSENT
|
|
17
|
+
* from the runtime entry is left stranded in the DOM for the rest of the
|
|
18
|
+
* session, while every tag the new route DOES declare moves correctly.
|
|
19
|
+
* Measured live across the fleet: a stale `meta keywords` on five sites and a
|
|
20
|
+
* stale `article:published_time` on iron-oak
|
|
21
|
+
* (`.docs/analysis/installseohead-defect-fa-explainer-2026-08-11.md` §2).
|
|
22
|
+
*
|
|
23
|
+
* THE FIX. On the FIRST matched in-app navigation, remove the baked managed
|
|
24
|
+
* tags that the incoming head input does not re-declare. From that moment the
|
|
25
|
+
* runtime is the sole owner of the managed head and Unhead's own reconciliation
|
|
26
|
+
* (proven correct by the C-362 fixture, on both shipped versions) handles every
|
|
27
|
+
* subsequent navigation. One sweep is therefore not a budget — it is
|
|
28
|
+
* SUFFICIENT, and §"WHY ONCE IS ENOUGH" below is the argument.
|
|
29
|
+
*
|
|
30
|
+
* ── THE FOUR REFUSALS ──────────────────────────────────────────────────────
|
|
31
|
+
*
|
|
32
|
+
* S1 NEVER SWEEP JSON-LD. This is the failure mode Q-453 named explicitly
|
|
33
|
+
* when it chose option A. `<script type="application/ld+json">` IS in the
|
|
34
|
+
* emitter's managed set, but the runtime DELIBERATELY never re-asserts it
|
|
35
|
+
* (the baked graph is built from inputs that do not exist at runtime:
|
|
36
|
+
* content.yaml reviews, business licence, FAQ pairs, breadcrumb trails).
|
|
37
|
+
* So a sweep that treated "managed" as "sweepable" would DELETE the graph
|
|
38
|
+
* outright and leave the page with none — strictly worse than the stale
|
|
39
|
+
* graph it replaced. Scripts are never snapshotted, so they are
|
|
40
|
+
* unreachable by this module rather than merely skipped.
|
|
41
|
+
*
|
|
42
|
+
* S2 NEVER SWEEP A TAG THE RUNTIME IS ABOUT TO DECLARE. The sweep set is the
|
|
43
|
+
* baked snapshot MINUS the identities in the incoming input. This is not
|
|
44
|
+
* an optimisation — it is what makes the sweep SAFE. Unhead reconciles
|
|
45
|
+
* against (and may adopt) a baked element whose identity appears in an
|
|
46
|
+
* entry; ripping such an element out from under Unhead would desync its
|
|
47
|
+
* side-effect bookkeeping and could leave the tag permanently missing.
|
|
48
|
+
* Excluding them means this module only ever touches elements Unhead has
|
|
49
|
+
* no entry for and therefore can never have adopted. It also means the
|
|
50
|
+
* swept set IS exactly the strandable set, by construction.
|
|
51
|
+
*
|
|
52
|
+
* S3 NEVER SWEEP `<title>`. Title is emitted unconditionally by
|
|
53
|
+
* `buildHeadTags`, so it is always re-asserted and can never strand; and
|
|
54
|
+
* it is the one managed tag with singleton semantics in the DOM and in
|
|
55
|
+
* `document.title`. It is excluded from the snapshot for the same reason
|
|
56
|
+
* as S1 — unreachable beats skipped.
|
|
57
|
+
*
|
|
58
|
+
* S4 THE SNAPSHOT IS TAKEN AT INSTALL TIME, NOT AT SWEEP TIME. "Baked" means
|
|
59
|
+
* "present in the document the browser fetched". `installSeoHead` runs at
|
|
60
|
+
* app entry, after `createHead()` but BEFORE `app.mount()` — and Unhead's
|
|
61
|
+
* DOM write is debounced to a later task — so `document.head` at that
|
|
62
|
+
* instant holds the served tags and nothing else. Snapshotting lazily on
|
|
63
|
+
* the first navigation would instead capture Unhead's own output and let
|
|
64
|
+
* the sweep delete live runtime tags. The install-time snapshot is what
|
|
65
|
+
* makes "the ones the static emitter baked" a fact rather than a guess.
|
|
66
|
+
*
|
|
67
|
+
* ── WHY ONCE IS ENOUGH ─────────────────────────────────────────────────────
|
|
68
|
+
* After the sweep, every managed element in the head is either (a) one the
|
|
69
|
+
* runtime just declared — Unhead owns it, and drops it on a later navigation
|
|
70
|
+
* that omits it (the RUNTIME->RUNTIME half, already proven working by ITEM 2b),
|
|
71
|
+
* or (b) gone. There is no third category left for a later navigation to
|
|
72
|
+
* strand, so a second sweep would have an empty set by definition. Sweeping
|
|
73
|
+
* repeatedly would also mean holding DOM references for the life of the
|
|
74
|
+
* session; the snapshot is released as soon as it is spent.
|
|
75
|
+
*
|
|
76
|
+
* ── WHAT CRAWLERS SEE: NOTHING CHANGES ─────────────────────────────────────
|
|
77
|
+
* The sweep can only fire on a MATCHED IN-APP NAVIGATION. `installSeoHead`'s R5
|
|
78
|
+
* already guarantees the fetched document is never written to, and a crawler or
|
|
79
|
+
* social scraper fetches each URL as an isolated page load and never performs an
|
|
80
|
+
* in-app navigation. The prerender path is likewise unaffected: the Playwright
|
|
81
|
+
* renderer loads each route as a fresh document, so no in-app navigation ever
|
|
82
|
+
* occurs and no sweep is reachable. Baked bytes are identical before and after
|
|
83
|
+
* this change — which is also why it cannot disturb the emitter-output pins in
|
|
84
|
+
* `runtimeVsBakedJsonLd.test.ts` (C-418): this module changes neither emitter.
|
|
85
|
+
*/
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* A stable identity for a managed head tag, shared by the DOM side and the
|
|
89
|
+
* head-input side so the two can be compared as sets.
|
|
90
|
+
*
|
|
91
|
+
* The shapes mirror how the head libraries dedupe:
|
|
92
|
+
* - `meta|name|<name>` — `description`, `keywords`, `twitter:*`
|
|
93
|
+
* - `meta|property|<property>` — `og:*`, `article:*`
|
|
94
|
+
* - `link|canonical` — the singleton canonical
|
|
95
|
+
* - `link|alternate|<hreflang>` — one per hreflang; the hreflang is part of
|
|
96
|
+
* the identity because a page may legitimately declare several alternates
|
|
97
|
+
* and they are only interchangeable within a single language.
|
|
98
|
+
*/
|
|
99
|
+
type ManagedTagIdentity = string;
|
|
100
|
+
/**
|
|
101
|
+
* The identities the runtime is ABOUT to assert — the S2 exclusion set.
|
|
102
|
+
*
|
|
103
|
+
* Deliberately typed against {@link HeadMetaTag}/{@link HeadLinkTag}, the exact
|
|
104
|
+
* structures `buildHeadTags` returns, so a future field on either flows through
|
|
105
|
+
* the same identity function the DOM side uses.
|
|
106
|
+
*/
|
|
107
|
+
declare function headInputIdentities(input: {
|
|
108
|
+
meta?: readonly HeadMetaTag[];
|
|
109
|
+
link?: readonly HeadLinkTag[];
|
|
110
|
+
}): Set<ManagedTagIdentity>;
|
|
111
|
+
/**
|
|
112
|
+
* Capture the managed tags the BUILD baked into the served document (S4).
|
|
113
|
+
*
|
|
114
|
+
* Call this at INSTALL time. `<title>` (S3) and `ld+json` scripts (S1) are not
|
|
115
|
+
* collected at all, so no later mistake can sweep them.
|
|
116
|
+
*
|
|
117
|
+
* Returns a plain array of live element references. It is empty — and the whole
|
|
118
|
+
* feature inert — when there is no document (SSR / node build context), which is
|
|
119
|
+
* the same defensive posture `readInjectedPages` takes.
|
|
120
|
+
*/
|
|
121
|
+
declare function snapshotBakedManagedHeadTags(doc?: Document): Element[];
|
|
122
|
+
/**
|
|
123
|
+
* Remove the baked managed tags the incoming head input does NOT re-declare.
|
|
124
|
+
*
|
|
125
|
+
* @param snapshot the install-time snapshot from
|
|
126
|
+
* {@link snapshotBakedManagedHeadTags}
|
|
127
|
+
* @param input the head payload about to be pushed to the head client — the S2
|
|
128
|
+
* exclusion set is derived from it
|
|
129
|
+
* @returns how many elements were actually removed, so the caller can REPORT
|
|
130
|
+
* the sweep instead of performing it silently
|
|
131
|
+
*
|
|
132
|
+
* Elements that have already left the document are skipped rather than removed
|
|
133
|
+
* again: a baked element can legitimately disappear between install and the
|
|
134
|
+
* first navigation (a view's own `useSEO` may have caused Unhead to reconcile
|
|
135
|
+
* it away), and "still connected" is the cheap, library-agnostic way to ask
|
|
136
|
+
* whether this module is still looking at the served DOM.
|
|
137
|
+
*/
|
|
138
|
+
declare function sweepBakedManagedHeadTags(snapshot: readonly Element[], input: {
|
|
139
|
+
meta?: readonly HeadMetaTag[];
|
|
140
|
+
link?: readonly HeadLinkTag[];
|
|
141
|
+
}): number;
|
|
142
|
+
|
|
8
143
|
/**
|
|
9
144
|
* Build-time BODY prerender for the Vue-SPA static-HTML emitter.
|
|
10
145
|
*
|
|
@@ -910,4 +1045,4 @@ declare class CharsetBudgetError extends Error {
|
|
|
910
1045
|
*/
|
|
911
1046
|
declare function formatCharsetHeadroomWarning(file: string, result: CharsetBudgetResult, warnBelow?: number): string | null;
|
|
912
1047
|
|
|
913
|
-
export { type BodyPrerenderEnableInput, BodyPrerenderError, CANONICAL_CHARSET_META, CHARSET_BUDGET_BYTES, CHARSET_HEADROOM_WARN_BYTES, CharsetBudgetError, type CharsetBudgetResult, type ClassifiedUrl, type EmittedUrl, type EmittedUrlAudit, EmittedUrlError, EmittedUrlNotRunError, type EmittedUrlViolation, type EmittedUrlWarning, type FilteredRoute, type ForeignWriterSite, HeadContractError, type HeadContractExemptionAudit, type HeadContractExemptionCheck, type HeadContractExemptionExpectation, type HeadContractExemptionRecord, type HeadContractExemptionValue, type HeadContractExemptionVerdict, type HeadContractHostProbe, type HeadContractInput, type HeadContractMode, type HeadContractReport, type HeadContractSeoConfig, type HeadContractSourceFile, type HeadContractViolation, type HeadContractViolationCode, type HeadContractWarning, type HeadContractWarningCode, HeadObservation, MIN_EXEMPTION_REASON_LENGTH, PageRouteEntry, type PrerenderBodiesParams, type PrerenderBodiesResult, type PrerenderFilterReason, type RemoteCheckOptions, type RemoteCheckResult, type RenderedHead, type RenderedRoute, type RouteRenderer, type SameOriginAuditResult, type UrlExpectation, type UrlLocality, assertHeadContract, auditCheckedTotal, auditExemptionHosts, auditHeadContract, auditSameOriginAssets, blankComments, checkCharsetBudget, checkRemoteAssets, checkSameOriginAssets, classifyUrl, collectUrlsFromHtml, collectUrlsFromLlmsTxt, collectUrlsFromSitemap, dedupeUrls, findCharsetByteOffset, formatCharsetHeadroomWarning, formatEmittedUrlAudit, formatEmittedUrlReport, formatExemptionAudit, formatHeadContractReport, hoistCharsetMeta, hostsNamedIn, isBodyPrerenderEnabled, normalizeExemption, parseHostProbeHtml, prerenderBodies, routeToOutputFile, spliceBodyHtml };
|
|
1048
|
+
export { type BodyPrerenderEnableInput, BodyPrerenderError, CANONICAL_CHARSET_META, CHARSET_BUDGET_BYTES, CHARSET_HEADROOM_WARN_BYTES, CharsetBudgetError, type CharsetBudgetResult, type ClassifiedUrl, type EmittedUrl, type EmittedUrlAudit, EmittedUrlError, EmittedUrlNotRunError, type EmittedUrlViolation, type EmittedUrlWarning, type FilteredRoute, type ForeignWriterSite, HeadContractError, type HeadContractExemptionAudit, type HeadContractExemptionCheck, type HeadContractExemptionExpectation, type HeadContractExemptionRecord, type HeadContractExemptionValue, type HeadContractExemptionVerdict, type HeadContractHostProbe, type HeadContractInput, type HeadContractMode, type HeadContractReport, type HeadContractSeoConfig, type HeadContractSourceFile, type HeadContractViolation, type HeadContractViolationCode, type HeadContractWarning, type HeadContractWarningCode, HeadLinkTag, HeadMetaTag, HeadObservation, MIN_EXEMPTION_REASON_LENGTH, PageRouteEntry, type PrerenderBodiesParams, type PrerenderBodiesResult, type PrerenderFilterReason, type RemoteCheckOptions, type RemoteCheckResult, type RenderedHead, type RenderedRoute, type RouteRenderer, type SameOriginAuditResult, type UrlExpectation, type UrlLocality, assertHeadContract, auditCheckedTotal, auditExemptionHosts, auditHeadContract, auditSameOriginAssets, blankComments, checkCharsetBudget, checkRemoteAssets, checkSameOriginAssets, classifyUrl, collectUrlsFromHtml, collectUrlsFromLlmsTxt, collectUrlsFromSitemap, dedupeUrls, findCharsetByteOffset, formatCharsetHeadroomWarning, formatEmittedUrlAudit, formatEmittedUrlReport, formatExemptionAudit, formatHeadContractReport, headInputIdentities, hoistCharsetMeta, hostsNamedIn, isBodyPrerenderEnabled, normalizeExemption, parseHostProbeHtml, prerenderBodies, routeToOutputFile, snapshotBakedManagedHeadTags, spliceBodyHtml, sweepBakedManagedHeadTags };
|
package/dist/seo/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { HeadContractError, MIN_EXEMPTION_REASON_LENGTH, assertHeadContract, auditExemptionHosts, auditHeadContract, blankComments, formatExemptionAudit, formatHeadContractReport, hostsNamedIn, normalizeExemption, parseHostProbeHtml } from '../chunk-
|
|
2
|
-
export { AI_BOTS, BodyPrerenderError, CANONICAL_CHARSET_META, CHARSET_BUDGET_BYTES, CHARSET_HEADROOM_WARN_BYTES, CharsetBudgetError, EmittedUrlError, EmittedUrlNotRunError, HeadHonestyError, HeadHonestyNotRunError, SEO_HEAD_ROUTE_HAS_NO_META, absolutizeUrl, assertHeadHonesty, assertHeadHonestyExecuted, auditCheckedTotal, auditSameOriginAssets, breadcrumbTrailFromRoute, buildBlogPosting, buildBreadcrumbList, buildFaqPage, buildGlobalGraph, buildHasCredential, buildHeadTags, buildLlmsTxt, buildReviewSchemaParts, buildRobotsTxt, buildSeoHeadRouteMap, buildSitemapXml, buildVitePressSeoHead, checkCharsetBudget, checkRemoteAssets, checkSameOriginAssets, classifyUrl, collectUrlsFromHtml, collectUrlsFromLlmsTxt, collectUrlsFromSitemap, createSeoTransformPageData, decodeHtmlEntities, dedupeUrls, defaultRelativePathToRoute, deriveSameAs, escapeJsonLd, extractBakedHead, filterRealFaq, filterRealReviews, findBusinessLicense, findCharsetByteOffset, findDuplicateNormalizedPaths, findHeadHonestyExecutionFaults, findHeadHonestyViolations, findLocalBusinessSchema, findReviewItemsForPage, formatCharsetHeadroomWarning, formatDuplicateNormalizedPaths, formatEmittedUrlAudit, formatEmittedUrlReport, formatHeadHonestyExecutionReport, formatHeadHonestyReport, generateJsonLd, generateOpenGraphMeta, generateTwitterMeta, graphAbsorbs, graphIds, hoistCharsetMeta, installSeoHead, isBodyPrerenderEnabled, isLocalBusinessType, isRouteEmitted, isRouteIndexable, loadPagesManifest, normalizeHeadText, normalizeSeoHeadPath, parsePagesManifest, prerenderBodies, renderHeadTags, resolveHonestyMode, resolvePageSeo, routeExclusionReason, routeToOutputFile, slugToTitle, spliceBodyHtml, spliceHeadHtml, stripManagedHeadTags } from '../chunk-
|
|
1
|
+
export { HeadContractError, MIN_EXEMPTION_REASON_LENGTH, assertHeadContract, auditExemptionHosts, auditHeadContract, blankComments, formatExemptionAudit, formatHeadContractReport, hostsNamedIn, normalizeExemption, parseHostProbeHtml } from '../chunk-OGAJX4KM.js';
|
|
2
|
+
export { AI_BOTS, BodyPrerenderError, CANONICAL_CHARSET_META, CHARSET_BUDGET_BYTES, CHARSET_HEADROOM_WARN_BYTES, CharsetBudgetError, EmittedUrlError, EmittedUrlNotRunError, HeadHonestyError, HeadHonestyNotRunError, SEO_HEAD_ROUTE_HAS_NO_META, absolutizeUrl, assertHeadHonesty, assertHeadHonestyExecuted, auditCheckedTotal, auditSameOriginAssets, breadcrumbTrailFromRoute, buildBlogPosting, buildBreadcrumbList, buildFaqPage, buildGlobalGraph, buildHasCredential, buildHeadTags, buildLlmsTxt, buildReviewSchemaParts, buildRobotsTxt, buildSeoHeadRouteMap, buildSitemapXml, buildVitePressSeoHead, checkCharsetBudget, checkRemoteAssets, checkSameOriginAssets, classifyUrl, collectUrlsFromHtml, collectUrlsFromLlmsTxt, collectUrlsFromSitemap, createSeoTransformPageData, decodeHtmlEntities, dedupeUrls, defaultRelativePathToRoute, deriveSameAs, escapeJsonLd, extractBakedHead, filterRealFaq, filterRealReviews, findBusinessLicense, findCharsetByteOffset, findDuplicateNormalizedPaths, findGlobalSchemasOfType, findHeadHonestyExecutionFaults, findHeadHonestyViolations, findLocalBusinessSchema, findReviewItemsForPage, formatCharsetHeadroomWarning, formatDuplicateNormalizedPaths, formatEmittedUrlAudit, formatEmittedUrlReport, formatHeadHonestyExecutionReport, formatHeadHonestyReport, generateJsonLd, generateOpenGraphMeta, generateTwitterMeta, graphAbsorbs, graphIds, headInputIdentities, hoistCharsetMeta, installSeoHead, isBodyPrerenderEnabled, isLocalBusinessType, isManagedLinkRel, isManagedMetaTag, isRouteEmitted, isRouteIndexable, loadPagesManifest, normalizeHeadText, normalizeSeoHeadPath, parsePagesManifest, prerenderBodies, renderHeadTags, resolveHonestyMode, resolvePageSeo, routeExclusionReason, routeToOutputFile, slugToTitle, snapshotBakedManagedHeadTags, spliceBodyHtml, spliceHeadHtml, stripManagedHeadTags, sweepBakedManagedHeadTags } from '../chunk-64BUBTW7.js';
|
|
3
3
|
//# sourceMappingURL=index.js.map
|
|
4
4
|
//# sourceMappingURL=index.js.map
|
|
@@ -426,8 +426,20 @@ declare function findLocalBusinessSchema(global: GlobalSeoConfig): {
|
|
|
426
426
|
* `Organization` / `WebSite` (the graph emits cross-linked versions of both).
|
|
427
427
|
*
|
|
428
428
|
* Shared by both emit paths so the de-dup rule is identical.
|
|
429
|
+
*
|
|
430
|
+
* INVARIANT (C-609): every schema this returns `true` for must be ABSORBED by
|
|
431
|
+
* `buildGlobalGraph` — its hand-authored properties merged into the corresponding
|
|
432
|
+
* `@graph` node. Absorbing here without merging there is silent data loss: the
|
|
433
|
+
* node is stripped from the per-schema emission and its properties never reach
|
|
434
|
+
* the crawler (the C-418 defect this pairing now prevents).
|
|
429
435
|
*/
|
|
430
436
|
declare function graphAbsorbs(schema: SeoSchemaConfig, global: GlobalSeoConfig): boolean;
|
|
437
|
+
/**
|
|
438
|
+
* All hand-authored `global.schemas` entries of an exact `@type`, in document
|
|
439
|
+
* order. Plural because `graphAbsorbs` absorbs EVERY `Organization` / `WebSite`
|
|
440
|
+
* entry — so the graph must merge every one of them or the extras vanish.
|
|
441
|
+
*/
|
|
442
|
+
declare function findGlobalSchemasOfType(global: GlobalSeoConfig, type: string): SeoSchemaConfig[];
|
|
431
443
|
/** One normalised, REAL review (passed the honesty gate). */
|
|
432
444
|
interface NormalisedReview {
|
|
433
445
|
rating: number;
|
|
@@ -489,10 +501,22 @@ declare function buildHasCredential(license: string | undefined): SchemaObject |
|
|
|
489
501
|
* `global.siteName` / `global.siteUrl` / `global.images.logo` / `global.social`.
|
|
490
502
|
* Requires NO new YAML.
|
|
491
503
|
*
|
|
492
|
-
*
|
|
493
|
-
*
|
|
494
|
-
*
|
|
495
|
-
*
|
|
504
|
+
* ALL THREE nodes ABSORB their hand-authored `global.schemas[*]` counterpart
|
|
505
|
+
* (C-609) — the properties are merged onto the derived node rather than
|
|
506
|
+
* discarded, which is what makes the baked `@graph` a true SUPERSET of the
|
|
507
|
+
* runtime per-schema emission that `graphAbsorbs` suppresses:
|
|
508
|
+
*
|
|
509
|
+
* • LocalBusiness — its NAP/geo/hours/offers are preserved verbatim and the
|
|
510
|
+
* node is promoted with an `@id`; honest `review` + `aggregateRating` and a
|
|
511
|
+
* `hasCredential` license are merged in when supplied.
|
|
512
|
+
* • Organization — e.g. the fleet default's hand-authored `logo`, which
|
|
513
|
+
* `global.images.logo` does not set.
|
|
514
|
+
* • WebSite — e.g. a `description` or a `potentialAction` SearchAction.
|
|
515
|
+
*
|
|
516
|
+
* Precedence is uniform: hand-authored properties OVERRIDE the derived value for
|
|
517
|
+
* the same key and ADD any key the graph does not derive, except the identity
|
|
518
|
+
* keys (`@type`, `@id`) and the spine's cross-links (`publisher`,
|
|
519
|
+
* `parentOrganization`), which the graph always owns so the `@id` refs resolve.
|
|
496
520
|
*
|
|
497
521
|
* Returns `[]` when there is no `siteUrl` (no stable `@id` anchor possible), so
|
|
498
522
|
* the existing per-schema emission is left untouched for un-configured sites.
|
|
@@ -1120,4 +1144,4 @@ declare function buildVitePressSeoHead(pageData: VitePressPageData, options: Cre
|
|
|
1120
1144
|
*/
|
|
1121
1145
|
declare function createSeoTransformPageData(options: CreateSeoTransformPageDataOptions): (pageData: VitePressPageData) => void;
|
|
1122
1146
|
|
|
1123
|
-
export { parsePagesManifest as $, AI_BOTS as A, type BuildSitemapParams as B, type CreateSeoTransformPageDataOptions as C, type DcsRobotsOptions as D, type ContentConfig as E, type FaqSource as F, type GlobalSeoConfig as G, type SeoConfiguration as H, type SeoAuthorConfig as I, type SeoSocialConfig as J, type SeoImagesConfig as K, type SeoOpenGraphConfig as L, type SeoTwitterConfig as M, type SeoSchemaConfig as N, type SeoAlternateConfig as O, type PageSeoConfig as P, type SeoVerificationConfig as Q, type ResolvedPageOverrides as R, type SeoPageContext as S, type ResolvedPageSeo as T, type UseSeoReturn as U, type VitePressPageData as V, type UseSeoConfig as W, type HeadOverrides as X, type SeoHonestyBlock as Y, type PageRouteEntry as Z, loadPagesManifest as _, buildSitemapXml as a, isRouteEmitted as a0, routeExclusionReason as a1, type RouteEmissionRules as a2, type RouteExclusionReason as a3, buildHasCredential as a4, findLocalBusinessSchema as a5,
|
|
1147
|
+
export { parsePagesManifest as $, AI_BOTS as A, type BuildSitemapParams as B, type CreateSeoTransformPageDataOptions as C, type DcsRobotsOptions as D, type ContentConfig as E, type FaqSource as F, type GlobalSeoConfig as G, type SeoConfiguration as H, type SeoAuthorConfig as I, type SeoSocialConfig as J, type SeoImagesConfig as K, type SeoOpenGraphConfig as L, type SeoTwitterConfig as M, type SeoSchemaConfig as N, type SeoAlternateConfig as O, type PageSeoConfig as P, type SeoVerificationConfig as Q, type ResolvedPageOverrides as R, type SeoPageContext as S, type ResolvedPageSeo as T, type UseSeoReturn as U, type VitePressPageData as V, type UseSeoConfig as W, type HeadOverrides as X, type SeoHonestyBlock as Y, type PageRouteEntry as Z, loadPagesManifest as _, buildSitemapXml as a, isRouteEmitted as a0, routeExclusionReason as a1, type RouteEmissionRules as a2, type RouteExclusionReason as a3, buildHasCredential as a4, findLocalBusinessSchema as a5, findGlobalSchemasOfType as a6, graphAbsorbs as a7, findBusinessLicense as a8, deriveSameAs as a9, graphIds as aa, isLocalBusinessType as ab, type NormalisedReview as ac, type NormalisedFaq as ad, type ReviewSchemaParts as ae, buildVitePressSeoHead as b, createSeoTransformPageData as c, defaultRelativePathToRoute as d, buildRobotsTxt as e, buildLlmsTxt as f, buildGlobalGraph as g, buildBreadcrumbList as h, isRouteIndexable as i, breadcrumbTrailFromRoute as j, buildBlogPosting as k, buildFaqPage as l, buildReviewSchemaParts as m, filterRealReviews as n, filterRealFaq as o, findReviewItemsForPage as p, absolutizeUrl as q, type SeoPageTypeRule as r, slugToTitle as s, type VitePressHeadConfig as t, type BuildRobotsParams as u, type BuildLlmsParams as v, type SchemaObject as w, type BreadcrumbCrumb as x, type BlogMeta as y, type ReviewSource as z };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@duffcloudservices/cms",
|
|
3
|
-
"version": "0.13.
|
|
3
|
+
"version": "0.13.2",
|
|
4
4
|
"description": "Vue 3 composables and Vite plugins for DCS CMS integration",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -58,7 +58,7 @@
|
|
|
58
58
|
},
|
|
59
59
|
"dependencies": {
|
|
60
60
|
"js-yaml": "^4.1.0",
|
|
61
|
-
"@duffcloudservices/cms-core": "0.8.
|
|
61
|
+
"@duffcloudservices/cms-core": "0.8.2"
|
|
62
62
|
},
|
|
63
63
|
"devDependencies": {
|
|
64
64
|
"@types/js-yaml": "^4.0.9",
|
|
@@ -84,9 +84,18 @@
|
|
|
84
84
|
</template>
|
|
85
85
|
|
|
86
86
|
<script setup lang="ts">
|
|
87
|
-
import { platformFetch } from '@duffcloudservices/cms-core'
|
|
88
87
|
import { ref, computed, onMounted, onBeforeUnmount, watch } from 'vue'
|
|
89
88
|
|
|
89
|
+
/*
|
|
90
|
+
* NO cms-core VALUE imports in this file. The `./components` export ships RAW SFC
|
|
91
|
+
* source (tsup builds no .vue), so consumer sites type-check this file directly —
|
|
92
|
+
* and cms-core is a transitive dep pnpm's strict layout never exposes to them.
|
|
93
|
+
* A value import here is TS2307 in every fleet site's type-check gate (measured on
|
|
94
|
+
* the 0.13.0 canary, 2026-08-10; kept/boogie-babies/bryans/mi-handyman all red).
|
|
95
|
+
* `import type` is erased and safe. The C-298 layer-2 guard this file needs from
|
|
96
|
+
* platformFetch is inlined below at its single call site instead.
|
|
97
|
+
*/
|
|
98
|
+
|
|
90
99
|
/* ------------------------------------------------------------------ */
|
|
91
100
|
/* Props */
|
|
92
101
|
/* ------------------------------------------------------------------ */
|
|
@@ -235,15 +244,22 @@ onMounted(async () => {
|
|
|
235
244
|
const base =
|
|
236
245
|
(import.meta.env as Record<string, string | undefined>)
|
|
237
246
|
.VITE_API_BASE_URL ?? 'https://portal.duffcloudservices.com'
|
|
238
|
-
// C-298 layer 2
|
|
239
|
-
//
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
)
|
|
247
|
+
// C-298 layer 2, INLINED (see the no-cms-core-value-imports note at the top of
|
|
248
|
+
// this <script>): the `catch { /* non-critical */ }` below is a swallow site, so
|
|
249
|
+
// a non-JSON (HTML-shell) body must go LOUD here before it can be swallowed.
|
|
250
|
+
const url = `${base}/api/v1/release-notes/latest`
|
|
251
|
+
const res = await fetch(url, { headers: { Accept: 'application/json' } })
|
|
244
252
|
if (res.ok) {
|
|
245
|
-
const
|
|
246
|
-
if (
|
|
253
|
+
const ct = res.headers?.get?.('content-type') ?? ''
|
|
254
|
+
if (!/\bjson\b/i.test(ct)) {
|
|
255
|
+
console.error(
|
|
256
|
+
`[dcs-cms] PreviewRibbon: ${url} returned content-type "${ct}" — ` +
|
|
257
|
+
'an HTML body here means a platform misroute (C-298); refusing to parse it.',
|
|
258
|
+
)
|
|
259
|
+
} else {
|
|
260
|
+
const data = await res.json()
|
|
261
|
+
if (data.version) resolvedVersion.value = data.version
|
|
262
|
+
}
|
|
247
263
|
}
|
|
248
264
|
} catch {
|
|
249
265
|
/* non-critical */
|
|
@@ -45,3 +45,48 @@ describe('ResponsiveImage self-healing fallback', () => {
|
|
|
45
45
|
expect(wrapper.find('img').attributes('src')).toBe(ORIGINAL)
|
|
46
46
|
})
|
|
47
47
|
})
|
|
48
|
+
|
|
49
|
+
describe('ResponsiveImage source-fit variants (C-804 / Q-561=A)', () => {
|
|
50
|
+
it('omits variants wider than the source once width/height are supplied', () => {
|
|
51
|
+
// The iron-oak banner shape: a 1024x212 source must stop advertising 1920w.
|
|
52
|
+
const wrapper = mount(ResponsiveImage, {
|
|
53
|
+
props: { src: ORIGINAL, alt: 'Banner', context: 'hero', width: 1024, height: 212 },
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
const srcset = wrapper.find('source').attributes('srcset')!
|
|
57
|
+
expect(srcset).toContain('abc-123-sm.webp 640w')
|
|
58
|
+
expect(srcset).toContain('abc-123-md.webp 1024w')
|
|
59
|
+
expect(srcset).not.toContain('-lg.webp')
|
|
60
|
+
expect(srcset).not.toContain('1920w')
|
|
61
|
+
|
|
62
|
+
const img = wrapper.find('img')
|
|
63
|
+
expect(img.attributes('src')).toContain('abc-123-md.webp')
|
|
64
|
+
// The same props also feed the pre-existing CLS hint, which this component
|
|
65
|
+
// could not emit at all before it grew width/height.
|
|
66
|
+
expect(img.attributes('width')).toBe('1024')
|
|
67
|
+
expect(img.attributes('height')).toBe('212')
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
it('re-points the <img> fallback when the source is narrower than -md', () => {
|
|
71
|
+
const wrapper = mount(ResponsiveImage, {
|
|
72
|
+
props: { src: ORIGINAL, alt: 'Narrow', context: 'content', width: 800 },
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
expect(wrapper.find('source').attributes('srcset')).toBe(
|
|
76
|
+
'https://files.duffcloudservices.com/kept/assets/hero/abc-123-sm.webp 640w'
|
|
77
|
+
)
|
|
78
|
+
expect(wrapper.find('img').attributes('src')).toContain('abc-123-sm.webp')
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
it('is byte-for-byte unchanged when no width is supplied', () => {
|
|
82
|
+
const wrapper = mount(ResponsiveImage, {
|
|
83
|
+
props: { src: ORIGINAL, alt: 'Hero', context: 'hero' },
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
expect(wrapper.find('source').attributes('srcset')).toBe(
|
|
87
|
+
'https://files.duffcloudservices.com/kept/assets/hero/abc-123-sm.webp 640w, ' +
|
|
88
|
+
'https://files.duffcloudservices.com/kept/assets/hero/abc-123-md.webp 1024w, ' +
|
|
89
|
+
'https://files.duffcloudservices.com/kept/assets/hero/abc-123-lg.webp 1920w'
|
|
90
|
+
)
|
|
91
|
+
})
|
|
92
|
+
})
|
|
@@ -32,6 +32,16 @@ const props = defineProps<{
|
|
|
32
32
|
sizes?: string
|
|
33
33
|
/** Skip responsive variants and use the original URL only. */
|
|
34
34
|
original?: boolean
|
|
35
|
+
/**
|
|
36
|
+
* Intrinsic pixel width of the SOURCE image. Two effects (both opt-in — with
|
|
37
|
+
* no `width` the rendered markup is unchanged):
|
|
38
|
+
* - C-804 source-fit srcset: variants wider than the source are omitted, so
|
|
39
|
+
* a narrow banner stops advertising a width it cannot deliver.
|
|
40
|
+
* - paired with `height`, emits the `<img width>/<img height>` CLS hint.
|
|
41
|
+
*/
|
|
42
|
+
width?: number
|
|
43
|
+
/** Intrinsic pixel height of the source image. See `width`. */
|
|
44
|
+
height?: number
|
|
35
45
|
}>()
|
|
36
46
|
|
|
37
47
|
const image = useResponsiveImage({
|
|
@@ -40,6 +50,8 @@ const image = useResponsiveImage({
|
|
|
40
50
|
context: () => props.context ?? 'content',
|
|
41
51
|
sizes: () => props.sizes,
|
|
42
52
|
original: () => props.original,
|
|
53
|
+
width: () => props.width,
|
|
54
|
+
height: () => props.height,
|
|
43
55
|
})
|
|
44
56
|
|
|
45
57
|
// Self-heal: when the rendered fit-variant (`-md`/`-lg`) 404s — legitimately
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { readFileSync, readdirSync } from 'node:fs'
|
|
3
|
+
import { join } from 'node:path'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The `./components` export ships RAW SFC source (tsup builds no .vue), so every
|
|
7
|
+
* consumer site type-checks these files directly — against a dependency tree where
|
|
8
|
+
* cms-core is TRANSITIVE and pnpm's strict layout never exposes it. A VALUE import
|
|
9
|
+
* of cms-core in any raw-shipped .vue is therefore TS2307 in every fleet site's
|
|
10
|
+
* type-check gate, invisible to this package's own checks (cms-core resolves fine
|
|
11
|
+
* HERE — that is exactly why only a consumer-shaped gate or this test can see it).
|
|
12
|
+
*
|
|
13
|
+
* Measured on the 0.13.0 canary (2026-08-10): one value import in PreviewRibbon.vue
|
|
14
|
+
* would have redded CI on kept (the payer), boogie-babies, bryans and mi-handyman.
|
|
15
|
+
* `import type` is erased at compile time and stays allowed.
|
|
16
|
+
*/
|
|
17
|
+
describe('raw-source .vue exports must not value-import cms-core', () => {
|
|
18
|
+
const dir = __dirname
|
|
19
|
+
const vueFiles = readdirSync(dir).filter((f) => f.endsWith('.vue'))
|
|
20
|
+
|
|
21
|
+
it('finds the raw-shipped component set (non-vacuity)', () => {
|
|
22
|
+
expect(vueFiles.length).toBeGreaterThan(0)
|
|
23
|
+
expect(vueFiles).toContain('PreviewRibbon.vue')
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
for (const file of vueFiles) {
|
|
27
|
+
it(`${file} has no cms-core value import`, () => {
|
|
28
|
+
const src = readFileSync(join(dir, file), 'utf8')
|
|
29
|
+
const valueImports = Array.from(src.matchAll(/^\s*import\s+(?!type\b)[^;'"]*['"]@duffcloudservices\/cms-core['"]/gm))
|
|
30
|
+
expect(
|
|
31
|
+
valueImports.map((m) => m[0].trim()),
|
|
32
|
+
`${file} value-imports cms-core — this ships as raw source and breaks every consumer site's type-check (see header comment)`,
|
|
33
|
+
).toEqual([])
|
|
34
|
+
})
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
it('kill-test: the guard regex catches the 0.13.0 defect shape', () => {
|
|
38
|
+
const bad = "import { platformFetch } from '@duffcloudservices/cms-core'"
|
|
39
|
+
const good = "import type { PlatformFetchOptions } from '@duffcloudservices/cms-core'"
|
|
40
|
+
const re = /^\s*import\s+(?!type\b)[^;'"]*['"]@duffcloudservices\/cms-core['"]/gm
|
|
41
|
+
expect(Array.from(bad.matchAll(re)).length).toBe(1)
|
|
42
|
+
expect(Array.from(good.matchAll(re)).length).toBe(0)
|
|
43
|
+
})
|
|
44
|
+
})
|