@getvitops/astro 8.1.0 → 8.2.0-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,332 @@
1
1
  # @getvitops/astro
2
2
 
3
+ ## 8.2.0-alpha.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 65a13cc: **New: `site.experiments`** — runtime, per-visitor A/B content experiments for a page that
8
+ must stay at one URL (unlike `site.abTesting`'s existing build-time variant merge, which is
9
+ for a full design-system/CSS-level test via two manual deploys — `site.experiments` is the
10
+ opposite granularity: one page, one cookie, content branched in the page itself). Keyed by
11
+ an open experiment name, so more than one can run at once, each with its own consent
12
+ category, cookie name/lifetime, split ratio and pair of variant names.
13
+
14
+ **New `vitopsAbTesting()` middleware** (`@getvitops/astro/middleware`) — the first Astro
15
+ middleware this toolchain ships. Wire it into your own `src/middleware.ts`:
16
+
17
+ ```ts
18
+ import { vitopsAbTesting } from '@getvitops/astro/middleware';
19
+ import config from '../site.json' with { type: 'json' };
20
+
21
+ export const onRequest = vitopsAbTesting({
22
+ experiments: config.site.experiments ?? {},
23
+ });
24
+ ```
25
+
26
+ It assigns and cookie-pins a visitor to one of an experiment's two variants on first visit,
27
+ by weighted random (`splitRatio`) — and never touches an already-set cookie, so a recognized
28
+ query parameter named after the cookie itself (`?_ab_<key>=<variant>`) reliably forces that
29
+ variant, even overriding an existing assignment. Combined with `splitRatio: 0`, this is a
30
+ blue/green-style "100% on control, but a shared link reveals the other variant" mode with no
31
+ special-cased logic.
32
+
33
+ A page reads its own experiment's cookie (`Astro.cookies.get(cookieName)?.value`) and
34
+ renders whichever of its two hand-written blocks matches — there's no config-schema
35
+ representation of "the variant content," which is what keeps this lightweight regardless of
36
+ how many experiments are configured.
37
+
38
+ **`<Analytics />` gained a reporting hook**: each configured provider's bootstrap now
39
+ attaches an enabled experiment's assignment to the real pageview via that provider's own
40
+ mechanism — Plausible via `plausible.init({customProperties})`, Matomo via
41
+ `setCustomDimension` (needs a new `site.analytics.matomo.abVariantDimensions` entry — the
42
+ dimension must exist in your Matomo instance first), GA4 via a runtime-merged
43
+ `gtag('config', …)` call. A no-op when `site.experiments` is unset — zero behavior change
44
+ for every existing site.
45
+
46
+ Also new: `experimentDimensionWarnings(site)` (`@getvitops/generator`) — a build-time warning
47
+ (not a hard error) when an enabled experiment's site also configures Matomo but has no
48
+ matching `abVariantDimensions` entry.
49
+
50
+ **Not included in this pass, deliberately**: a mechanism for a fully prerendered
51
+ (`output: 'static'`) page — this needs a genuinely separate Cloudflare Worker Route + two
52
+ pre-built static variants, real but bounded follow-up work. Everything above applies to an
53
+ on-demand-rendered page (the current EmDash templates' default), which needs zero new
54
+ deploy infrastructure.
55
+
56
+ - 0574388: Add `site.crm` — push form submissions into HubSpot, Zoho Bigin, or Jobber.
57
+
58
+ Client sites need form submissions to land in the client's CRM. The options available before this
59
+ — a HubSpot/Bigin/Jobber iframe or JS embed — fail on three counts: the markup can't be
60
+ white-labelled against this design system, nothing can operate on the submission before it leaves
61
+ the page, and the submission never touched `createConversionRoute()`, so `_ac` ad-click attribution
62
+ was lost and no conversion notification was planned.
63
+
64
+ This adds one server-side path for all three CRMs, hung off the conversion route that already
65
+ exists. `createConversionRoute()` gets a new `crm` option (`{ config, credentials, fetch?,
66
+ timeoutMs? }`); credentials arrive as a function of `locals`, exactly like the existing `binding`
67
+ option, and are never config. A failed CRM write never fails the request and is folded into the
68
+ notification email's new "Delivery" section, so a broken CRM token is never silent.
69
+
70
+ New `@getvitops/utils/crm` subpath: `deliverToCrm()`, `mapSubmission()`, `deriveLastName()` (Bigin
71
+ requires a last name; this derives one from an explicit field, a full name, or an email address,
72
+ refusing rather than fabricating one), `submitToHubSpot()`, `submitToBigin()`, `submitToJobber()`.
73
+ `identifySubmitter()` was promoted out of `notify`'s private name-guessing cascade into
74
+ `@getvitops/utils/tracking`, since the CRM mapper needed the whole identity (first/last name,
75
+ email, phone, company), not just a display name.
76
+
77
+ Jobber is GraphQL, not REST, and answers `200 OK` for failures — a rejected `clientCreate` returns
78
+ `userErrors[]`, a malformed request returns top-level `errors[]`. `submitToJobber()` classifies on
79
+ the response body, not just the status. Its refresh tokens also rotate by default: **Refresh Token
80
+ Rotation must be turned off** in the app's Developer Center settings, since a Worker reading a
81
+ static `VITOPS_JOBBER_REFRESH_TOKEN` cannot persist a rotated one — a rotated token is detected at
82
+ exchange time and reported with the fix, rather than surfacing an hour later as an opaque 401.
83
+ Attribution and unmapped extra fields render into a best-effort note by default (works on every
84
+ Jobber plan); a resubmitting visitor creates a second Jobber client unless `crm.jobber.dedupe: true`
85
+ is set once its lookup query has been verified against a live account (defaults `false` pending
86
+ that verification — see `jobber.ts`'s docblock).
87
+
88
+ New `site.crm` config block (`provider`, `hubspot`/`bigin`/`jobber`, `fieldMap`, `trackingFields`)
89
+ and two CLI commands: `vitops crm check` (fetches the live HubSpot property list or Bigin field
90
+ list and reports every mapped name that doesn't exist there — HubSpot rejects an unknown field
91
+ outright, Bigin silently drops it; Jobber has nothing live to check yet, since its mapped values
92
+ currently render as note text) and `vitops crm setup` (creates missing HubSpot properties; Bigin
93
+ and Jobber have no such API, so both always print a manual reminder).
94
+
95
+ The generated privacy policy now names HubSpot, Zoho Bigin, or Jobber when `site.crm` configures
96
+ one. Bigin gets the same cross-border-transfer derivation Matomo Cloud already gets: a
97
+ `dataCenter: 'ca'` asserts no transfer outside Canada, any other data centre does. Jobber's
98
+ operator (Octopusapp Inc. d/b/a Jobber) is Canadian, but its own privacy policy names the United
99
+ States as a processing location too, so the policy always asserts a cross-border transfer for
100
+ Jobber regardless of config — a Canadian operator is not read as Canadian-only storage.
101
+
102
+ Credentials: `VITOPS_HUBSPOT_ACCESS_TOKEN` for HubSpot; `VITOPS_ZOHO_CLIENT_ID` /
103
+ `VITOPS_ZOHO_CLIENT_SECRET` / `VITOPS_ZOHO_REFRESH_TOKEN` for Bigin; `VITOPS_JOBBER_CLIENT_ID` /
104
+ `VITOPS_JOBBER_CLIENT_SECRET` / `VITOPS_JOBBER_REFRESH_TOKEN` for Jobber. Never config.
105
+
106
+ No consumer-facing change in `@getvitops/astro` beyond the new `crm` route option and a build
107
+ warning when `site.crm` is configured with no route answering `TRACKING_ENDPOINT` — CSS/Bricks/
108
+ tailwind outputs are unaffected.
109
+
110
+ - 0574388: Fire a Google Ads conversion on a tel:/mailto: click, and fix a rate-limit bug on the conversion
111
+ route that could silently drop one of the two.
112
+
113
+ `site.ads.google.conversionLabel` used to be accepted by the config schema and typed all the way
114
+ through to the rendered tag, but nothing actually read it — no `gtag('event', 'conversion', …)`
115
+ was ever emitted. When set, the click tracker `<Tracking />` inlines now fires the conversion at
116
+ the moment a visitor taps a `tel:` or `mailto:` link, using the `AW-…/label` target Google Ads
117
+ expects. A `conversionLabel` that can't safely land inside the inline bootstrap script (the same
118
+ check `pixelId`/`accountId` already get) is dropped with a warning rather than firing something
119
+ malformed — the ad tag itself still loads.
120
+
121
+ Also: `createConversionRoute()`'s rate limiter previously keyed on IP address alone, so a call
122
+ beacon and an email-click beacon from the same visitor, seconds apart, competed for one window —
123
+ the second could 429 and vanish with no error anywhere. It's now keyed on IP _and_ event type, so
124
+ distinct conversion types never starve each other; repeats of the same type are still throttled
125
+ exactly as before.
126
+
127
+ No config changes — a site with `site.ads.google.conversionLabel` already set picks this up
128
+ automatically on the next build.
129
+
130
+ - 2a0a559: The Astro integration now warns when `site.locales`/`site.defaultLocale` disagree with
131
+ `astro.config.mjs`'s own `i18n` block — naming which locale codes are missing from each side, and
132
+ when the two `defaultLocale`s differ. EmDash reads its locale list straight from `i18n`, while our
133
+ own SEO/legal code reads `site.locales`, so the two had no way to notice they'd drifted apart. A
134
+ warning, not a build error — like the existing ads/analytics/consent consistency checks, either
135
+ side can legitimately be configured without the other, so only a genuine disagreement is flagged.
136
+ - 0574388: Track `mailto:` link clicks as conversions, alongside the existing `tel:` link tracking.
137
+
138
+ Before this, the capture script only beaconed `tel:` clicks; a `mailto:` link on the same page was
139
+ invisible to attribution — a visitor who emailed the business instead of calling generated no
140
+ conversion event at all. `<Tracking />`'s inlined script now listens for both, sending `{ event:
141
+ 'email', email }` via `navigator.sendBeacon` the same way it already sends `{ event: 'call', phone
142
+ }`.
143
+
144
+ `ConversionEvent['type']` gains an `'email'` member alongside `'form'`/`'call'`, with a new
145
+ `email` field. `createConversionRoute()` handles it exactly like a call: no honeypot, Turnstile or
146
+ CRM checks (there are no form fields to validate or map), a 204 response, and a notification email
147
+ subject reading `[Email Click] Visitor emailing ...`.
148
+
149
+ No config changes and no new options — a site with `<Tracking />` already mounted picks this up
150
+ automatically the next time it rebuilds.
151
+
152
+ - dde3df3: **`site.ads.google.conversionLabel` now accepts a per-action map** —
153
+ `{ call?, email?, form? }` — instead of one bare string shared by every
154
+ interaction. A bare string still works (back-compat, treated as `call`), but a
155
+ call and an email click used to report as the **same** Google Ads conversion
156
+ action, indistinguishable in the account; each action now gets its own
157
+ `send_to` target.
158
+
159
+ **New: `vitopsFormConversion(formEl)`**, exposed as `window.vitopsFormConversion`
160
+ by `<Tracking />`'s capture script — call it from your own fetch-based form
161
+ submit handler once the POST to `createConversionRoute()` succeeds, to fire the
162
+ `form` Google Ads conversion action client-side (mirroring the existing
163
+ tel:/mailto: click handler, which now fires `call`/`email` instead of sharing
164
+ one label). Idempotent per form element. There's deliberately no server-side
165
+ equivalent for a no-JS `<form method="post">` submission — that's a full-page
166
+ redirect with no script to fire from, and a real server-side Google Ads
167
+ conversion needs the Ads API's click-conversion upload (developer token + MCC
168
+ access), tracked separately in the vNext backlog.
169
+
170
+ **Fixed: the conversion route's rate limiter no longer trusts the client's
171
+ raw `event` field.** It used to key on whatever string a client sent
172
+ (`` `${ip}:${body.event}` ``), so varying that value on every request let a
173
+ form submission dodge the per-IP rate limit entirely — only `'call'`/`'email'`
174
+ were ever recognized, and anything else got its own fresh bucket. The key now
175
+ uses the server's own classification (`call` | `email` | `form`), closing that
176
+ bypass without changing the existing call-vs-email-don't-compete behavior.
177
+
178
+ **New: `site.notifications.actionUrl`** — an optional action-link template
179
+ added to the conversion-notification email as an "Actions" section.
180
+ `{clickId}` is replaced with the ad click ID captured on `_ac` (gclid, fbclid,
181
+ etc.); the section is omitted when the event has no click ID to interpolate,
182
+ rather than emailing a broken link.
183
+
184
+ - 3bbb8c3: Add `toNotifyContext(config, locale?)` and `toCrmConfig(config)`, exported from
185
+ `@getvitops/astro/routes` beside `createConversionRoute()`. Builds the `context`/`crm.config`
186
+ options straight from a resolved `Config` instead of hand-writing a `NotifyContext` literal —
187
+ `conversion.ts`'s own docblock used to state this helper "does not exist."
188
+
189
+ `organization.name` is localizable; `toNotifyContext` resolves it for a given locale (default
190
+ `site.defaultLocale`), falling back to `legalName` when neither locale nor default resolves to
191
+ anything. `locationEmails` is ordered primary location first, then every other location with an
192
+ `email` set, in `organization.locations`' own key order.
193
+
194
+ ### Patch Changes
195
+
196
+ - c3c1bcb: **New: the design-system editor, mounted inside the EmDash admin.** `vitopsEmdash({
197
+ designSystemEditor: { config } })` (`config` is your `site.json`, e.g. `import config from
198
+ './site.json' with { type: 'json' }`) adds a `/design-system` admin page mounting
199
+ `<wc-theme-editor>` — the same live token editor `@getvitops/vite`'s dev server has always
200
+ offered, now usable by the client directly, no dev server required. An edit is visible
201
+ site-wide immediately (a runtime `:root` override, served via a new plugin API route and
202
+ `page:fragments`) and — once `site.deployment.repo`/`branch` and a `VITOPS_GITHUB_TOKEN`
203
+ secret are set (see the scaffold's `SETUP.html`) — auto-commits to your `dev` branch, so it
204
+ reaches the git-tracked `site.json` through your repo's normal review path rather than
205
+ becoming a second, unreviewed source of truth. Until that's configured, the live half still
206
+ works; only the auto-commit half is skipped (and says so in the admin page). The save route
207
+ runs the exact same accessibility (APCA contrast) gate `generate()` enforces at build time,
208
+ synchronously, before persisting — nothing non-compliant ever goes live even momentarily.
209
+
210
+ Requires `@astrojs/react` + `react` + `react-dom` (already scaffold dependencies, for
211
+ EmDash's own admin) and `@getvitops/core` (added as a new scaffold dependency this release).
212
+ Declares two new plugin capabilities when enabled: `network:request` (for the GitHub
213
+ auto-commit) and `hooks.page-fragments:register` (for the live override) — reviewed once at
214
+ plugin-install time.
215
+
216
+ **Breaking for `@getvitops/emdash`**: `vitopsEmdash()`'s `hooks.page-fragments:register`
217
+ capability is now also granted whenever `designSystemEditor` is set, independent of the
218
+ existing `scripts: 'fragments'` option (which governs something unrelated — web-component
219
+ script delivery). No change if you don't use `designSystemEditor`.
220
+
221
+ Also new, supporting the above:
222
+ - `@getvitops/generator`: `validateColorContract()` and `buildReverseIndex()` (both
223
+ `/tokens` subpath, Worker-safe) — the monotonicity/contrast gate and the
224
+ `design-manifest.json` reverse-index, each extracted from `generate()`'s `build()` into
225
+ standalone, non-throwing functions. `patchToOverrideCss()` turns a design-system patch
226
+ into the `:root` override `<style>` block. `designSystemPath`/`getAt`/`setAt`/
227
+ `mergePatch`/`formatConfig` moved from `@getvitops/vite` into `@getvitops/generator/config`
228
+ (re-exported from `@getvitops/vite` unchanged) — one "patch a config file's design-system
229
+ subtree" implementation shared by the dev endpoint and the new plugin route. New
230
+ `site.deployment.repo`/`branch` config fields.
231
+ - `@getvitops/utils`: new `/git` subpath — `commitFile()`, a Worker-safe (fetch-only)
232
+ wrapper over GitHub's Contents API.
233
+ - `@getvitops/core`: `<wc-theme-editor>` gains a `save-endpoint` attribute (default
234
+ unchanged — `/__vitops/design-system`) so the same element can target the new plugin
235
+ route instead of the dev-only Vite endpoint.
236
+
237
+ - 0574388: Bump `emdash`/`@emdash-cms/cloudflare` peers to `>=1.0.0 <2` and `vite-plus` to `^1.0.0`, following
238
+ both projects' GA releases.
239
+
240
+ **Breaking for `@getvitops/emdash` consumers on EmDash 0.x**: the peer range no longer admits
241
+ `emdash@0.31.x`/`@emdash-cms/cloudflare@0.31.x`. Upgrade `emdash` and `@emdash-cms/cloudflare` to
242
+ `^1.0.1` first — nothing in `@getvitops/emdash`'s own plugin surface (`admin.portableTextBlocks`,
243
+ the `hooks.page-fragments:register` capability, `definePlugin()`) changed against EmDash 1.0, so no
244
+ code here needed to change, only the declared range.
245
+
246
+ The `emdash` scaffold template (`vp create @getvitops:emdash`) picks up several fixes alongside
247
+ the version bump:
248
+ - `tailwindcss`/`@tailwindcss/vite` are now real dependencies — the template selected
249
+ `css.format: 'tailwind'` without installing either, which threw on build.
250
+ - `site.json` now sets `site.icons.sprite: true` plus the full `semantic` list the
251
+ `vitops.actionLink` block's icon picker offers — previously its sprite icons pointed at a file
252
+ the build never generated.
253
+ - `astro.config.mjs` sets `migrations: { runtime: 'check', dev: 'auto' }`, and
254
+ `site-deploy-emdash.yml` runs EmDash 1.0's documented `build → status → apply → deploy → check`
255
+ migration sequence explicitly, rather than relying solely on first-request auto-migration. This
256
+ needs a new GitHub Environment variable, `EMDASH_TARGET_FINGERPRINT` (one per environment),
257
+ which only a human can set — run `emdash migrate --status` against the real deployed database,
258
+ review the printed identity, then `gh variable set EMDASH_TARGET_FINGERPRINT --env <env> --body
259
+ <value>`. Deploys fail clearly on the migration-apply step until it's set.
260
+ - `lint.options.typeAware`/`typeCheck` in the template's own `vite.config.ts` are commented out,
261
+ not deleted: vite-plus 1.0 only honours that option in a project's actual root config, and hard
262
+ errors on it in a nested one (which this file is, sitting inside the `design-system` monorepo).
263
+ Uncomment it once scaffolded into its own repo.
264
+
265
+ No config schema changes. `@getvitops/generator` also switches from the `zod/mini` subpath import
266
+ to the standalone `@zod/mini` package (zod 4.5+; same API) and fixes `validateConfig()` silently
267
+ losing the offending key name on an `unrecognized_keys` error (e.g. a typo'd `site.ads` platform
268
+ now names the bad key instead of reporting a bare "Invalid input").
269
+
270
+ Also fixed: `vp check`'s `tsconfig-error` for `packages/create/templates/emdash/tsconfig.json`
271
+ (`extends: "astro/tsconfigs/strict"`, unresolvable from inside this monorepo since the template
272
+ has no `node_modules` of its own — a pre-existing, structural gap, newly surfaced as a hard error
273
+ by oxlint 1.85's deeper config discovery, and blocking on every commit since this repo's pre-commit
274
+ hook runs `vp check --fix`). Fixed with a new `scripts/link-template-deps.mjs`, wired as the root
275
+ `postinstall` script: symlinks `astro`/`emdash`/`@emdash-cms/cloudflare` into the template's own
276
+ `node_modules/` (resolved fresh from sibling packages that already depend on them for real, not a
277
+ hardcoded `.pnpm` hash) and writes a minimal `astro:content` ambient shim at `.astro/types.d.ts`
278
+ (the same path `astro sync` would generate for real). Both are gitignored and only ever exist
279
+ inside this monorepo's copy of the template — a real scaffold installs its own real copies instead.
280
+ Considered and rejected first: inlining astro's strict preset (drift risk against astro's own
281
+ evolving default) and making the template a real pnpm workspace member (would change how its
282
+ `@getvitops/*: "latest"` deps resolve locally, which `templates.test.ts` exists to protect).
283
+
284
+ **Known gap, not fixed here**: `packages/astro/src/components/container.test.ts` (the
285
+ `AstroContainer`-based render tests) fails to even load under vite-plus 1.0's Vite 8 — direct
286
+ `.astro` imports in a Vitest file need the Astro Vite plugin registered, which nothing in this
287
+ repo's test config actually does (Astro's own docs call for `getViteConfig()`, which doesn't
288
+ compose with vite-plus's own `defineConfig`). A type-level shim
289
+ (`packages/astro/src/components/astro-modules.d.ts`) fixes `vp check`'s type-aware pass, but the
290
+ runtime `vp test` failure for this one file is still open.
291
+
292
+ - 1a3e811: **Fixed: `<Icon />` no longer crashes a Cloudflare Workers SSR page.** It statically imported
293
+ `loadIconSvg`/`spriteId` from `@getvitops/generator`'s root entry, which also re-exports
294
+ `generate.ts` — reaching lightningcss, whose native binary Workers cannot `require()`.
295
+ Rendering `<Icon>` anywhere in an SSR project 500'd every request, regardless of
296
+ `icons.engine`, since the import itself (not just calling the function) pulled lightningcss
297
+ into the bundle. Fixed by publishing a new Worker-safe `@getvitops/generator/icons-sprite`
298
+ subpath (`buildIconSprite`/`loadIconSvg`/`spriteId`, no lightningcss in its import graph) and
299
+ switching `<Icon />` to import from there.
300
+ - 0f8c018: **New: `resolveOrganizationName(config, locale?)`**, exported from `@getvitops/astro/routes`
301
+ alongside `toNotifyContext`/`toCrmConfig` — the same `organization.name` → `legalName` →
302
+ `undefined` fallback chain `toNotifyContext` already used for its `organizationName` field,
303
+ now usable standalone anywhere a build-time-only display name is needed (favicon generation,
304
+ a scaffold's `astro.config.mjs`, a `Layout.astro` fallback). `toNotifyContext` is unchanged —
305
+ it now delegates to this helper internally.
306
+
307
+ The emdash scaffold uses it to remove a three-way duplicated `'My EmDash Site'` string:
308
+ `astro.config.mjs`'s favicon `name` and `Layout.astro`'s title fallback both previously
309
+ hardcoded it independently of `site.json`'s `organization.name`. EmDash's own
310
+ `getSiteSettings().title` (set in the admin) still wins at render time when it's set — it's a
311
+ runtime-only fact that can't be read at build time at all — this only replaces the two
312
+ hardcoded _fallback_ strings.
313
+
314
+ - Updated dependencies [65a13cc]
315
+ - Updated dependencies [67e2088]
316
+ - Updated dependencies [0574388]
317
+ - Updated dependencies [c3c1bcb]
318
+ - Updated dependencies [0574388]
319
+ - Updated dependencies [0574388]
320
+ - Updated dependencies [0574388]
321
+ - Updated dependencies [1a3e811]
322
+ - Updated dependencies [0574388]
323
+ - Updated dependencies [dde3df3]
324
+ - Updated dependencies [d9f213b]
325
+ - @getvitops/generator@8.2.0-alpha.0
326
+ - @getvitops/utils@8.2.0-alpha.0
327
+ - @getvitops/core@8.2.0-alpha.0
328
+ - @getvitops/vite@8.2.0-alpha.0
329
+
3
330
  ## 8.1.0
4
331
 
5
332
  ### Patch Changes
@@ -52,7 +379,6 @@
52
379
 
53
380
  **Breaking (`css`/`bricks` and `tailwind` formats — `.lightbox-dialog`'s markup
54
381
  contract changed):**
55
-
56
382
  - `<Gallery />` now emits ONE `<dialog class="lightbox-dialog">` per gallery,
57
383
  wrapping a `<Carousel />`, instead of one dialog per image. A `.gallery__trigger`
58
384
  is now a plain `<a href="#slide-id">`, not a `command="show-modal"` button.
@@ -93,7 +419,6 @@
93
419
  ### Minor Changes
94
420
 
95
421
  - Add Google Business Profile reviews: fetch, cache, render.
96
-
97
422
  - **`vitops reviews`** (new CLI command) fetches a client's GBP reviews (legacy v4 API — the only
98
423
  one that has them) and writes them to `.vitops/reviews.json`, meant to be committed so a build
99
424
  never needs a live Google credential. `--dry` prints the plan; `--check` diffs against the cache
@@ -128,7 +453,6 @@
128
453
  `--site` for now.)
129
454
 
130
455
  **Fixed:**
131
-
132
456
  - `@getvitops/core`'s 12 runtime deps (`lit`, both `@oddbird` polyfills, both
133
457
  `@webcomponents` packages, `colorjs.io`, etc.) were listed as `dependencies` despite
134
458
  being bundled into `dist/elements.js`/`dist/polyfills.js`/`dist/deferred.js` — a
@@ -878,20 +1202,28 @@ hidden` would pull it out of the accessible-name computation along with the visu
878
1202
  {
879
1203
  "designSystem": {
880
1204
  "themes": {
881
- "default": {/* … */},
1205
+ "default": {
1206
+ /* … */
1207
+ },
882
1208
  },
883
1209
  },
884
1210
  "organization": {
885
1211
  "name": "Acme",
886
1212
  "contact": "hq",
887
1213
  "locations": {
888
- "hq": {/* … */},
1214
+ "hq": {
1215
+ /* … */
1216
+ },
889
1217
  },
890
1218
  },
891
1219
  "site": {
892
1220
  "defaultLocale": "en",
893
- "domains": {/* … */},
894
- "analytics": {/* … */},
1221
+ "domains": {
1222
+ /* … */
1223
+ },
1224
+ "analytics": {
1225
+ /* … */
1226
+ },
895
1227
  },
896
1228
  }
897
1229
  ```
@@ -1060,7 +1392,7 @@ open-fade-in"` composes slide and fade. Applied at zero specificity with identit
1060
1392
  Also new, and worth wiring at the same time — **`gitLastmod()` for real sitemap dates**:
1061
1393
 
1062
1394
  ```js
1063
- import vitops, { gitLastmod } from "@getvitops/astro";
1395
+ import vitops, { gitLastmod } from '@getvitops/astro';
1064
1396
  vitops({ sitemap: { serialize: await gitLastmod() } });
1065
1397
  ```
1066
1398
 
@@ -1160,8 +1492,8 @@ ffmpeg`, `apt install ffmpeg`, `winget install Gyan.FFmpeg`). The command fails
1160
1492
 
1161
1493
  ```js
1162
1494
  vitops({
1163
- analytics: { googleAnalytics: "G-XXXXXXXXXX", plausible: "acme.com" },
1164
- consent: { policyUrl: "/legal/cookies" },
1495
+ analytics: { googleAnalytics: 'G-XXXXXXXXXX', plausible: 'acme.com' },
1496
+ consent: { policyUrl: '/legal/cookies' },
1165
1497
  });
1166
1498
  ```
1167
1499
 
@@ -1301,7 +1633,7 @@ inset(0 100% 0 0)`, and `clip-path` clips **hit-testing** as well as painting
1301
1633
 
1302
1634
  ```js
1303
1635
  // astro.config.mjs
1304
- getvitops({ css: { input: "company.json", format: "css" } });
1636
+ getvitops({ css: { input: 'company.json', format: 'css' } });
1305
1637
  ```
1306
1638
 
1307
1639
  ```
@@ -1400,11 +1732,11 @@ inset(0 100% 0 0)`, and `clip-path` clips **hit-testing** as well as painting
1400
1732
  getvitops({
1401
1733
  fonts: [
1402
1734
  {
1403
- name: "League Spartan",
1404
- provider: "fontsource",
1405
- cssVariable: "--font-league-spartan",
1406
- weights: ["100 900"],
1407
- subsets: ["latin"],
1735
+ name: 'League Spartan',
1736
+ provider: 'fontsource',
1737
+ cssVariable: '--font-league-spartan',
1738
+ weights: ['100 900'],
1739
+ subsets: ['latin'],
1408
1740
  preload: true,
1409
1741
  },
1410
1742
  ],
@@ -1632,14 +1964,14 @@ inset(0 100% 0 0)`, and `clip-path` clips **hit-testing** as well as painting
1632
1964
  // astro.config.mjs
1633
1965
  vitops({
1634
1966
  seo: {
1635
- siteName: "Acme",
1636
- titleTemplate: "%s · Acme",
1637
- defaultDescription: "We make the thing.",
1967
+ siteName: 'Acme',
1968
+ titleTemplate: '%s · Acme',
1969
+ defaultDescription: 'We make the thing.',
1638
1970
  openGraph: {
1639
- locale: "en_CA",
1640
- image: { url: "/og.png", alt: "Acme", width: 1200, height: 630 },
1971
+ locale: 'en_CA',
1972
+ image: { url: '/og.png', alt: 'Acme', width: 1200, height: 630 },
1641
1973
  },
1642
- twitter: { site: "@acme" },
1974
+ twitter: { site: '@acme' },
1643
1975
  },
1644
1976
  });
1645
1977
  ```
@@ -1683,7 +2015,7 @@ inset(0 100% 0 0)`, and `clip-path` clips **hit-testing** as well as painting
1683
2015
 
1684
2016
  ```js
1685
2017
  getvitops({ sitemap: true });
1686
- getvitops({ sitemap: { filter: (page) => !page.includes("/draft/") } });
2018
+ getvitops({ sitemap: { filter: (page) => !page.includes('/draft/') } });
1687
2019
  ```
1688
2020
 
1689
2021
  `@astrojs/sitemap` is an **optional peer** — install it yourself (`pnpm add -D @astrojs/sitemap`) and
@@ -1918,15 +2250,15 @@ inset(0 100% 0 0)`, and `clip-path` clips **hit-testing** as well as painting
1918
2250
  which `@getvitops/astro` re-exports wholesale. So from `astro.config.mjs`:
1919
2251
 
1920
2252
  ```js
1921
- import { generateIconInclude } from "@getvitops/astro";
1922
- import icon from "astro-icon";
2253
+ import { generateIconInclude } from '@getvitops/astro';
2254
+ import icon from 'astro-icon';
1923
2255
 
1924
2256
  integrations: [
1925
2257
  icon({
1926
2258
  include: generateIconInclude({
1927
- ui: "fa7-solid",
1928
- brand: "simple-icons",
1929
- semantic: ["menu", "close", "search", "github"],
2259
+ ui: 'fa7-solid',
2260
+ brand: 'simple-icons',
2261
+ semantic: ['menu', 'close', 'search', 'github'],
1930
2262
  }),
1931
2263
  }),
1932
2264
  ];
@@ -1960,7 +2292,7 @@ inset(0 100% 0 0)`, and `clip-path` clips **hit-testing** as well as painting
1960
2292
  ```js
1961
2293
  // astro.config.mjs
1962
2294
  getvitops({
1963
- css: { input: "design-system.json", format: "css", out: "src/styles" },
2295
+ css: { input: 'design-system.json', format: 'css', out: 'src/styles' },
1964
2296
  editor: true,
1965
2297
  });
1966
2298
  ```
@@ -1,3 +1,3 @@
1
- import { a as TRACKING_ENDPOINT } from "../tracking-DdEKExlE.mjs";
1
+ import { a as TRACKING_ENDPOINT } from "../tracking-BNKKBqOe.mjs";
2
2
  export * from "@getvitops/utils/tracking";
3
3
  export { TRACKING_ENDPOINT };
@@ -1,4 +1,4 @@
1
- import { n as TRACKING_ENDPOINT } from "../tracking-CmSfxWC3.mjs";
1
+ import { n as TRACKING_ENDPOINT } from "../tracking-M2PqlMpw.mjs";
2
2
 
3
3
  export * from "@getvitops/utils/tracking"
4
4
 
package/dist/index.d.mts CHANGED
@@ -1,11 +1,10 @@
1
- import { i as TRACKING_COOKIE, n as ResolveTrackingContext, o as TrackingCategory, r as ResolvedTracking, s as resolveTracking, t as GetvitopsTrackingOptions } from "./tracking-DdEKExlE.mjs";
1
+ import { i as TRACKING_COOKIE, n as ResolveTrackingContext, o as TrackingCategory, r as ResolvedTracking, s as resolveTracking, t as GetvitopsTrackingOptions } from "./tracking-BNKKBqOe.mjs";
2
2
  import { LegalOutput, SiteFont, StylesheetFormat } from "@getvitops/generator";
3
3
  import { generateIconInclude } from "@getvitops/utils";
4
- import { AdProvider } from "@getvitops/utils/ads";
4
+ import { AdProvider, ConversionLabelMap } from "@getvitops/utils/ads";
5
5
  import { OutputKind } from "@getvitops/utils/media";
6
6
  import { AstroIntegration } from "astro";
7
7
  export * from "@getvitops/utils";
8
-
9
8
  //#region src/analytics.d.ts
10
9
  /**
11
10
  * The `<Analytics />` resolution layer — provider config in, the exact tags to
@@ -152,6 +151,19 @@ interface ResolvedAnalytics {
152
151
  interface ResolveAnalyticsContext {
153
152
  /** Is the consent gate enabled on this site (`vitops({ consent })`)? */
154
153
  consent?: boolean;
154
+ /**
155
+ * Enabled `site.experiments` entries, keyed the same way, each carrying the
156
+ * resolved cookie name `vitopsAbTesting()`'s middleware assigns
157
+ * (`_ab_<key>` unless overridden). When set, each provider's bootstrap
158
+ * reads these cookies client-side and reports the assignment via that
159
+ * provider's own custom-dimension/property mechanism — a no-op when this
160
+ * is empty, so a site not using experiments sees no behavior change.
161
+ */
162
+ experiments?: Record<string, {
163
+ cookieName: string;
164
+ }>;
165
+ /** `site.analytics.matomo.abVariantDimensions` — Matomo's pre-created custom-dimension index per experiment key. */
166
+ matomoAbVariantDimensions?: Record<string, number>;
155
167
  }
156
168
  /**
157
169
  * Resolve the configured providers into the tags to emit.
@@ -160,9 +172,9 @@ interface ResolveAnalyticsContext {
160
172
  * malformed ID should not take down a build, and a missing tag is visible in the
161
173
  * output while a failed build is just a red wall.
162
174
  */
163
- declare function resolveAnalytics(opts: GetvitopsAnalyticsOptions | undefined, ctx?: ResolveAnalyticsContext): ResolvedAnalytics;
175
+ export declare function resolveAnalytics(opts: GetvitopsAnalyticsOptions | undefined, ctx?: ResolveAnalyticsContext): ResolvedAnalytics;
164
176
  /** Which optional categories a set of tags actually needs a visitor to decide. */
165
- declare function consentCategories(tags: GatedTag[]): OptionalConsentCategory[];
177
+ export declare function consentCategories(tags: GatedTag[]): OptionalConsentCategory[];
166
178
  //#endregion
167
179
  //#region src/seo.d.ts
168
180
  /**
@@ -216,9 +228,13 @@ interface GetvitopsSeoOptions {
216
228
  /** Bing Webmaster Tools verification token. */
217
229
  bingSiteVerification?: string;
218
230
  openGraph?: {
219
- /** Default `og:type` (default `'website'`). */type?: SeoOgType; /** `og:locale`. Written either way — `en-CA` and `en_CA` both emit `en_CA`. */
220
- locale?: string; /** Additional locales the same content exists in → `og:locale:alternate`. */
221
- localeAlternates?: string[]; /** Fallback share image. Relative URLs resolve against `site`. */
231
+ /** Default `og:type` (default `'website'`). */
232
+ type?: SeoOgType;
233
+ /** `og:locale`. Written either way — `en-CA` and `en_CA` both emit `en_CA`. */
234
+ locale?: string;
235
+ /** Additional locales the same content exists in → `og:locale:alternate`. */
236
+ localeAlternates?: string[];
237
+ /** Fallback share image. Relative URLs resolve against `site`. */
222
238
  image?: {
223
239
  url: string;
224
240
  alt?: string;
@@ -228,8 +244,11 @@ interface GetvitopsSeoOptions {
228
244
  };
229
245
  };
230
246
  twitter?: {
231
- /** Card type when no image resolves (default `'summary'`). */card?: SeoTwitterCard; /** The site's own handle, `@acme`. */
232
- site?: string; /** Default author handle, `@ada`. */
247
+ /** Card type when no image resolves (default `'summary'`). */
248
+ card?: SeoTwitterCard;
249
+ /** The site's own handle, `@acme`. */
250
+ site?: string;
251
+ /** Default author handle, `@ada`. */
233
252
  creator?: string;
234
253
  };
235
254
  }
@@ -341,7 +360,7 @@ interface SeoContext {
341
360
  * through its three arguments, which is what makes it testable and what keeps the
342
361
  * package free of the consumer-global coupling that sank the previous site model.
343
362
  */
344
- declare function resolveSeo(defaults: GetvitopsSeoOptions, props: SeoProps, ctx: SeoContext): ResolvedSeo;
363
+ export declare function resolveSeo(defaults: GetvitopsSeoOptions, props: SeoProps, ctx: SeoContext): ResolvedSeo;
345
364
  //#endregion
346
365
  //#region src/integration.d.ts
347
366
  interface GetvitopsFaviconOptions {
@@ -750,7 +769,7 @@ interface GetvitopsIconsOptions {
750
769
  /** Aliases applied before the semantic map — legacy names, or values stored in CMS content. */
751
770
  overrides?: Record<string, string>;
752
771
  }
753
- declare function vitops(opts?: GetvitopsOptions): AstroIntegration;
772
+ export default function vitops(opts?: GetvitopsOptions): AstroIntegration;
754
773
  //#endregion
755
774
  //#region src/ads.d.ts
756
775
  /**
@@ -763,7 +782,15 @@ declare function vitops(opts?: GetvitopsOptions): AstroIntegration;
763
782
  interface GetvitopsAdProperty {
764
783
  accountId?: string;
765
784
  pixelId?: string;
766
- conversionLabel?: string;
785
+ /**
786
+ * Google Ads only: the conversion action label(s) paired with `pixelId` (the
787
+ * `AW-…` conversion id) — a bare string (back-compat, treated as `call`) or a
788
+ * map of action → label. `<Tracking />`'s tel:/mailto: click handler and
789
+ * `vitopsFormConversion()` each fire `gtag('event', 'conversion', …)` for
790
+ * their own action at the moment of the interaction — see `gtagBootstrap` in
791
+ * `@getvitops/utils/ads`.
792
+ */
793
+ conversionLabel?: string | ConversionLabelMap;
767
794
  /** Default `marketing`. `analytics` for a property used purely for measurement. */
768
795
  category?: 'marketing' | 'analytics';
769
796
  /** False keeps the property on record without emitting its tag. */
@@ -792,7 +819,7 @@ interface ResolveAdsContext {
792
819
  strategy?: AnalyticsStrategy;
793
820
  }
794
821
  /** Resolve the configured ad properties into the tags `<Ads />` emits. */
795
- declare function resolveAds(ads: GetvitopsAdsOptions | undefined, ctx?: ResolveAdsContext): ResolvedAds;
822
+ export declare function resolveAds(ads: GetvitopsAdsOptions | undefined, ctx?: ResolveAdsContext): ResolvedAds;
796
823
  //#endregion
797
824
  //#region src/lastmod.d.ts
798
825
  interface GitLastmodOptions {
@@ -810,9 +837,9 @@ interface GitLastmodOptions {
810
837
  * commit date describes the template rather than any of them. Stamping every
811
838
  * generated page with the template's date is exactly the "wrong date" case above.
812
839
  */
813
- declare function routeFromPage(file: string): string | undefined;
840
+ export declare function routeFromPage(file: string): string | undefined;
814
841
  /** The slug a content entry is most likely published under. */
815
- declare function slugFromContent(file: string): string | undefined;
842
+ export declare function slugFromContent(file: string): string | undefined;
816
843
  /**
817
844
  * Build a `serialize` function that stamps `lastmod` from git history.
818
845
  *
@@ -821,7 +848,7 @@ declare function slugFromContent(file: string): string | undefined;
821
848
  * is dropped rather than guessed at, since picking either one would be a coin
822
849
  * flip presented as a fact.
823
850
  */
824
- declare function gitLastmod(options?: GitLastmodOptions): Promise<(item: GetvitopsSitemapEntry) => GetvitopsSitemapEntry>;
851
+ export declare function gitLastmod(options?: GitLastmodOptions): Promise<(item: GetvitopsSitemapEntry) => GetvitopsSitemapEntry>;
825
852
  //#endregion
826
- export { type AnalyticsStrategy, type ClarityOptions, type ConsentCategory, type GatedTag, type GetvitopsAdProperty, type GetvitopsAdsOptions, type GetvitopsAnalyticsOptions, type GetvitopsConsentOptions, type GetvitopsCssOptions, type GetvitopsFaviconOptions, type GetvitopsMediaOptions, type GetvitopsOptions, type GetvitopsSeoOptions, type GetvitopsSitemapChangeFreq, type GetvitopsSitemapEntry, type GetvitopsSitemapOptions, type GetvitopsTrackingOptions, type GitLastmodOptions, type GoogleAnalyticsOptions, type MatomoOptions, type OptionalConsentCategory, type PlausibleOptions, type ResolveAdsContext, type ResolveAnalyticsContext, type ResolveTrackingContext, type ResolvedAdTag, type ResolvedAds, type ResolvedAnalytics, type ResolvedSeo, type ResolvedTag, type ResolvedTracking, type SeoContext, type SeoNameTag, type SeoOgType, type SeoPropertyTag, type SeoProps, type SeoTwitterCard, TRACKING_COOKIE, type TrackingCategory, consentCategories, vitops as default, gitLastmod, resolveAds, resolveAnalytics, resolveSeo, resolveTracking, routeFromPage, slugFromContent };
853
+ export { type AnalyticsStrategy, type ClarityOptions, type ConsentCategory, type GatedTag, type GetvitopsAdProperty, type GetvitopsAdsOptions, type GetvitopsAnalyticsOptions, type GetvitopsConsentOptions, type GetvitopsCssOptions, type GetvitopsFaviconOptions, type GetvitopsMediaOptions, type GetvitopsOptions, type GetvitopsSeoOptions, type GetvitopsSitemapChangeFreq, type GetvitopsSitemapEntry, type GetvitopsSitemapOptions, type GetvitopsTrackingOptions, type GitLastmodOptions, type GoogleAnalyticsOptions, type MatomoOptions, type OptionalConsentCategory, type PlausibleOptions, type ResolveAdsContext, type ResolveAnalyticsContext, type ResolveTrackingContext, type ResolvedAdTag, type ResolvedAds, type ResolvedAnalytics, type ResolvedSeo, type ResolvedTag, type ResolvedTracking, type SeoContext, type SeoNameTag, type SeoOgType, type SeoPropertyTag, type SeoProps, type SeoTwitterCard, TRACKING_COOKIE, type TrackingCategory, resolveTracking };
827
854
  //# sourceMappingURL=index.d.mts.map