@getvitops/astro 8.1.0 → 8.2.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 +824 -33
- package/dist/config-adapters-CbTywgEY.mjs +71 -0
- package/dist/config-adapters-CbTywgEY.mjs.map +1 -0
- package/dist/exports/tracking.d.mts +1 -1
- package/dist/exports/tracking.mjs +1 -1
- package/dist/index.d.mts +65 -48
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +240 -32
- package/dist/index.mjs.map +1 -1
- package/dist/middleware.d.mts +23 -0
- package/dist/middleware.d.mts.map +1 -0
- package/dist/middleware.mjs +34 -0
- package/dist/middleware.mjs.map +1 -0
- package/dist/routes/conversion.d.mts +131 -16
- package/dist/routes/conversion.d.mts.map +1 -1
- package/dist/routes/conversion.mjs +133 -28
- package/dist/routes/conversion.mjs.map +1 -1
- package/dist/{tracking-CmSfxWC3.mjs → tracking-Cb3A0BrC.mjs} +3 -4
- package/dist/tracking-Cb3A0BrC.mjs.map +1 -0
- package/dist/{tracking-DdEKExlE.d.mts → tracking-D2xOzjFD.d.mts} +6 -26
- package/dist/tracking-D2xOzjFD.d.mts.map +1 -0
- package/package.json +12 -8
- package/src/ads.ts +52 -16
- package/src/analytics.ts +98 -11
- package/src/components/Analytics.astro +5 -1
- package/src/components/CookieConsent.astro +2 -1
- package/src/components/Icon.astro +8 -1
- package/src/components/ReviewCardBody.astro +5 -2
- package/src/components/Tracking.astro +2 -1
- package/src/schemas/Article.astro +2 -1
- package/src/schemas/BookActions.astro +2 -1
- package/src/schemas/Breadcrumb.astro +2 -1
- package/src/schemas/Carousel.astro +2 -1
- package/src/schemas/CourseList.astro +2 -1
- package/src/schemas/Dataset.astro +2 -1
- package/src/schemas/DiscussionForum.astro +2 -1
- package/src/schemas/EducationQA.astro +2 -1
- package/src/schemas/EmployerAggregateRating.astro +2 -1
- package/src/schemas/Event.astro +2 -1
- package/src/schemas/FAQ.astro +2 -1
- package/src/schemas/FactCheck.astro +2 -1
- package/src/schemas/ImageMetadata.astro +2 -1
- package/src/schemas/JobPosting.astro +2 -1
- package/src/schemas/LocalBusiness.astro +2 -1
- package/src/schemas/MathSolver.astro +2 -1
- package/src/schemas/MovieCarousel.astro +2 -1
- package/src/schemas/Organization.astro +2 -1
- package/src/schemas/Product.astro +2 -1
- package/src/schemas/ProfilePage.astro +2 -1
- package/src/schemas/QAPage.astro +2 -1
- package/src/schemas/Recipe.astro +2 -1
- package/src/schemas/Review.astro +2 -1
- package/src/schemas/SoftwareApp.astro +2 -1
- package/src/scripts/tracking.ts +114 -21
- package/src/seo.ts +6 -5
- package/src/tracking.ts +6 -10
- package/virtual.d.ts +17 -1
- package/dist/tracking-CmSfxWC3.mjs.map +0 -1
- package/dist/tracking-DdEKExlE.d.mts.map +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,799 @@
|
|
|
1
1
|
# @getvitops/astro
|
|
2
2
|
|
|
3
|
+
## 8.2.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
|
+
- 89265af: **New package: `@getvitops/config`** — the site-config provider blocks (`site.crm`, `site.ads`,
|
|
57
|
+
`site.reviews`, `site.seo.indexing`, `site.searchConsole`, `site.notifications`, `site.tracking`,
|
|
58
|
+
`site.security`, `site.deployment`) as zod schemas and inferred types, with the closed provider lists
|
|
59
|
+
(`AD_PROVIDERS`, `CRM_PROVIDERS`, `BIGIN_DATA_CENTERS`) on a zod-free `./providers` entry. It joins the
|
|
60
|
+
`fixed` group.
|
|
61
|
+
|
|
62
|
+
Why: `@getvitops/utils` holds the provider code but could not import its configuration types — the schema
|
|
63
|
+
lived in `@getvitops/generator`, a layer above — so it kept a hand-copied mirror beside each module,
|
|
64
|
+
an adapter converting into it, and a test holding the copy to the schema. The mirrors are gone.
|
|
65
|
+
|
|
66
|
+
- **`@getvitops/generator`**: `AD_PROVIDERS`, `CRM_PROVIDERS`, `BIGIN_DATA_CENTERS`, `AdProvider` and
|
|
67
|
+
`CrmProviderConfig` are re-exported unchanged. No import path changes.
|
|
68
|
+
- **`@getvitops/utils`**: the config types in `./crm`, `./ads`, `./indexing`, `./onboarding`, `./reviews`,
|
|
69
|
+
`./tracking` and `./notify` (`CrmConfig`, `HubSpotConfig`, `BiginConfig`, `JobberConfig`,
|
|
70
|
+
`IndexNowConfig`, `SearchConsoleConfig`, `ReviewsConfig`, `ReviewLocationConfig`, `TrackingConfig`,
|
|
71
|
+
`NotificationsConfig`, `EmailChannelConfig`, `DomainSetup`, `AdPropertySetup`) are now inferred from the
|
|
72
|
+
schema. Same names, same fields, but **optional fields now follow zod's inference**, so a hand-written
|
|
73
|
+
object literal typed against them may need `| undefined` handled. `AdPropertySetup` gains the fields the
|
|
74
|
+
schema always had (`conversionActionId`, `reporting`).
|
|
75
|
+
- **`@getvitops/astro`**: `GetvitopsAdProperty` and `GetvitopsTrackingOptions` are aliases of the schema
|
|
76
|
+
types. `toCrmConfig()` and `toGoogleAdsConfig()` are **deprecated** — they returned their argument's
|
|
77
|
+
field unchanged; pass `config.site.crm` / `config.site.ads?.google` directly.
|
|
78
|
+
- **`@getvitops/cli`**: `vitops ads` now passes `conversionActionId` and `reporting` through to the planner;
|
|
79
|
+
it previously copied a hand-picked subset of the block and dropped them.
|
|
80
|
+
|
|
81
|
+
Formats reached: none — no CSS change.
|
|
82
|
+
|
|
83
|
+
- 0574388: Add `site.crm` — push form submissions into HubSpot, Zoho Bigin, or Jobber.
|
|
84
|
+
|
|
85
|
+
Client sites need form submissions to land in the client's CRM. The options available before this
|
|
86
|
+
— a HubSpot/Bigin/Jobber iframe or JS embed — fail on three counts: the markup can't be
|
|
87
|
+
white-labelled against this design system, nothing can operate on the submission before it leaves
|
|
88
|
+
the page, and the submission never touched `createConversionRoute()`, so `_ac` ad-click attribution
|
|
89
|
+
was lost and no conversion notification was planned.
|
|
90
|
+
|
|
91
|
+
This adds one server-side path for all three CRMs, hung off the conversion route that already
|
|
92
|
+
exists. `createConversionRoute()` gets a new `crm` option (`{ config, credentials, fetch?,
|
|
93
|
+
timeoutMs? }`); credentials arrive as a function of `locals`, exactly like the existing `binding`
|
|
94
|
+
option, and are never config. A failed CRM write never fails the request and is folded into the
|
|
95
|
+
notification email's new "Delivery" section, so a broken CRM token is never silent.
|
|
96
|
+
|
|
97
|
+
New `@getvitops/utils/crm` subpath: `deliverToCrm()`, `mapSubmission()`, `deriveLastName()` (Bigin
|
|
98
|
+
requires a last name; this derives one from an explicit field, a full name, or an email address,
|
|
99
|
+
refusing rather than fabricating one), `submitToHubSpot()`, `submitToBigin()`, `submitToJobber()`.
|
|
100
|
+
`identifySubmitter()` was promoted out of `notify`'s private name-guessing cascade into
|
|
101
|
+
`@getvitops/utils/tracking`, since the CRM mapper needed the whole identity (first/last name,
|
|
102
|
+
email, phone, company), not just a display name.
|
|
103
|
+
|
|
104
|
+
Jobber is GraphQL, not REST, and answers `200 OK` for failures — a rejected `clientCreate` returns
|
|
105
|
+
`userErrors[]`, a malformed request returns top-level `errors[]`. `submitToJobber()` classifies on
|
|
106
|
+
the response body, not just the status. Its refresh tokens also rotate by default: **Refresh Token
|
|
107
|
+
Rotation must be turned off** in the app's Developer Center settings, since a Worker reading a
|
|
108
|
+
static `VITOPS_JOBBER_REFRESH_TOKEN` cannot persist a rotated one — a rotated token is detected at
|
|
109
|
+
exchange time and reported with the fix, rather than surfacing an hour later as an opaque 401.
|
|
110
|
+
Attribution and unmapped extra fields render into a best-effort note by default (works on every
|
|
111
|
+
Jobber plan); a resubmitting visitor creates a second Jobber client unless `crm.jobber.dedupe: true`
|
|
112
|
+
is set once its lookup query has been verified against a live account (defaults `false` pending
|
|
113
|
+
that verification — see `jobber.ts`'s docblock).
|
|
114
|
+
|
|
115
|
+
New `site.crm` config block (`provider`, `hubspot`/`bigin`/`jobber`, `fieldMap`, `trackingFields`)
|
|
116
|
+
and two CLI commands: `vitops crm check` (fetches the live HubSpot property list or Bigin field
|
|
117
|
+
list and reports every mapped name that doesn't exist there — HubSpot rejects an unknown field
|
|
118
|
+
outright, Bigin silently drops it; Jobber has nothing live to check yet, since its mapped values
|
|
119
|
+
currently render as note text) and `vitops crm setup` (creates missing HubSpot properties; Bigin
|
|
120
|
+
and Jobber have no such API, so both always print a manual reminder).
|
|
121
|
+
|
|
122
|
+
The generated privacy policy now names HubSpot, Zoho Bigin, or Jobber when `site.crm` configures
|
|
123
|
+
one. Bigin gets the same cross-border-transfer derivation Matomo Cloud already gets: a
|
|
124
|
+
`dataCenter: 'ca'` asserts no transfer outside Canada, any other data centre does. Jobber's
|
|
125
|
+
operator (Octopusapp Inc. d/b/a Jobber) is Canadian, but its own privacy policy names the United
|
|
126
|
+
States as a processing location too, so the policy always asserts a cross-border transfer for
|
|
127
|
+
Jobber regardless of config — a Canadian operator is not read as Canadian-only storage.
|
|
128
|
+
|
|
129
|
+
Credentials: `VITOPS_HUBSPOT_ACCESS_TOKEN` for HubSpot; `VITOPS_ZOHO_CLIENT_ID` /
|
|
130
|
+
`VITOPS_ZOHO_CLIENT_SECRET` / `VITOPS_ZOHO_REFRESH_TOKEN` for Bigin; `VITOPS_JOBBER_CLIENT_ID` /
|
|
131
|
+
`VITOPS_JOBBER_CLIENT_SECRET` / `VITOPS_JOBBER_REFRESH_TOKEN` for Jobber. Never config.
|
|
132
|
+
|
|
133
|
+
No consumer-facing change in `@getvitops/astro` beyond the new `crm` route option and a build
|
|
134
|
+
warning when `site.crm` is configured with no route answering `TRACKING_ENDPOINT` — CSS/Bricks/
|
|
135
|
+
tailwind outputs are unaffected.
|
|
136
|
+
|
|
137
|
+
- 8d8aad7: **New: server-side Google Ads offline conversion upload.** Set
|
|
138
|
+
`site.ads.google.reporting: "server"` and a matching
|
|
139
|
+
`conversionActionId` (a numeric id or full
|
|
140
|
+
`customers/…/conversionActions/…` resource name — **not** the same value
|
|
141
|
+
as `conversionLabel`, which is the opaque label the web tag uses and
|
|
142
|
+
which the Ads API's upload endpoint doesn't accept) and
|
|
143
|
+
`createConversionRoute`'s new `googleAdsOffline` option uploads the
|
|
144
|
+
conversion via `ConversionUploadService.UploadClickConversions` instead
|
|
145
|
+
of relying on the visitor's browser to fire `gtag.js`. An action with no
|
|
146
|
+
matching `conversionActionId` keeps firing client-side regardless of
|
|
147
|
+
`reporting`, so turning it on before the id is filled in never silently
|
|
148
|
+
drops a conversion. `@getvitops/astro/routes` exports a matching
|
|
149
|
+
`toGoogleAdsConfig(config)` adapter, alongside `toCrmConfig`.
|
|
150
|
+
|
|
151
|
+
Structurally typed against Google's documented REST shape, not verified
|
|
152
|
+
against a live Ads account — no developer-token/MCC access exists
|
|
153
|
+
anywhere in this toolchain's own setup to test against. Wrapped so a
|
|
154
|
+
wrong guess degrades to a stated failure reason (folded into the
|
|
155
|
+
conversion email's "Delivery" section, same as a failed CRM write) rather
|
|
156
|
+
than breaking, the same caveat `@getvitops/utils/crm`'s Jobber executor
|
|
157
|
+
already carries for its own unverified surfaces.
|
|
158
|
+
|
|
159
|
+
`vitops ads lint` now flags a `reporting: "server"` property whose
|
|
160
|
+
configured actions have no matching `conversionActionId` — not a
|
|
161
|
+
failure, since that action keeps firing client-side, but worth surfacing
|
|
162
|
+
rather than leaving someone to wonder why nothing changed.
|
|
163
|
+
|
|
164
|
+
Not built: `vitops ads provision` (upserting conversion actions via
|
|
165
|
+
`ConversionActionService` and printing back both the label and the
|
|
166
|
+
resource id) — same credential blocker, and deliberately left unscoped
|
|
167
|
+
until there's a live account to verify the request/response shape
|
|
168
|
+
against.
|
|
169
|
+
|
|
170
|
+
- 0574388: Fire a Google Ads conversion on a tel:/mailto: click, and fix a rate-limit bug on the conversion
|
|
171
|
+
route that could silently drop one of the two.
|
|
172
|
+
|
|
173
|
+
`site.ads.google.conversionLabel` used to be accepted by the config schema and typed all the way
|
|
174
|
+
through to the rendered tag, but nothing actually read it — no `gtag('event', 'conversion', …)`
|
|
175
|
+
was ever emitted. When set, the click tracker `<Tracking />` inlines now fires the conversion at
|
|
176
|
+
the moment a visitor taps a `tel:` or `mailto:` link, using the `AW-…/label` target Google Ads
|
|
177
|
+
expects. A `conversionLabel` that can't safely land inside the inline bootstrap script (the same
|
|
178
|
+
check `pixelId`/`accountId` already get) is dropped with a warning rather than firing something
|
|
179
|
+
malformed — the ad tag itself still loads.
|
|
180
|
+
|
|
181
|
+
Also: `createConversionRoute()`'s rate limiter previously keyed on IP address alone, so a call
|
|
182
|
+
beacon and an email-click beacon from the same visitor, seconds apart, competed for one window —
|
|
183
|
+
the second could 429 and vanish with no error anywhere. It's now keyed on IP _and_ event type, so
|
|
184
|
+
distinct conversion types never starve each other; repeats of the same type are still throttled
|
|
185
|
+
exactly as before.
|
|
186
|
+
|
|
187
|
+
No config changes — a site with `site.ads.google.conversionLabel` already set picks this up
|
|
188
|
+
automatically on the next build.
|
|
189
|
+
|
|
190
|
+
- 2a0a559: The Astro integration now warns when `site.locales`/`site.defaultLocale` disagree with
|
|
191
|
+
`astro.config.mjs`'s own `i18n` block — naming which locale codes are missing from each side, and
|
|
192
|
+
when the two `defaultLocale`s differ. EmDash reads its locale list straight from `i18n`, while our
|
|
193
|
+
own SEO/legal code reads `site.locales`, so the two had no way to notice they'd drifted apart. A
|
|
194
|
+
warning, not a build error — like the existing ads/analytics/consent consistency checks, either
|
|
195
|
+
side can legitimately be configured without the other, so only a genuine disagreement is flagged.
|
|
196
|
+
- 0574388: Track `mailto:` link clicks as conversions, alongside the existing `tel:` link tracking.
|
|
197
|
+
|
|
198
|
+
Before this, the capture script only beaconed `tel:` clicks; a `mailto:` link on the same page was
|
|
199
|
+
invisible to attribution — a visitor who emailed the business instead of calling generated no
|
|
200
|
+
conversion event at all. `<Tracking />`'s inlined script now listens for both, sending `{ event:
|
|
201
|
+
'email', email }` via `navigator.sendBeacon` the same way it already sends `{ event: 'call', phone
|
|
202
|
+
}`.
|
|
203
|
+
|
|
204
|
+
`ConversionEvent['type']` gains an `'email'` member alongside `'form'`/`'call'`, with a new
|
|
205
|
+
`email` field. `createConversionRoute()` handles it exactly like a call: no honeypot, Turnstile or
|
|
206
|
+
CRM checks (there are no form fields to validate or map), a 204 response, and a notification email
|
|
207
|
+
subject reading `[Email Click] Visitor emailing ...`.
|
|
208
|
+
|
|
209
|
+
No config changes and no new options — a site with `<Tracking />` already mounted picks this up
|
|
210
|
+
automatically the next time it rebuilds.
|
|
211
|
+
|
|
212
|
+
- dde3df3: **`site.ads.google.conversionLabel` now accepts a per-action map** —
|
|
213
|
+
`{ call?, email?, form? }` — instead of one bare string shared by every
|
|
214
|
+
interaction. A bare string still works (back-compat, treated as `call`), but a
|
|
215
|
+
call and an email click used to report as the **same** Google Ads conversion
|
|
216
|
+
action, indistinguishable in the account; each action now gets its own
|
|
217
|
+
`send_to` target.
|
|
218
|
+
|
|
219
|
+
**New: `vitopsFormConversion(formEl)`**, exposed as `window.vitopsFormConversion`
|
|
220
|
+
by `<Tracking />`'s capture script — call it from your own fetch-based form
|
|
221
|
+
submit handler once the POST to `createConversionRoute()` succeeds, to fire the
|
|
222
|
+
`form` Google Ads conversion action client-side (mirroring the existing
|
|
223
|
+
tel:/mailto: click handler, which now fires `call`/`email` instead of sharing
|
|
224
|
+
one label). Idempotent per form element. There's deliberately no server-side
|
|
225
|
+
equivalent for a no-JS `<form method="post">` submission — that's a full-page
|
|
226
|
+
redirect with no script to fire from, and a real server-side Google Ads
|
|
227
|
+
conversion needs the Ads API's click-conversion upload (developer token + MCC
|
|
228
|
+
access), tracked separately in the vNext backlog.
|
|
229
|
+
|
|
230
|
+
**Fixed: the conversion route's rate limiter no longer trusts the client's
|
|
231
|
+
raw `event` field.** It used to key on whatever string a client sent
|
|
232
|
+
(`` `${ip}:${body.event}` ``), so varying that value on every request let a
|
|
233
|
+
form submission dodge the per-IP rate limit entirely — only `'call'`/`'email'`
|
|
234
|
+
were ever recognized, and anything else got its own fresh bucket. The key now
|
|
235
|
+
uses the server's own classification (`call` | `email` | `form`), closing that
|
|
236
|
+
bypass without changing the existing call-vs-email-don't-compete behavior.
|
|
237
|
+
|
|
238
|
+
**New: `site.notifications.actionUrl`** — an optional action-link template
|
|
239
|
+
added to the conversion-notification email as an "Actions" section.
|
|
240
|
+
`{clickId}` is replaced with the ad click ID captured on `_ac` (gclid, fbclid,
|
|
241
|
+
etc.); the section is omitted when the event has no click ID to interpolate,
|
|
242
|
+
rather than emailing a broken link.
|
|
243
|
+
|
|
244
|
+
- e5bc937: **Security: JSON-LD can no longer break out of its `<script>` tag.** All 24 `schemas/*.astro`
|
|
245
|
+
components inlined `JSON.stringify(jsonLd)` as-is, so a review, FAQ answer or forum post containing
|
|
246
|
+
`</script><script>…` ran as script on the page. They now go through `jsonLdScript()`, which escapes
|
|
247
|
+
`<`, `>`, `&`, U+2028 and U+2029 (still valid JSON — consumers and search engines parse the same
|
|
248
|
+
document). Sites rendering any user-supplied text through a schema component should upgrade promptly.
|
|
249
|
+
|
|
250
|
+
**`createConversionRoute()` hardening** — behaviour changes:
|
|
251
|
+
|
|
252
|
+
- The body-size cap is now enforced on the stream itself, not only on `Content-Length`; a chunked
|
|
253
|
+
request with no header is refused with 413 once it passes `maxBodyBytes`.
|
|
254
|
+
- **Breaking for non-Cloudflare hosts:** `X-Forwarded-For` is no longer used as the rate-limit key by
|
|
255
|
+
default, since off Cloudflare a client can set it freely and mint a new bucket per request. Pass
|
|
256
|
+
`trustForwardedFor: true` when running behind a proxy that overwrites it. Without
|
|
257
|
+
`CF-Connecting-IP` or that opt-in there is no IP and rate limiting is skipped. No change on
|
|
258
|
+
Cloudflare Workers.
|
|
259
|
+
|
|
260
|
+
`@getvitops/emdash`: the link/button block (`vitops.actionLink`) drops hrefs with a scheme other
|
|
261
|
+
than `http`, `https`, `mailto` or `tel` (e.g. `javascript:`, `data:`) and renders nothing for them.
|
|
262
|
+
|
|
263
|
+
Formats reached: none — no CSS change. Other packages in the `fixed` group: No consumer-facing
|
|
264
|
+
change in this package.
|
|
265
|
+
|
|
266
|
+
- 7fd5f8f: **`site.seo` is now the single source of site-level SEO defaults** (ADR-0002). Until now `site.seo`
|
|
267
|
+
declared `titleTemplate`, verification tokens, `openGraph` and `twitter` that nothing read, while
|
|
268
|
+
`<Seo />` took a second, differently-shaped vocabulary through `vitops({ seo })` that no other stack
|
|
269
|
+
could see.
|
|
270
|
+
|
|
271
|
+
- `SeoSchema` moves to `@getvitops/config` and gains what only the integration had: `siteName`,
|
|
272
|
+
`defaultTitle`, `defaultDescription`, `openGraph.localeAlternates` and `openGraph.image` as
|
|
273
|
+
`{ src, alt, width, height, type }`. Text fields are localisable.
|
|
274
|
+
- `@getvitops/astro` exports `resolveSeoDefaults(config, { locale, env })`. The integration reads
|
|
275
|
+
`site.seo` from the site config it already loads; `siteName` falls back to `organization.name`, and
|
|
276
|
+
the environment's `robots` wins over `site.seo.robots`, the same fact `vitops robots` and
|
|
277
|
+
`search notify` read.
|
|
278
|
+
- `vitops({ seo })` keeps working unchanged and is now an **override** merged over the config, field by
|
|
279
|
+
field (`openGraph` and `twitter` merge key by key).
|
|
280
|
+
- **Removed from the config schema** (nothing read them): `descriptionTemplate` → use
|
|
281
|
+
`defaultDescription`; `openGraph.siteName`/`title`/`description` and
|
|
282
|
+
`twitter.title`/`description`/`image` → set per page on `<Seo />`, or the site-wide values above.
|
|
283
|
+
A config that still carries one now fails `vitops validate` with an unrecognised-key error.
|
|
284
|
+
- `openGraph.type` is now `website | article | profile`.
|
|
285
|
+
|
|
286
|
+
- 3bbb8c3: Add `toNotifyContext(config, locale?)` and `toCrmConfig(config)`, exported from
|
|
287
|
+
`@getvitops/astro/routes` beside `createConversionRoute()`. Builds the `context`/`crm.config`
|
|
288
|
+
options straight from a resolved `Config` instead of hand-writing a `NotifyContext` literal —
|
|
289
|
+
`conversion.ts`'s own docblock used to state this helper "does not exist."
|
|
290
|
+
|
|
291
|
+
`organization.name` is localizable; `toNotifyContext` resolves it for a given locale (default
|
|
292
|
+
`site.defaultLocale`), falling back to `legalName` when neither locale nor default resolves to
|
|
293
|
+
anything. `locationEmails` is ordered primary location first, then every other location with an
|
|
294
|
+
`email` set, in `organization.locations`' own key order.
|
|
295
|
+
|
|
296
|
+
### Patch Changes
|
|
297
|
+
|
|
298
|
+
- c3c1bcb: **New: the design-system editor, mounted inside the EmDash admin.** `vitopsEmdash({
|
|
299
|
+
designSystemEditor: { config } })` (`config` is your `site.json`, e.g. `import config from
|
|
300
|
+
'./site.json' with { type: 'json' }`) adds a `/design-system` admin page mounting
|
|
301
|
+
`<wc-theme-editor>` — the same live token editor `@getvitops/vite`'s dev server has always
|
|
302
|
+
offered, now usable by the client directly, no dev server required. An edit is visible
|
|
303
|
+
site-wide immediately (a runtime `:root` override, served via a new plugin API route and
|
|
304
|
+
`page:fragments`) and — once `site.deployment.repo`/`branch` and a `VITOPS_GITHUB_TOKEN`
|
|
305
|
+
secret are set (see the scaffold's `SETUP.html`) — auto-commits to your `dev` branch, so it
|
|
306
|
+
reaches the git-tracked `site.json` through your repo's normal review path rather than
|
|
307
|
+
becoming a second, unreviewed source of truth. Until that's configured, the live half still
|
|
308
|
+
works; only the auto-commit half is skipped (and says so in the admin page). The save route
|
|
309
|
+
runs the exact same accessibility (APCA contrast) gate `generate()` enforces at build time,
|
|
310
|
+
synchronously, before persisting — nothing non-compliant ever goes live even momentarily.
|
|
311
|
+
|
|
312
|
+
Requires `@astrojs/react` + `react` + `react-dom` (already scaffold dependencies, for
|
|
313
|
+
EmDash's own admin) and `@getvitops/core` (added as a new scaffold dependency this release).
|
|
314
|
+
Declares two new plugin capabilities when enabled: `network:request` (for the GitHub
|
|
315
|
+
auto-commit) and `hooks.page-fragments:register` (for the live override) — reviewed once at
|
|
316
|
+
plugin-install time.
|
|
317
|
+
|
|
318
|
+
**Breaking for `@getvitops/emdash`**: `vitopsEmdash()`'s `hooks.page-fragments:register`
|
|
319
|
+
capability is now also granted whenever `designSystemEditor` is set, independent of the
|
|
320
|
+
existing `scripts: 'fragments'` option (which governs something unrelated — web-component
|
|
321
|
+
script delivery). No change if you don't use `designSystemEditor`.
|
|
322
|
+
|
|
323
|
+
Also new, supporting the above:
|
|
324
|
+
|
|
325
|
+
- `@getvitops/generator`: `validateColorContract()` and `buildReverseIndex()` (both
|
|
326
|
+
`/tokens` subpath, Worker-safe) — the monotonicity/contrast gate and the
|
|
327
|
+
`design-manifest.json` reverse-index, each extracted from `generate()`'s `build()` into
|
|
328
|
+
standalone, non-throwing functions. `patchToOverrideCss()` turns a design-system patch
|
|
329
|
+
into the `:root` override `<style>` block. `designSystemPath`/`getAt`/`setAt`/
|
|
330
|
+
`mergePatch`/`formatConfig` moved from `@getvitops/vite` into `@getvitops/generator/config`
|
|
331
|
+
(re-exported from `@getvitops/vite` unchanged) — one "patch a config file's design-system
|
|
332
|
+
subtree" implementation shared by the dev endpoint and the new plugin route. New
|
|
333
|
+
`site.deployment.repo`/`branch` config fields.
|
|
334
|
+
- `@getvitops/utils`: new `/git` subpath — `commitFile()`, a Worker-safe (fetch-only)
|
|
335
|
+
wrapper over GitHub's Contents API.
|
|
336
|
+
- `@getvitops/core`: `<wc-theme-editor>` gains a `save-endpoint` attribute (default
|
|
337
|
+
unchanged — `/__vitops/design-system`) so the same element can target the new plugin
|
|
338
|
+
route instead of the dev-only Vite endpoint.
|
|
339
|
+
|
|
340
|
+
- 0574388: Bump `emdash`/`@emdash-cms/cloudflare` peers to `>=1.0.0 <2` and `vite-plus` to `^1.0.0`, following
|
|
341
|
+
both projects' GA releases.
|
|
342
|
+
|
|
343
|
+
**Breaking for `@getvitops/emdash` consumers on EmDash 0.x**: the peer range no longer admits
|
|
344
|
+
`emdash@0.31.x`/`@emdash-cms/cloudflare@0.31.x`. Upgrade `emdash` and `@emdash-cms/cloudflare` to
|
|
345
|
+
`^1.0.1` first — nothing in `@getvitops/emdash`'s own plugin surface (`admin.portableTextBlocks`,
|
|
346
|
+
the `hooks.page-fragments:register` capability, `definePlugin()`) changed against EmDash 1.0, so no
|
|
347
|
+
code here needed to change, only the declared range.
|
|
348
|
+
|
|
349
|
+
The `emdash` scaffold template (`vp create @getvitops:emdash`) picks up several fixes alongside
|
|
350
|
+
the version bump:
|
|
351
|
+
|
|
352
|
+
- `tailwindcss`/`@tailwindcss/vite` are now real dependencies — the template selected
|
|
353
|
+
`css.format: 'tailwind'` without installing either, which threw on build.
|
|
354
|
+
- `site.json` now sets `site.icons.sprite: true` plus the full `semantic` list the
|
|
355
|
+
`vitops.actionLink` block's icon picker offers — previously its sprite icons pointed at a file
|
|
356
|
+
the build never generated.
|
|
357
|
+
- `astro.config.mjs` sets `migrations: { runtime: 'check', dev: 'auto' }`, and
|
|
358
|
+
`site-deploy-emdash.yml` runs EmDash 1.0's documented `build → status → apply → deploy → check`
|
|
359
|
+
migration sequence explicitly, rather than relying solely on first-request auto-migration. This
|
|
360
|
+
needs a new GitHub Environment variable, `EMDASH_TARGET_FINGERPRINT` (one per environment),
|
|
361
|
+
which only a human can set — run `emdash migrate --status` against the real deployed database,
|
|
362
|
+
review the printed identity, then `gh variable set EMDASH_TARGET_FINGERPRINT --env <env> --body
|
|
363
|
+
<value>`. Deploys fail clearly on the migration-apply step until it's set.
|
|
364
|
+
- `lint.options.typeAware`/`typeCheck` in the template's own `vite.config.ts` are commented out,
|
|
365
|
+
not deleted: vite-plus 1.0 only honours that option in a project's actual root config, and hard
|
|
366
|
+
errors on it in a nested one (which this file is, sitting inside the `design-system` monorepo).
|
|
367
|
+
Uncomment it once scaffolded into its own repo.
|
|
368
|
+
|
|
369
|
+
No config schema changes. `@getvitops/generator` also switches from the `zod/mini` subpath import
|
|
370
|
+
to the standalone `@zod/mini` package (zod 4.5+; same API) and fixes `validateConfig()` silently
|
|
371
|
+
losing the offending key name on an `unrecognized_keys` error (e.g. a typo'd `site.ads` platform
|
|
372
|
+
now names the bad key instead of reporting a bare "Invalid input").
|
|
373
|
+
|
|
374
|
+
Also fixed: `vp check`'s `tsconfig-error` for `packages/create/templates/emdash/tsconfig.json`
|
|
375
|
+
(`extends: "astro/tsconfigs/strict"`, unresolvable from inside this monorepo since the template
|
|
376
|
+
has no `node_modules` of its own — a pre-existing, structural gap, newly surfaced as a hard error
|
|
377
|
+
by oxlint 1.85's deeper config discovery, and blocking on every commit since this repo's pre-commit
|
|
378
|
+
hook runs `vp check --fix`). Fixed with a new `scripts/link-template-deps.mjs`, wired as the root
|
|
379
|
+
`postinstall` script: symlinks `astro`/`emdash`/`@emdash-cms/cloudflare` into the template's own
|
|
380
|
+
`node_modules/` (resolved fresh from sibling packages that already depend on them for real, not a
|
|
381
|
+
hardcoded `.pnpm` hash) and writes a minimal `astro:content` ambient shim at `.astro/types.d.ts`
|
|
382
|
+
(the same path `astro sync` would generate for real). Both are gitignored and only ever exist
|
|
383
|
+
inside this monorepo's copy of the template — a real scaffold installs its own real copies instead.
|
|
384
|
+
Considered and rejected first: inlining astro's strict preset (drift risk against astro's own
|
|
385
|
+
evolving default) and making the template a real pnpm workspace member (would change how its
|
|
386
|
+
`@getvitops/*: "latest"` deps resolve locally, which `templates.test.ts` exists to protect).
|
|
387
|
+
|
|
388
|
+
**Known gap, not fixed here**: `packages/astro/src/components/container.test.ts` (the
|
|
389
|
+
`AstroContainer`-based render tests) fails to even load under vite-plus 1.0's Vite 8 — direct
|
|
390
|
+
`.astro` imports in a Vitest file need the Astro Vite plugin registered, which nothing in this
|
|
391
|
+
repo's test config actually does (Astro's own docs call for `getViteConfig()`, which doesn't
|
|
392
|
+
compose with vite-plus's own `defineConfig`). A type-level shim
|
|
393
|
+
(`packages/astro/src/components/astro-modules.d.ts`) fixes `vp check`'s type-aware pass, but the
|
|
394
|
+
runtime `vp test` failure for this one file is still open.
|
|
395
|
+
|
|
396
|
+
- 1a3e811: **Fixed: `<Icon />` no longer crashes a Cloudflare Workers SSR page.** It statically imported
|
|
397
|
+
`loadIconSvg`/`spriteId` from `@getvitops/generator`'s root entry, which also re-exports
|
|
398
|
+
`generate.ts` — reaching lightningcss, whose native binary Workers cannot `require()`.
|
|
399
|
+
Rendering `<Icon>` anywhere in an SSR project 500'd every request, regardless of
|
|
400
|
+
`icons.engine`, since the import itself (not just calling the function) pulled lightningcss
|
|
401
|
+
into the bundle. Fixed by publishing a new Worker-safe `@getvitops/generator/icons-sprite`
|
|
402
|
+
subpath (`buildIconSprite`/`loadIconSvg`/`spriteId`, no lightningcss in its import graph) and
|
|
403
|
+
switching `<Icon />` to import from there.
|
|
404
|
+
- 0f8c018: **New: `resolveOrganizationName(config, locale?)`**, exported from `@getvitops/astro/routes`
|
|
405
|
+
alongside `toNotifyContext`/`toCrmConfig` — the same `organization.name` → `legalName` →
|
|
406
|
+
`undefined` fallback chain `toNotifyContext` already used for its `organizationName` field,
|
|
407
|
+
now usable standalone anywhere a build-time-only display name is needed (favicon generation,
|
|
408
|
+
a scaffold's `astro.config.mjs`, a `Layout.astro` fallback). `toNotifyContext` is unchanged —
|
|
409
|
+
it now delegates to this helper internally.
|
|
410
|
+
|
|
411
|
+
The emdash scaffold uses it to remove a three-way duplicated `'My EmDash Site'` string:
|
|
412
|
+
`astro.config.mjs`'s favicon `name` and `Layout.astro`'s title fallback both previously
|
|
413
|
+
hardcoded it independently of `site.json`'s `organization.name`. EmDash's own
|
|
414
|
+
`getSiteSettings().title` (set in the admin) still wins at render time when it's set — it's a
|
|
415
|
+
runtime-only fact that can't be read at build time at all — this only replaces the two
|
|
416
|
+
hardcoded _fallback_ strings.
|
|
417
|
+
|
|
418
|
+
- 7f4f50e: `@getvitops/utils/url` exports `safeHref()` (a Worker-safe scheme allowlist: http, https, mailto, tel and relative), shared by the EmDash blocks and the Astro components. It now also guards the EmDash image-compare block, the review avatar and the cookie-notice policy link. `nodesToHtml`/`toHtml` throw on invalid tag or attribute names and on `on*` event-handler attributes, and drop `href`/`src`-style values that fail `safeHref`. EmDash cards link text now names the card for screen readers.
|
|
419
|
+
- 2ef2d41: **`resolveOrganizationName()` accepts a plain `import site from './site.json'`.** It was typed
|
|
420
|
+
against the full `Config`, which a JSON import cannot satisfy — TypeScript widens its literals
|
|
421
|
+
(`colors.utilities` becomes `string[]`, not the schema's union) — so every scaffolded site's
|
|
422
|
+
`Layout.astro` failed `astro check`. It now takes `OrganizationNameSource`, the slice it actually
|
|
423
|
+
reads (`organization.name` / `legalName`, `site.defaultLocale`); a resolved `Config` still
|
|
424
|
+
satisfies it, so nothing that compiled before stops compiling.
|
|
425
|
+
|
|
426
|
+
**Both templates now typecheck and lint clean on a fresh scaffold**, found by the new `scaffold`
|
|
427
|
+
CI job (it installs each template against this checkout's packed packages):
|
|
428
|
+
|
|
429
|
+
- `site.json` declares `neutral` and `surface` as `{ "hue": "ink", "kind": "surface" }`. As bare
|
|
430
|
+
hue strings they were chromatic roles, which emit no bare `bg-neutral` and no
|
|
431
|
+
`text-neutral-muted`, so the template's own footer class resolved to nothing and `vitops lint
|
|
432
|
+
--strict` (run by the template's pre-commit hook) failed. **Existing sites are unaffected** —
|
|
433
|
+
this changes only what a new scaffold starts with; if your own `site.json` has the same two
|
|
434
|
+
lines and uses `text-neutral-*` or `bg-surface*`, change them the same way.
|
|
435
|
+
- The `emdash` template's `index.astro` and `[...slug].astro` cast `page.data.title`, which is
|
|
436
|
+
`unknown` until `emdash types` has run against a live site, so `astro check` passed only after
|
|
437
|
+
the first `pnpm dev`.
|
|
438
|
+
|
|
439
|
+
No consumer-facing change in `@getvitops/core`, `@getvitops/generator`, `@getvitops/utils`,
|
|
440
|
+
`@getvitops/cli` or `@getvitops/vite`.
|
|
441
|
+
|
|
442
|
+
- fdc3f7e: `<wc-marquee>` adds a "Pause animation" button beside the track (WCAG 2.2.2), and the marquee also pauses while focus is inside it (`data-paused` and `:focus-within`). Pattern CSS: all formats. `<wc-consent>` moves focus into the banner when a visitor opens it from a settings control and returns it on close; an unprompted banner still doesn't take focus. `<wc-image-compare>`'s before/after panes are `role="group"` so their labels are announced. The conversion route format-checks `phone`/`email` beacon payloads and imports the Worker-safe `@getvitops/utils/content` instead of the utils root barrel.
|
|
443
|
+
- Updated dependencies [65a13cc]
|
|
444
|
+
- Updated dependencies [67e2088]
|
|
445
|
+
- Updated dependencies [89265af]
|
|
446
|
+
- Updated dependencies [0574388]
|
|
447
|
+
- Updated dependencies [c3c1bcb]
|
|
448
|
+
- Updated dependencies [0574388]
|
|
449
|
+
- Updated dependencies [734ab60]
|
|
450
|
+
- Updated dependencies [1a8231d]
|
|
451
|
+
- Updated dependencies [779d22e]
|
|
452
|
+
- Updated dependencies [0574388]
|
|
453
|
+
- Updated dependencies [8d8aad7]
|
|
454
|
+
- Updated dependencies [0574388]
|
|
455
|
+
- Updated dependencies [1a3e811]
|
|
456
|
+
- Updated dependencies [0574388]
|
|
457
|
+
- Updated dependencies [dde3df3]
|
|
458
|
+
- Updated dependencies [d9f213b]
|
|
459
|
+
- Updated dependencies [7f4f50e]
|
|
460
|
+
- Updated dependencies [7fd5f8f]
|
|
461
|
+
- Updated dependencies [ceb8310]
|
|
462
|
+
- Updated dependencies [88b465a]
|
|
463
|
+
- Updated dependencies [fdc3f7e]
|
|
464
|
+
- @getvitops/generator@8.2.0
|
|
465
|
+
- @getvitops/config@8.2.0
|
|
466
|
+
- @getvitops/utils@8.2.0
|
|
467
|
+
- @getvitops/core@8.2.0
|
|
468
|
+
- @getvitops/vite@8.2.0
|
|
469
|
+
|
|
470
|
+
## 8.2.0-alpha.0
|
|
471
|
+
|
|
472
|
+
### Minor Changes
|
|
473
|
+
|
|
474
|
+
- 65a13cc: **New: `site.experiments`** — runtime, per-visitor A/B content experiments for a page that
|
|
475
|
+
must stay at one URL (unlike `site.abTesting`'s existing build-time variant merge, which is
|
|
476
|
+
for a full design-system/CSS-level test via two manual deploys — `site.experiments` is the
|
|
477
|
+
opposite granularity: one page, one cookie, content branched in the page itself). Keyed by
|
|
478
|
+
an open experiment name, so more than one can run at once, each with its own consent
|
|
479
|
+
category, cookie name/lifetime, split ratio and pair of variant names.
|
|
480
|
+
|
|
481
|
+
**New `vitopsAbTesting()` middleware** (`@getvitops/astro/middleware`) — the first Astro
|
|
482
|
+
middleware this toolchain ships. Wire it into your own `src/middleware.ts`:
|
|
483
|
+
|
|
484
|
+
```ts
|
|
485
|
+
import { vitopsAbTesting } from '@getvitops/astro/middleware';
|
|
486
|
+
import config from '../site.json' with { type: 'json' };
|
|
487
|
+
|
|
488
|
+
export const onRequest = vitopsAbTesting({
|
|
489
|
+
experiments: config.site.experiments ?? {},
|
|
490
|
+
});
|
|
491
|
+
```
|
|
492
|
+
|
|
493
|
+
It assigns and cookie-pins a visitor to one of an experiment's two variants on first visit,
|
|
494
|
+
by weighted random (`splitRatio`) — and never touches an already-set cookie, so a recognized
|
|
495
|
+
query parameter named after the cookie itself (`?_ab_<key>=<variant>`) reliably forces that
|
|
496
|
+
variant, even overriding an existing assignment. Combined with `splitRatio: 0`, this is a
|
|
497
|
+
blue/green-style "100% on control, but a shared link reveals the other variant" mode with no
|
|
498
|
+
special-cased logic.
|
|
499
|
+
|
|
500
|
+
A page reads its own experiment's cookie (`Astro.cookies.get(cookieName)?.value`) and
|
|
501
|
+
renders whichever of its two hand-written blocks matches — there's no config-schema
|
|
502
|
+
representation of "the variant content," which is what keeps this lightweight regardless of
|
|
503
|
+
how many experiments are configured.
|
|
504
|
+
|
|
505
|
+
**`<Analytics />` gained a reporting hook**: each configured provider's bootstrap now
|
|
506
|
+
attaches an enabled experiment's assignment to the real pageview via that provider's own
|
|
507
|
+
mechanism — Plausible via `plausible.init({customProperties})`, Matomo via
|
|
508
|
+
`setCustomDimension` (needs a new `site.analytics.matomo.abVariantDimensions` entry — the
|
|
509
|
+
dimension must exist in your Matomo instance first), GA4 via a runtime-merged
|
|
510
|
+
`gtag('config', …)` call. A no-op when `site.experiments` is unset — zero behavior change
|
|
511
|
+
for every existing site.
|
|
512
|
+
|
|
513
|
+
Also new: `experimentDimensionWarnings(site)` (`@getvitops/generator`) — a build-time warning
|
|
514
|
+
(not a hard error) when an enabled experiment's site also configures Matomo but has no
|
|
515
|
+
matching `abVariantDimensions` entry.
|
|
516
|
+
|
|
517
|
+
**Not included in this pass, deliberately**: a mechanism for a fully prerendered
|
|
518
|
+
(`output: 'static'`) page — this needs a genuinely separate Cloudflare Worker Route + two
|
|
519
|
+
pre-built static variants, real but bounded follow-up work. Everything above applies to an
|
|
520
|
+
on-demand-rendered page (the current EmDash templates' default), which needs zero new
|
|
521
|
+
deploy infrastructure.
|
|
522
|
+
|
|
523
|
+
- 0574388: Add `site.crm` — push form submissions into HubSpot, Zoho Bigin, or Jobber.
|
|
524
|
+
|
|
525
|
+
Client sites need form submissions to land in the client's CRM. The options available before this
|
|
526
|
+
— a HubSpot/Bigin/Jobber iframe or JS embed — fail on three counts: the markup can't be
|
|
527
|
+
white-labelled against this design system, nothing can operate on the submission before it leaves
|
|
528
|
+
the page, and the submission never touched `createConversionRoute()`, so `_ac` ad-click attribution
|
|
529
|
+
was lost and no conversion notification was planned.
|
|
530
|
+
|
|
531
|
+
This adds one server-side path for all three CRMs, hung off the conversion route that already
|
|
532
|
+
exists. `createConversionRoute()` gets a new `crm` option (`{ config, credentials, fetch?,
|
|
533
|
+
timeoutMs? }`); credentials arrive as a function of `locals`, exactly like the existing `binding`
|
|
534
|
+
option, and are never config. A failed CRM write never fails the request and is folded into the
|
|
535
|
+
notification email's new "Delivery" section, so a broken CRM token is never silent.
|
|
536
|
+
|
|
537
|
+
New `@getvitops/utils/crm` subpath: `deliverToCrm()`, `mapSubmission()`, `deriveLastName()` (Bigin
|
|
538
|
+
requires a last name; this derives one from an explicit field, a full name, or an email address,
|
|
539
|
+
refusing rather than fabricating one), `submitToHubSpot()`, `submitToBigin()`, `submitToJobber()`.
|
|
540
|
+
`identifySubmitter()` was promoted out of `notify`'s private name-guessing cascade into
|
|
541
|
+
`@getvitops/utils/tracking`, since the CRM mapper needed the whole identity (first/last name,
|
|
542
|
+
email, phone, company), not just a display name.
|
|
543
|
+
|
|
544
|
+
Jobber is GraphQL, not REST, and answers `200 OK` for failures — a rejected `clientCreate` returns
|
|
545
|
+
`userErrors[]`, a malformed request returns top-level `errors[]`. `submitToJobber()` classifies on
|
|
546
|
+
the response body, not just the status. Its refresh tokens also rotate by default: **Refresh Token
|
|
547
|
+
Rotation must be turned off** in the app's Developer Center settings, since a Worker reading a
|
|
548
|
+
static `VITOPS_JOBBER_REFRESH_TOKEN` cannot persist a rotated one — a rotated token is detected at
|
|
549
|
+
exchange time and reported with the fix, rather than surfacing an hour later as an opaque 401.
|
|
550
|
+
Attribution and unmapped extra fields render into a best-effort note by default (works on every
|
|
551
|
+
Jobber plan); a resubmitting visitor creates a second Jobber client unless `crm.jobber.dedupe: true`
|
|
552
|
+
is set once its lookup query has been verified against a live account (defaults `false` pending
|
|
553
|
+
that verification — see `jobber.ts`'s docblock).
|
|
554
|
+
|
|
555
|
+
New `site.crm` config block (`provider`, `hubspot`/`bigin`/`jobber`, `fieldMap`, `trackingFields`)
|
|
556
|
+
and two CLI commands: `vitops crm check` (fetches the live HubSpot property list or Bigin field
|
|
557
|
+
list and reports every mapped name that doesn't exist there — HubSpot rejects an unknown field
|
|
558
|
+
outright, Bigin silently drops it; Jobber has nothing live to check yet, since its mapped values
|
|
559
|
+
currently render as note text) and `vitops crm setup` (creates missing HubSpot properties; Bigin
|
|
560
|
+
and Jobber have no such API, so both always print a manual reminder).
|
|
561
|
+
|
|
562
|
+
The generated privacy policy now names HubSpot, Zoho Bigin, or Jobber when `site.crm` configures
|
|
563
|
+
one. Bigin gets the same cross-border-transfer derivation Matomo Cloud already gets: a
|
|
564
|
+
`dataCenter: 'ca'` asserts no transfer outside Canada, any other data centre does. Jobber's
|
|
565
|
+
operator (Octopusapp Inc. d/b/a Jobber) is Canadian, but its own privacy policy names the United
|
|
566
|
+
States as a processing location too, so the policy always asserts a cross-border transfer for
|
|
567
|
+
Jobber regardless of config — a Canadian operator is not read as Canadian-only storage.
|
|
568
|
+
|
|
569
|
+
Credentials: `VITOPS_HUBSPOT_ACCESS_TOKEN` for HubSpot; `VITOPS_ZOHO_CLIENT_ID` /
|
|
570
|
+
`VITOPS_ZOHO_CLIENT_SECRET` / `VITOPS_ZOHO_REFRESH_TOKEN` for Bigin; `VITOPS_JOBBER_CLIENT_ID` /
|
|
571
|
+
`VITOPS_JOBBER_CLIENT_SECRET` / `VITOPS_JOBBER_REFRESH_TOKEN` for Jobber. Never config.
|
|
572
|
+
|
|
573
|
+
No consumer-facing change in `@getvitops/astro` beyond the new `crm` route option and a build
|
|
574
|
+
warning when `site.crm` is configured with no route answering `TRACKING_ENDPOINT` — CSS/Bricks/
|
|
575
|
+
tailwind outputs are unaffected.
|
|
576
|
+
|
|
577
|
+
- 0574388: Fire a Google Ads conversion on a tel:/mailto: click, and fix a rate-limit bug on the conversion
|
|
578
|
+
route that could silently drop one of the two.
|
|
579
|
+
|
|
580
|
+
`site.ads.google.conversionLabel` used to be accepted by the config schema and typed all the way
|
|
581
|
+
through to the rendered tag, but nothing actually read it — no `gtag('event', 'conversion', …)`
|
|
582
|
+
was ever emitted. When set, the click tracker `<Tracking />` inlines now fires the conversion at
|
|
583
|
+
the moment a visitor taps a `tel:` or `mailto:` link, using the `AW-…/label` target Google Ads
|
|
584
|
+
expects. A `conversionLabel` that can't safely land inside the inline bootstrap script (the same
|
|
585
|
+
check `pixelId`/`accountId` already get) is dropped with a warning rather than firing something
|
|
586
|
+
malformed — the ad tag itself still loads.
|
|
587
|
+
|
|
588
|
+
Also: `createConversionRoute()`'s rate limiter previously keyed on IP address alone, so a call
|
|
589
|
+
beacon and an email-click beacon from the same visitor, seconds apart, competed for one window —
|
|
590
|
+
the second could 429 and vanish with no error anywhere. It's now keyed on IP _and_ event type, so
|
|
591
|
+
distinct conversion types never starve each other; repeats of the same type are still throttled
|
|
592
|
+
exactly as before.
|
|
593
|
+
|
|
594
|
+
No config changes — a site with `site.ads.google.conversionLabel` already set picks this up
|
|
595
|
+
automatically on the next build.
|
|
596
|
+
|
|
597
|
+
- 2a0a559: The Astro integration now warns when `site.locales`/`site.defaultLocale` disagree with
|
|
598
|
+
`astro.config.mjs`'s own `i18n` block — naming which locale codes are missing from each side, and
|
|
599
|
+
when the two `defaultLocale`s differ. EmDash reads its locale list straight from `i18n`, while our
|
|
600
|
+
own SEO/legal code reads `site.locales`, so the two had no way to notice they'd drifted apart. A
|
|
601
|
+
warning, not a build error — like the existing ads/analytics/consent consistency checks, either
|
|
602
|
+
side can legitimately be configured without the other, so only a genuine disagreement is flagged.
|
|
603
|
+
- 0574388: Track `mailto:` link clicks as conversions, alongside the existing `tel:` link tracking.
|
|
604
|
+
|
|
605
|
+
Before this, the capture script only beaconed `tel:` clicks; a `mailto:` link on the same page was
|
|
606
|
+
invisible to attribution — a visitor who emailed the business instead of calling generated no
|
|
607
|
+
conversion event at all. `<Tracking />`'s inlined script now listens for both, sending `{ event:
|
|
608
|
+
'email', email }` via `navigator.sendBeacon` the same way it already sends `{ event: 'call', phone
|
|
609
|
+
}`.
|
|
610
|
+
|
|
611
|
+
`ConversionEvent['type']` gains an `'email'` member alongside `'form'`/`'call'`, with a new
|
|
612
|
+
`email` field. `createConversionRoute()` handles it exactly like a call: no honeypot, Turnstile or
|
|
613
|
+
CRM checks (there are no form fields to validate or map), a 204 response, and a notification email
|
|
614
|
+
subject reading `[Email Click] Visitor emailing ...`.
|
|
615
|
+
|
|
616
|
+
No config changes and no new options — a site with `<Tracking />` already mounted picks this up
|
|
617
|
+
automatically the next time it rebuilds.
|
|
618
|
+
|
|
619
|
+
- dde3df3: **`site.ads.google.conversionLabel` now accepts a per-action map** —
|
|
620
|
+
`{ call?, email?, form? }` — instead of one bare string shared by every
|
|
621
|
+
interaction. A bare string still works (back-compat, treated as `call`), but a
|
|
622
|
+
call and an email click used to report as the **same** Google Ads conversion
|
|
623
|
+
action, indistinguishable in the account; each action now gets its own
|
|
624
|
+
`send_to` target.
|
|
625
|
+
|
|
626
|
+
**New: `vitopsFormConversion(formEl)`**, exposed as `window.vitopsFormConversion`
|
|
627
|
+
by `<Tracking />`'s capture script — call it from your own fetch-based form
|
|
628
|
+
submit handler once the POST to `createConversionRoute()` succeeds, to fire the
|
|
629
|
+
`form` Google Ads conversion action client-side (mirroring the existing
|
|
630
|
+
tel:/mailto: click handler, which now fires `call`/`email` instead of sharing
|
|
631
|
+
one label). Idempotent per form element. There's deliberately no server-side
|
|
632
|
+
equivalent for a no-JS `<form method="post">` submission — that's a full-page
|
|
633
|
+
redirect with no script to fire from, and a real server-side Google Ads
|
|
634
|
+
conversion needs the Ads API's click-conversion upload (developer token + MCC
|
|
635
|
+
access), tracked separately in the vNext backlog.
|
|
636
|
+
|
|
637
|
+
**Fixed: the conversion route's rate limiter no longer trusts the client's
|
|
638
|
+
raw `event` field.** It used to key on whatever string a client sent
|
|
639
|
+
(`` `${ip}:${body.event}` ``), so varying that value on every request let a
|
|
640
|
+
form submission dodge the per-IP rate limit entirely — only `'call'`/`'email'`
|
|
641
|
+
were ever recognized, and anything else got its own fresh bucket. The key now
|
|
642
|
+
uses the server's own classification (`call` | `email` | `form`), closing that
|
|
643
|
+
bypass without changing the existing call-vs-email-don't-compete behavior.
|
|
644
|
+
|
|
645
|
+
**New: `site.notifications.actionUrl`** — an optional action-link template
|
|
646
|
+
added to the conversion-notification email as an "Actions" section.
|
|
647
|
+
`{clickId}` is replaced with the ad click ID captured on `_ac` (gclid, fbclid,
|
|
648
|
+
etc.); the section is omitted when the event has no click ID to interpolate,
|
|
649
|
+
rather than emailing a broken link.
|
|
650
|
+
|
|
651
|
+
- 3bbb8c3: Add `toNotifyContext(config, locale?)` and `toCrmConfig(config)`, exported from
|
|
652
|
+
`@getvitops/astro/routes` beside `createConversionRoute()`. Builds the `context`/`crm.config`
|
|
653
|
+
options straight from a resolved `Config` instead of hand-writing a `NotifyContext` literal —
|
|
654
|
+
`conversion.ts`'s own docblock used to state this helper "does not exist."
|
|
655
|
+
|
|
656
|
+
`organization.name` is localizable; `toNotifyContext` resolves it for a given locale (default
|
|
657
|
+
`site.defaultLocale`), falling back to `legalName` when neither locale nor default resolves to
|
|
658
|
+
anything. `locationEmails` is ordered primary location first, then every other location with an
|
|
659
|
+
`email` set, in `organization.locations`' own key order.
|
|
660
|
+
|
|
661
|
+
### Patch Changes
|
|
662
|
+
|
|
663
|
+
- c3c1bcb: **New: the design-system editor, mounted inside the EmDash admin.** `vitopsEmdash({
|
|
664
|
+
designSystemEditor: { config } })` (`config` is your `site.json`, e.g. `import config from
|
|
665
|
+
'./site.json' with { type: 'json' }`) adds a `/design-system` admin page mounting
|
|
666
|
+
`<wc-theme-editor>` — the same live token editor `@getvitops/vite`'s dev server has always
|
|
667
|
+
offered, now usable by the client directly, no dev server required. An edit is visible
|
|
668
|
+
site-wide immediately (a runtime `:root` override, served via a new plugin API route and
|
|
669
|
+
`page:fragments`) and — once `site.deployment.repo`/`branch` and a `VITOPS_GITHUB_TOKEN`
|
|
670
|
+
secret are set (see the scaffold's `SETUP.html`) — auto-commits to your `dev` branch, so it
|
|
671
|
+
reaches the git-tracked `site.json` through your repo's normal review path rather than
|
|
672
|
+
becoming a second, unreviewed source of truth. Until that's configured, the live half still
|
|
673
|
+
works; only the auto-commit half is skipped (and says so in the admin page). The save route
|
|
674
|
+
runs the exact same accessibility (APCA contrast) gate `generate()` enforces at build time,
|
|
675
|
+
synchronously, before persisting — nothing non-compliant ever goes live even momentarily.
|
|
676
|
+
|
|
677
|
+
Requires `@astrojs/react` + `react` + `react-dom` (already scaffold dependencies, for
|
|
678
|
+
EmDash's own admin) and `@getvitops/core` (added as a new scaffold dependency this release).
|
|
679
|
+
Declares two new plugin capabilities when enabled: `network:request` (for the GitHub
|
|
680
|
+
auto-commit) and `hooks.page-fragments:register` (for the live override) — reviewed once at
|
|
681
|
+
plugin-install time.
|
|
682
|
+
|
|
683
|
+
**Breaking for `@getvitops/emdash`**: `vitopsEmdash()`'s `hooks.page-fragments:register`
|
|
684
|
+
capability is now also granted whenever `designSystemEditor` is set, independent of the
|
|
685
|
+
existing `scripts: 'fragments'` option (which governs something unrelated — web-component
|
|
686
|
+
script delivery). No change if you don't use `designSystemEditor`.
|
|
687
|
+
|
|
688
|
+
Also new, supporting the above:
|
|
689
|
+
- `@getvitops/generator`: `validateColorContract()` and `buildReverseIndex()` (both
|
|
690
|
+
`/tokens` subpath, Worker-safe) — the monotonicity/contrast gate and the
|
|
691
|
+
`design-manifest.json` reverse-index, each extracted from `generate()`'s `build()` into
|
|
692
|
+
standalone, non-throwing functions. `patchToOverrideCss()` turns a design-system patch
|
|
693
|
+
into the `:root` override `<style>` block. `designSystemPath`/`getAt`/`setAt`/
|
|
694
|
+
`mergePatch`/`formatConfig` moved from `@getvitops/vite` into `@getvitops/generator/config`
|
|
695
|
+
(re-exported from `@getvitops/vite` unchanged) — one "patch a config file's design-system
|
|
696
|
+
subtree" implementation shared by the dev endpoint and the new plugin route. New
|
|
697
|
+
`site.deployment.repo`/`branch` config fields.
|
|
698
|
+
- `@getvitops/utils`: new `/git` subpath — `commitFile()`, a Worker-safe (fetch-only)
|
|
699
|
+
wrapper over GitHub's Contents API.
|
|
700
|
+
- `@getvitops/core`: `<wc-theme-editor>` gains a `save-endpoint` attribute (default
|
|
701
|
+
unchanged — `/__vitops/design-system`) so the same element can target the new plugin
|
|
702
|
+
route instead of the dev-only Vite endpoint.
|
|
703
|
+
|
|
704
|
+
- 0574388: Bump `emdash`/`@emdash-cms/cloudflare` peers to `>=1.0.0 <2` and `vite-plus` to `^1.0.0`, following
|
|
705
|
+
both projects' GA releases.
|
|
706
|
+
|
|
707
|
+
**Breaking for `@getvitops/emdash` consumers on EmDash 0.x**: the peer range no longer admits
|
|
708
|
+
`emdash@0.31.x`/`@emdash-cms/cloudflare@0.31.x`. Upgrade `emdash` and `@emdash-cms/cloudflare` to
|
|
709
|
+
`^1.0.1` first — nothing in `@getvitops/emdash`'s own plugin surface (`admin.portableTextBlocks`,
|
|
710
|
+
the `hooks.page-fragments:register` capability, `definePlugin()`) changed against EmDash 1.0, so no
|
|
711
|
+
code here needed to change, only the declared range.
|
|
712
|
+
|
|
713
|
+
The `emdash` scaffold template (`vp create @getvitops:emdash`) picks up several fixes alongside
|
|
714
|
+
the version bump:
|
|
715
|
+
- `tailwindcss`/`@tailwindcss/vite` are now real dependencies — the template selected
|
|
716
|
+
`css.format: 'tailwind'` without installing either, which threw on build.
|
|
717
|
+
- `site.json` now sets `site.icons.sprite: true` plus the full `semantic` list the
|
|
718
|
+
`vitops.actionLink` block's icon picker offers — previously its sprite icons pointed at a file
|
|
719
|
+
the build never generated.
|
|
720
|
+
- `astro.config.mjs` sets `migrations: { runtime: 'check', dev: 'auto' }`, and
|
|
721
|
+
`site-deploy-emdash.yml` runs EmDash 1.0's documented `build → status → apply → deploy → check`
|
|
722
|
+
migration sequence explicitly, rather than relying solely on first-request auto-migration. This
|
|
723
|
+
needs a new GitHub Environment variable, `EMDASH_TARGET_FINGERPRINT` (one per environment),
|
|
724
|
+
which only a human can set — run `emdash migrate --status` against the real deployed database,
|
|
725
|
+
review the printed identity, then `gh variable set EMDASH_TARGET_FINGERPRINT --env <env> --body
|
|
726
|
+
<value>`. Deploys fail clearly on the migration-apply step until it's set.
|
|
727
|
+
- `lint.options.typeAware`/`typeCheck` in the template's own `vite.config.ts` are commented out,
|
|
728
|
+
not deleted: vite-plus 1.0 only honours that option in a project's actual root config, and hard
|
|
729
|
+
errors on it in a nested one (which this file is, sitting inside the `design-system` monorepo).
|
|
730
|
+
Uncomment it once scaffolded into its own repo.
|
|
731
|
+
|
|
732
|
+
No config schema changes. `@getvitops/generator` also switches from the `zod/mini` subpath import
|
|
733
|
+
to the standalone `@zod/mini` package (zod 4.5+; same API) and fixes `validateConfig()` silently
|
|
734
|
+
losing the offending key name on an `unrecognized_keys` error (e.g. a typo'd `site.ads` platform
|
|
735
|
+
now names the bad key instead of reporting a bare "Invalid input").
|
|
736
|
+
|
|
737
|
+
Also fixed: `vp check`'s `tsconfig-error` for `packages/create/templates/emdash/tsconfig.json`
|
|
738
|
+
(`extends: "astro/tsconfigs/strict"`, unresolvable from inside this monorepo since the template
|
|
739
|
+
has no `node_modules` of its own — a pre-existing, structural gap, newly surfaced as a hard error
|
|
740
|
+
by oxlint 1.85's deeper config discovery, and blocking on every commit since this repo's pre-commit
|
|
741
|
+
hook runs `vp check --fix`). Fixed with a new `scripts/link-template-deps.mjs`, wired as the root
|
|
742
|
+
`postinstall` script: symlinks `astro`/`emdash`/`@emdash-cms/cloudflare` into the template's own
|
|
743
|
+
`node_modules/` (resolved fresh from sibling packages that already depend on them for real, not a
|
|
744
|
+
hardcoded `.pnpm` hash) and writes a minimal `astro:content` ambient shim at `.astro/types.d.ts`
|
|
745
|
+
(the same path `astro sync` would generate for real). Both are gitignored and only ever exist
|
|
746
|
+
inside this monorepo's copy of the template — a real scaffold installs its own real copies instead.
|
|
747
|
+
Considered and rejected first: inlining astro's strict preset (drift risk against astro's own
|
|
748
|
+
evolving default) and making the template a real pnpm workspace member (would change how its
|
|
749
|
+
`@getvitops/*: "latest"` deps resolve locally, which `templates.test.ts` exists to protect).
|
|
750
|
+
|
|
751
|
+
**Known gap, not fixed here**: `packages/astro/src/components/container.test.ts` (the
|
|
752
|
+
`AstroContainer`-based render tests) fails to even load under vite-plus 1.0's Vite 8 — direct
|
|
753
|
+
`.astro` imports in a Vitest file need the Astro Vite plugin registered, which nothing in this
|
|
754
|
+
repo's test config actually does (Astro's own docs call for `getViteConfig()`, which doesn't
|
|
755
|
+
compose with vite-plus's own `defineConfig`). A type-level shim
|
|
756
|
+
(`packages/astro/src/components/astro-modules.d.ts`) fixes `vp check`'s type-aware pass, but the
|
|
757
|
+
runtime `vp test` failure for this one file is still open.
|
|
758
|
+
|
|
759
|
+
- 1a3e811: **Fixed: `<Icon />` no longer crashes a Cloudflare Workers SSR page.** It statically imported
|
|
760
|
+
`loadIconSvg`/`spriteId` from `@getvitops/generator`'s root entry, which also re-exports
|
|
761
|
+
`generate.ts` — reaching lightningcss, whose native binary Workers cannot `require()`.
|
|
762
|
+
Rendering `<Icon>` anywhere in an SSR project 500'd every request, regardless of
|
|
763
|
+
`icons.engine`, since the import itself (not just calling the function) pulled lightningcss
|
|
764
|
+
into the bundle. Fixed by publishing a new Worker-safe `@getvitops/generator/icons-sprite`
|
|
765
|
+
subpath (`buildIconSprite`/`loadIconSvg`/`spriteId`, no lightningcss in its import graph) and
|
|
766
|
+
switching `<Icon />` to import from there.
|
|
767
|
+
- 0f8c018: **New: `resolveOrganizationName(config, locale?)`**, exported from `@getvitops/astro/routes`
|
|
768
|
+
alongside `toNotifyContext`/`toCrmConfig` — the same `organization.name` → `legalName` →
|
|
769
|
+
`undefined` fallback chain `toNotifyContext` already used for its `organizationName` field,
|
|
770
|
+
now usable standalone anywhere a build-time-only display name is needed (favicon generation,
|
|
771
|
+
a scaffold's `astro.config.mjs`, a `Layout.astro` fallback). `toNotifyContext` is unchanged —
|
|
772
|
+
it now delegates to this helper internally.
|
|
773
|
+
|
|
774
|
+
The emdash scaffold uses it to remove a three-way duplicated `'My EmDash Site'` string:
|
|
775
|
+
`astro.config.mjs`'s favicon `name` and `Layout.astro`'s title fallback both previously
|
|
776
|
+
hardcoded it independently of `site.json`'s `organization.name`. EmDash's own
|
|
777
|
+
`getSiteSettings().title` (set in the admin) still wins at render time when it's set — it's a
|
|
778
|
+
runtime-only fact that can't be read at build time at all — this only replaces the two
|
|
779
|
+
hardcoded _fallback_ strings.
|
|
780
|
+
|
|
781
|
+
- Updated dependencies [65a13cc]
|
|
782
|
+
- Updated dependencies [67e2088]
|
|
783
|
+
- Updated dependencies [0574388]
|
|
784
|
+
- Updated dependencies [c3c1bcb]
|
|
785
|
+
- Updated dependencies [0574388]
|
|
786
|
+
- Updated dependencies [0574388]
|
|
787
|
+
- Updated dependencies [0574388]
|
|
788
|
+
- Updated dependencies [1a3e811]
|
|
789
|
+
- Updated dependencies [0574388]
|
|
790
|
+
- Updated dependencies [dde3df3]
|
|
791
|
+
- Updated dependencies [d9f213b]
|
|
792
|
+
- @getvitops/generator@8.2.0-alpha.0
|
|
793
|
+
- @getvitops/utils@8.2.0-alpha.0
|
|
794
|
+
- @getvitops/core@8.2.0-alpha.0
|
|
795
|
+
- @getvitops/vite@8.2.0-alpha.0
|
|
796
|
+
|
|
3
797
|
## 8.1.0
|
|
4
798
|
|
|
5
799
|
### Patch Changes
|
|
@@ -52,7 +846,6 @@
|
|
|
52
846
|
|
|
53
847
|
**Breaking (`css`/`bricks` and `tailwind` formats — `.lightbox-dialog`'s markup
|
|
54
848
|
contract changed):**
|
|
55
|
-
|
|
56
849
|
- `<Gallery />` now emits ONE `<dialog class="lightbox-dialog">` per gallery,
|
|
57
850
|
wrapping a `<Carousel />`, instead of one dialog per image. A `.gallery__trigger`
|
|
58
851
|
is now a plain `<a href="#slide-id">`, not a `command="show-modal"` button.
|
|
@@ -93,7 +886,6 @@
|
|
|
93
886
|
### Minor Changes
|
|
94
887
|
|
|
95
888
|
- Add Google Business Profile reviews: fetch, cache, render.
|
|
96
|
-
|
|
97
889
|
- **`vitops reviews`** (new CLI command) fetches a client's GBP reviews (legacy v4 API — the only
|
|
98
890
|
one that has them) and writes them to `.vitops/reviews.json`, meant to be committed so a build
|
|
99
891
|
never needs a live Google credential. `--dry` prints the plan; `--check` diffs against the cache
|
|
@@ -128,7 +920,6 @@
|
|
|
128
920
|
`--site` for now.)
|
|
129
921
|
|
|
130
922
|
**Fixed:**
|
|
131
|
-
|
|
132
923
|
- `@getvitops/core`'s 12 runtime deps (`lit`, both `@oddbird` polyfills, both
|
|
133
924
|
`@webcomponents` packages, `colorjs.io`, etc.) were listed as `dependencies` despite
|
|
134
925
|
being bundled into `dist/elements.js`/`dist/polyfills.js`/`dist/deferred.js` — a
|
|
@@ -274,7 +1065,7 @@
|
|
|
274
1065
|
created, with `patterns/counter.css`'s own reduced-motion query as a second belt.
|
|
275
1066
|
- The accessible name stays the final value for the whole animation: `.counter__value` is toggled
|
|
276
1067
|
to the framework's `.sr-only` utility while running, never hidden — `display: none`/`visibility:
|
|
277
|
-
hidden` would pull it out of the accessible-name computation along with the visual hiding, and a
|
|
1068
|
+
hidden` would pull it out of the accessible-name computation along with the visual hiding, and a
|
|
278
1069
|
screen reader must never be read ~60 intermediate numbers. The animated text lives in a separate
|
|
279
1070
|
`aria-hidden` presentation span it never reaches.
|
|
280
1071
|
- `data-from` (default `0`), `data-duration` (ms, default `1200`), `data-easing` (`linear` |
|
|
@@ -985,7 +1776,7 @@ hidden` would pull it out of the accessible-name computation along with the visu
|
|
|
985
1776
|
`animate-view` / `animate-scroll` / `animate-trigger` / `transition`, and every effect
|
|
986
1777
|
gained an `open-<fx>` state variant. Overlays now state _where they start_
|
|
987
1778
|
(`--translate-x-from`) instead of owning a keyframe: `class="drawer drawer--right
|
|
988
|
-
open-fade-in"` composes slide and fade. Applied at zero specificity with identity
|
|
1779
|
+
open-fade-in"` composes slide and fade. Applied at zero specificity with identity
|
|
989
1780
|
defaults, so a popover that sets no effect vars is unchanged.
|
|
990
1781
|
- **One scrim.** `--scrim` / `--scrim-filter` tokens and `.no-scrim` in `popover.css`,
|
|
991
1782
|
replacing 18 `::backdrop` blocks across seven partials. `.drawer--modeless` is aliased.
|
|
@@ -1060,7 +1851,7 @@ open-fade-in"` composes slide and fade. Applied at zero specificity with identit
|
|
|
1060
1851
|
Also new, and worth wiring at the same time — **`gitLastmod()` for real sitemap dates**:
|
|
1061
1852
|
|
|
1062
1853
|
```js
|
|
1063
|
-
import vitops, { gitLastmod } from
|
|
1854
|
+
import vitops, { gitLastmod } from '@getvitops/astro';
|
|
1064
1855
|
vitops({ sitemap: { serialize: await gitLastmod() } });
|
|
1065
1856
|
```
|
|
1066
1857
|
|
|
@@ -1126,7 +1917,7 @@ open-fade-in"` composes slide and fade. Applied at zero specificity with identit
|
|
|
1126
1917
|
toolchain bump. Use `--force` when you mean to re-encode.
|
|
1127
1918
|
|
|
1128
1919
|
**`ffmpeg` is an external tool, not an npm dependency** — install it yourself (`brew install
|
|
1129
|
-
ffmpeg`, `apt install ffmpeg`, `winget install Gyan.FFmpeg`). The command fails without it rather
|
|
1920
|
+
ffmpeg`, `apt install ffmpeg`, `winget install Gyan.FFmpeg`). The command fails without it rather
|
|
1130
1921
|
than skipping: a page referencing a video that was never encoded is broken, not degraded.
|
|
1131
1922
|
|
|
1132
1923
|
Two defaults worth knowing about. The MP4 exists because older iOS and the in-app webviews inside
|
|
@@ -1160,8 +1951,8 @@ ffmpeg`, `apt install ffmpeg`, `winget install Gyan.FFmpeg`). The command fails
|
|
|
1160
1951
|
|
|
1161
1952
|
```js
|
|
1162
1953
|
vitops({
|
|
1163
|
-
analytics: { googleAnalytics:
|
|
1164
|
-
consent: { policyUrl:
|
|
1954
|
+
analytics: { googleAnalytics: 'G-XXXXXXXXXX', plausible: 'acme.com' },
|
|
1955
|
+
consent: { policyUrl: '/legal/cookies' },
|
|
1165
1956
|
});
|
|
1166
1957
|
```
|
|
1167
1958
|
|
|
@@ -1219,8 +2010,8 @@ ffmpeg`, `apt install ffmpeg`, `winget install Gyan.FFmpeg`). The command fails
|
|
|
1219
2010
|
replace it with `--anim-range`. `--stagger-range-step` is now a viewport length (`5vh`) to match.
|
|
1220
2011
|
|
|
1221
2012
|
**Fixed: every journey ran on a truncated range.** The generator emitted `animation-range: entry
|
|
1222
|
-
exit`; lightningcss parsed the end of that shorthand as `exit 0%` rather than the spec's `exit
|
|
1223
|
-
100%`, so the bundle shipped `entry exit 0%`. Journeys reach their `100%` keyframe — the hidden
|
|
2013
|
+
exit`; lightningcss parsed the end of that shorthand as `exit 0%` rather than the spec's `exit
|
|
2014
|
+
100%`, so the bundle shipped `entry exit 0%`. Journeys reach their `100%` keyframe — the hidden
|
|
1224
2015
|
`from` state — as the element hits the top of the viewport, which is to say they faded out while
|
|
1225
2016
|
still fully on screen. Journeys now run `entry calc(50% + 10vh) → exit 100%`: they start on the same
|
|
1226
2017
|
midpoint pivot as everything else and keep the full entry → hold → exit arc, so the hold sits in the
|
|
@@ -1233,7 +2024,7 @@ exit`; lightningcss parsed the end of that shorthand as `exit 0%` rather than th
|
|
|
1233
2024
|
so one knob tunes both. If your own config has an empty `slide` base, add the same line.
|
|
1234
2025
|
|
|
1235
2026
|
**Fixed: `reveal-*` on hover was unreachable.** A `hover-reveal-left` element rests at `clip-path:
|
|
1236
|
-
inset(0 100% 0 0)`, and `clip-path` clips **hit-testing** as well as painting — so it had zero
|
|
2027
|
+
inset(0 100% 0 0)`, and `clip-path` clips **hit-testing** as well as painting — so it had zero
|
|
1237
2028
|
hittable area and could never receive the hover that would reveal it. The state variants now match
|
|
1238
2029
|
the element **or its direct parent**, mirroring what `animation.css` already did for the trigger
|
|
1239
2030
|
driver (`:is(.is-active, [data-active]) > .animate-trigger`):
|
|
@@ -1301,7 +2092,7 @@ inset(0 100% 0 0)`, and `clip-path` clips **hit-testing** as well as painting
|
|
|
1301
2092
|
|
|
1302
2093
|
```js
|
|
1303
2094
|
// astro.config.mjs
|
|
1304
|
-
getvitops({ css: { input:
|
|
2095
|
+
getvitops({ css: { input: 'company.json', format: 'css' } });
|
|
1305
2096
|
```
|
|
1306
2097
|
|
|
1307
2098
|
```
|
|
@@ -1400,11 +2191,11 @@ inset(0 100% 0 0)`, and `clip-path` clips **hit-testing** as well as painting
|
|
|
1400
2191
|
getvitops({
|
|
1401
2192
|
fonts: [
|
|
1402
2193
|
{
|
|
1403
|
-
name:
|
|
1404
|
-
provider:
|
|
1405
|
-
cssVariable:
|
|
1406
|
-
weights: [
|
|
1407
|
-
subsets: [
|
|
2194
|
+
name: 'League Spartan',
|
|
2195
|
+
provider: 'fontsource',
|
|
2196
|
+
cssVariable: '--font-league-spartan',
|
|
2197
|
+
weights: ['100 900'],
|
|
2198
|
+
subsets: ['latin'],
|
|
1408
2199
|
preload: true,
|
|
1409
2200
|
},
|
|
1410
2201
|
],
|
|
@@ -1584,7 +2375,7 @@ inset(0 100% 0 0)`, and `clip-path` clips **hit-testing** as well as painting
|
|
|
1584
2375
|
- bf453b0: Fixed: a site on `css.format: 'css'` failed to build because of Tailwind.
|
|
1585
2376
|
|
|
1586
2377
|
`getvitops()` loads `@tailwindcss/vite` lazily, so a project that never sets `css.format:
|
|
1587
|
-
'tailwind'` should never touch it. But the specifier was a literal, which makes it statically
|
|
2378
|
+
'tailwind'` should never touch it. But the specifier was a literal, which makes it statically
|
|
1588
2379
|
analysable — so the bundler **followed** the import regardless of the branch ever running, down
|
|
1589
2380
|
through `@tailwindcss/node` to `@tailwindcss/oxide`'s native `.node` binding, which it cannot parse.
|
|
1590
2381
|
The build died on `[UNLOADABLE_DEPENDENCY] … stream did not contain valid UTF-8`, naming a package
|
|
@@ -1632,14 +2423,14 @@ inset(0 100% 0 0)`, and `clip-path` clips **hit-testing** as well as painting
|
|
|
1632
2423
|
// astro.config.mjs
|
|
1633
2424
|
vitops({
|
|
1634
2425
|
seo: {
|
|
1635
|
-
siteName:
|
|
1636
|
-
titleTemplate:
|
|
1637
|
-
defaultDescription:
|
|
2426
|
+
siteName: 'Acme',
|
|
2427
|
+
titleTemplate: '%s · Acme',
|
|
2428
|
+
defaultDescription: 'We make the thing.',
|
|
1638
2429
|
openGraph: {
|
|
1639
|
-
locale:
|
|
1640
|
-
image: { url:
|
|
2430
|
+
locale: 'en_CA',
|
|
2431
|
+
image: { url: '/og.png', alt: 'Acme', width: 1200, height: 630 },
|
|
1641
2432
|
},
|
|
1642
|
-
twitter: { site:
|
|
2433
|
+
twitter: { site: '@acme' },
|
|
1643
2434
|
},
|
|
1644
2435
|
});
|
|
1645
2436
|
```
|
|
@@ -1683,7 +2474,7 @@ inset(0 100% 0 0)`, and `clip-path` clips **hit-testing** as well as painting
|
|
|
1683
2474
|
|
|
1684
2475
|
```js
|
|
1685
2476
|
getvitops({ sitemap: true });
|
|
1686
|
-
getvitops({ sitemap: { filter: (page) => !page.includes(
|
|
2477
|
+
getvitops({ sitemap: { filter: (page) => !page.includes('/draft/') } });
|
|
1687
2478
|
```
|
|
1688
2479
|
|
|
1689
2480
|
`@astrojs/sitemap` is an **optional peer** — install it yourself (`pnpm add -D @astrojs/sitemap`) and
|
|
@@ -1918,15 +2709,15 @@ inset(0 100% 0 0)`, and `clip-path` clips **hit-testing** as well as painting
|
|
|
1918
2709
|
which `@getvitops/astro` re-exports wholesale. So from `astro.config.mjs`:
|
|
1919
2710
|
|
|
1920
2711
|
```js
|
|
1921
|
-
import { generateIconInclude } from
|
|
1922
|
-
import icon from
|
|
2712
|
+
import { generateIconInclude } from '@getvitops/astro';
|
|
2713
|
+
import icon from 'astro-icon';
|
|
1923
2714
|
|
|
1924
2715
|
integrations: [
|
|
1925
2716
|
icon({
|
|
1926
2717
|
include: generateIconInclude({
|
|
1927
|
-
ui:
|
|
1928
|
-
brand:
|
|
1929
|
-
semantic: [
|
|
2718
|
+
ui: 'fa7-solid',
|
|
2719
|
+
brand: 'simple-icons',
|
|
2720
|
+
semantic: ['menu', 'close', 'search', 'github'],
|
|
1930
2721
|
}),
|
|
1931
2722
|
}),
|
|
1932
2723
|
];
|
|
@@ -1960,7 +2751,7 @@ inset(0 100% 0 0)`, and `clip-path` clips **hit-testing** as well as painting
|
|
|
1960
2751
|
```js
|
|
1961
2752
|
// astro.config.mjs
|
|
1962
2753
|
getvitops({
|
|
1963
|
-
css: { input:
|
|
2754
|
+
css: { input: 'design-system.json', format: 'css', out: 'src/styles' },
|
|
1964
2755
|
editor: true,
|
|
1965
2756
|
});
|
|
1966
2757
|
```
|
|
@@ -2071,7 +2862,7 @@ inset(0 100% 0 0)`, and `clip-path` clips **hit-testing** as well as painting
|
|
|
2071
2862
|
- `.chip-list__chip` and `.chip-list__chip-remove` are **removed**. A tag list is a list of tags, so
|
|
2072
2863
|
its items are now the existing `.tag` / `.tag__remove` — which also means they pick up tag role
|
|
2073
2864
|
variants. **Migration:** replace `<span class="chip-list__chip">x <button
|
|
2074
|
-
class="chip-list__chip-remove">` with `<span class="tag">x <button class="tag__remove">`. Note the
|
|
2865
|
+
class="chip-list__chip-remove">` with `<span class="tag">x <button class="tag__remove">`. Note the
|
|
2075
2866
|
items change appearance: `.tag` is outlined (border + neutral text) where the old chip was filled
|
|
2076
2867
|
with `--color-surface-muted`.
|
|
2077
2868
|
- The `radii.chip` primitive is **removed** from the example config; it was only ever an alias of
|