@getvitops/utils 8.0.1 → 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 +289 -8
- package/dist/{adc-BFrzpMHq.d.mts → adc-BoEoogyo.d.mts} +2 -3
- package/dist/adc-BoEoogyo.d.mts.map +1 -0
- package/dist/ads/index.d.mts +50 -17
- package/dist/ads/index.d.mts.map +1 -1
- package/dist/ads/index.mjs +77 -8
- package/dist/ads/index.mjs.map +1 -1
- package/dist/cloudflare-BiK61Oq4.d.mts.map +1 -1
- package/dist/{cloudflare-CgAsXiqe.mjs → cloudflare-CD8vwrgj.mjs} +2 -24
- package/dist/cloudflare-CD8vwrgj.mjs.map +1 -0
- package/dist/color/index.d.mts +34 -35
- package/dist/color/index.d.mts.map +1 -1
- package/dist/color/index.mjs +3 -1
- package/dist/color/index.mjs.map +1 -1
- package/dist/crm/index.d.mts +288 -0
- package/dist/crm/index.d.mts.map +1 -0
- package/dist/crm/index.mjs +731 -0
- package/dist/crm/index.mjs.map +1 -0
- package/dist/email/index.d.mts +216 -0
- package/dist/email/index.d.mts.map +1 -0
- package/dist/email/index.mjs +331 -0
- package/dist/email/index.mjs.map +1 -0
- package/dist/favicon.d.mts +8 -9
- package/dist/favicon.d.mts.map +1 -1
- package/dist/git/index.d.mts +65 -0
- package/dist/git/index.d.mts.map +1 -0
- package/dist/git/index.mjs +107 -0
- package/dist/git/index.mjs.map +1 -0
- package/dist/hosting/index.d.mts +184 -0
- package/dist/hosting/index.d.mts.map +1 -0
- package/dist/hosting/index.mjs +553 -0
- package/dist/hosting/index.mjs.map +1 -0
- package/dist/identity-BoRddTx3.mjs +35 -0
- package/dist/identity-BoRddTx3.mjs.map +1 -0
- package/dist/index.d.mts +24 -25
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +5 -5
- package/dist/index.mjs.map +1 -1
- package/dist/indexing/index.d.mts +23 -24
- package/dist/indexing/index.d.mts.map +1 -1
- package/dist/media/index.d.mts +24 -24
- package/dist/media/index.d.mts.map +1 -1
- package/dist/media/index.mjs +3 -3
- package/dist/media/index.mjs.map +1 -1
- package/dist/notify/index.d.mts +26 -15
- package/dist/notify/index.d.mts.map +1 -1
- package/dist/notify/index.mjs +50 -20
- package/dist/notify/index.mjs.map +1 -1
- package/dist/onboarding/index.d.mts +10 -141
- package/dist/onboarding/index.d.mts.map +1 -1
- package/dist/onboarding/index.mjs +2 -144
- package/dist/onboarding/index.mjs.map +1 -1
- package/dist/plan-BLKikoOG.mjs +150 -0
- package/dist/plan-BLKikoOG.mjs.map +1 -0
- package/dist/plan-Hq8Pb0Uw.d.mts +133 -0
- package/dist/plan-Hq8Pb0Uw.d.mts.map +1 -0
- package/dist/plan-format-A18SvJ7C.mjs +24 -0
- package/dist/plan-format-A18SvJ7C.mjs.map +1 -0
- package/dist/reviews/index.d.mts +12 -13
- package/dist/reviews/index.d.mts.map +1 -1
- package/dist/schema/index.d.mts +7 -7
- package/dist/schema/index.d.mts.map +1 -1
- package/dist/security/index.d.mts +2 -2
- package/dist/security/index.d.mts.map +1 -1
- package/dist/{token-Bz_-igw-.d.mts → token-DmmoQ7um.d.mts} +6 -3
- package/dist/token-DmmoQ7um.d.mts.map +1 -0
- package/dist/tracking/index.d.mts +43 -17
- package/dist/tracking/index.d.mts.map +1 -1
- package/dist/tracking/index.mjs +2 -1
- package/dist/tracking/index.mjs.map +1 -1
- package/dist/{types-CHHjzRrv.d.mts → types-7ELci8RQ.d.mts} +4 -2
- package/dist/types-7ELci8RQ.d.mts.map +1 -0
- package/package.json +18 -4
- package/dist/adc-BFrzpMHq.d.mts.map +0 -1
- package/dist/cloudflare-CgAsXiqe.mjs.map +0 -1
- package/dist/token-Bz_-igw-.d.mts.map +0 -1
- package/dist/types-CHHjzRrv.d.mts.map +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,281 @@
|
|
|
1
1
|
# @getvitops/utils
|
|
2
2
|
|
|
3
|
+
## 8.2.0-alpha.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 0574388: Add `site.crm` — push form submissions into HubSpot, Zoho Bigin, or Jobber.
|
|
8
|
+
|
|
9
|
+
Client sites need form submissions to land in the client's CRM. The options available before this
|
|
10
|
+
— a HubSpot/Bigin/Jobber iframe or JS embed — fail on three counts: the markup can't be
|
|
11
|
+
white-labelled against this design system, nothing can operate on the submission before it leaves
|
|
12
|
+
the page, and the submission never touched `createConversionRoute()`, so `_ac` ad-click attribution
|
|
13
|
+
was lost and no conversion notification was planned.
|
|
14
|
+
|
|
15
|
+
This adds one server-side path for all three CRMs, hung off the conversion route that already
|
|
16
|
+
exists. `createConversionRoute()` gets a new `crm` option (`{ config, credentials, fetch?,
|
|
17
|
+
timeoutMs? }`); credentials arrive as a function of `locals`, exactly like the existing `binding`
|
|
18
|
+
option, and are never config. A failed CRM write never fails the request and is folded into the
|
|
19
|
+
notification email's new "Delivery" section, so a broken CRM token is never silent.
|
|
20
|
+
|
|
21
|
+
New `@getvitops/utils/crm` subpath: `deliverToCrm()`, `mapSubmission()`, `deriveLastName()` (Bigin
|
|
22
|
+
requires a last name; this derives one from an explicit field, a full name, or an email address,
|
|
23
|
+
refusing rather than fabricating one), `submitToHubSpot()`, `submitToBigin()`, `submitToJobber()`.
|
|
24
|
+
`identifySubmitter()` was promoted out of `notify`'s private name-guessing cascade into
|
|
25
|
+
`@getvitops/utils/tracking`, since the CRM mapper needed the whole identity (first/last name,
|
|
26
|
+
email, phone, company), not just a display name.
|
|
27
|
+
|
|
28
|
+
Jobber is GraphQL, not REST, and answers `200 OK` for failures — a rejected `clientCreate` returns
|
|
29
|
+
`userErrors[]`, a malformed request returns top-level `errors[]`. `submitToJobber()` classifies on
|
|
30
|
+
the response body, not just the status. Its refresh tokens also rotate by default: **Refresh Token
|
|
31
|
+
Rotation must be turned off** in the app's Developer Center settings, since a Worker reading a
|
|
32
|
+
static `VITOPS_JOBBER_REFRESH_TOKEN` cannot persist a rotated one — a rotated token is detected at
|
|
33
|
+
exchange time and reported with the fix, rather than surfacing an hour later as an opaque 401.
|
|
34
|
+
Attribution and unmapped extra fields render into a best-effort note by default (works on every
|
|
35
|
+
Jobber plan); a resubmitting visitor creates a second Jobber client unless `crm.jobber.dedupe: true`
|
|
36
|
+
is set once its lookup query has been verified against a live account (defaults `false` pending
|
|
37
|
+
that verification — see `jobber.ts`'s docblock).
|
|
38
|
+
|
|
39
|
+
New `site.crm` config block (`provider`, `hubspot`/`bigin`/`jobber`, `fieldMap`, `trackingFields`)
|
|
40
|
+
and two CLI commands: `vitops crm check` (fetches the live HubSpot property list or Bigin field
|
|
41
|
+
list and reports every mapped name that doesn't exist there — HubSpot rejects an unknown field
|
|
42
|
+
outright, Bigin silently drops it; Jobber has nothing live to check yet, since its mapped values
|
|
43
|
+
currently render as note text) and `vitops crm setup` (creates missing HubSpot properties; Bigin
|
|
44
|
+
and Jobber have no such API, so both always print a manual reminder).
|
|
45
|
+
|
|
46
|
+
The generated privacy policy now names HubSpot, Zoho Bigin, or Jobber when `site.crm` configures
|
|
47
|
+
one. Bigin gets the same cross-border-transfer derivation Matomo Cloud already gets: a
|
|
48
|
+
`dataCenter: 'ca'` asserts no transfer outside Canada, any other data centre does. Jobber's
|
|
49
|
+
operator (Octopusapp Inc. d/b/a Jobber) is Canadian, but its own privacy policy names the United
|
|
50
|
+
States as a processing location too, so the policy always asserts a cross-border transfer for
|
|
51
|
+
Jobber regardless of config — a Canadian operator is not read as Canadian-only storage.
|
|
52
|
+
|
|
53
|
+
Credentials: `VITOPS_HUBSPOT_ACCESS_TOKEN` for HubSpot; `VITOPS_ZOHO_CLIENT_ID` /
|
|
54
|
+
`VITOPS_ZOHO_CLIENT_SECRET` / `VITOPS_ZOHO_REFRESH_TOKEN` for Bigin; `VITOPS_JOBBER_CLIENT_ID` /
|
|
55
|
+
`VITOPS_JOBBER_CLIENT_SECRET` / `VITOPS_JOBBER_REFRESH_TOKEN` for Jobber. Never config.
|
|
56
|
+
|
|
57
|
+
No consumer-facing change in `@getvitops/astro` beyond the new `crm` route option and a build
|
|
58
|
+
warning when `site.crm` is configured with no route answering `TRACKING_ENDPOINT` — CSS/Bricks/
|
|
59
|
+
tailwind outputs are unaffected.
|
|
60
|
+
|
|
61
|
+
- c3c1bcb: **New: the design-system editor, mounted inside the EmDash admin.** `vitopsEmdash({
|
|
62
|
+
designSystemEditor: { config } })` (`config` is your `site.json`, e.g. `import config from
|
|
63
|
+
'./site.json' with { type: 'json' }`) adds a `/design-system` admin page mounting
|
|
64
|
+
`<wc-theme-editor>` — the same live token editor `@getvitops/vite`'s dev server has always
|
|
65
|
+
offered, now usable by the client directly, no dev server required. An edit is visible
|
|
66
|
+
site-wide immediately (a runtime `:root` override, served via a new plugin API route and
|
|
67
|
+
`page:fragments`) and — once `site.deployment.repo`/`branch` and a `VITOPS_GITHUB_TOKEN`
|
|
68
|
+
secret are set (see the scaffold's `SETUP.html`) — auto-commits to your `dev` branch, so it
|
|
69
|
+
reaches the git-tracked `site.json` through your repo's normal review path rather than
|
|
70
|
+
becoming a second, unreviewed source of truth. Until that's configured, the live half still
|
|
71
|
+
works; only the auto-commit half is skipped (and says so in the admin page). The save route
|
|
72
|
+
runs the exact same accessibility (APCA contrast) gate `generate()` enforces at build time,
|
|
73
|
+
synchronously, before persisting — nothing non-compliant ever goes live even momentarily.
|
|
74
|
+
|
|
75
|
+
Requires `@astrojs/react` + `react` + `react-dom` (already scaffold dependencies, for
|
|
76
|
+
EmDash's own admin) and `@getvitops/core` (added as a new scaffold dependency this release).
|
|
77
|
+
Declares two new plugin capabilities when enabled: `network:request` (for the GitHub
|
|
78
|
+
auto-commit) and `hooks.page-fragments:register` (for the live override) — reviewed once at
|
|
79
|
+
plugin-install time.
|
|
80
|
+
|
|
81
|
+
**Breaking for `@getvitops/emdash`**: `vitopsEmdash()`'s `hooks.page-fragments:register`
|
|
82
|
+
capability is now also granted whenever `designSystemEditor` is set, independent of the
|
|
83
|
+
existing `scripts: 'fragments'` option (which governs something unrelated — web-component
|
|
84
|
+
script delivery). No change if you don't use `designSystemEditor`.
|
|
85
|
+
|
|
86
|
+
Also new, supporting the above:
|
|
87
|
+
- `@getvitops/generator`: `validateColorContract()` and `buildReverseIndex()` (both
|
|
88
|
+
`/tokens` subpath, Worker-safe) — the monotonicity/contrast gate and the
|
|
89
|
+
`design-manifest.json` reverse-index, each extracted from `generate()`'s `build()` into
|
|
90
|
+
standalone, non-throwing functions. `patchToOverrideCss()` turns a design-system patch
|
|
91
|
+
into the `:root` override `<style>` block. `designSystemPath`/`getAt`/`setAt`/
|
|
92
|
+
`mergePatch`/`formatConfig` moved from `@getvitops/vite` into `@getvitops/generator/config`
|
|
93
|
+
(re-exported from `@getvitops/vite` unchanged) — one "patch a config file's design-system
|
|
94
|
+
subtree" implementation shared by the dev endpoint and the new plugin route. New
|
|
95
|
+
`site.deployment.repo`/`branch` config fields.
|
|
96
|
+
- `@getvitops/utils`: new `/git` subpath — `commitFile()`, a Worker-safe (fetch-only)
|
|
97
|
+
wrapper over GitHub's Contents API.
|
|
98
|
+
- `@getvitops/core`: `<wc-theme-editor>` gains a `save-endpoint` attribute (default
|
|
99
|
+
unchanged — `/__vitops/design-system`) so the same element can target the new plugin
|
|
100
|
+
route instead of the dev-only Vite endpoint.
|
|
101
|
+
|
|
102
|
+
- 0574388: Add `vitops email setup|check` — connect `site.notifications.email.from` to whether its domain is
|
|
103
|
+
actually onboarded onto an outbound email provider.
|
|
104
|
+
|
|
105
|
+
Before this, nothing checked that. `EMAIL.send()` throws `E_SENDER_NOT_VERIFIED` at runtime with no
|
|
106
|
+
build or deploy-time warning — the failure mode is silent and production-only.
|
|
107
|
+
|
|
108
|
+
`vitops email setup [domain]` onboards a sending domain (currently Cloudflare Email Sending only)
|
|
109
|
+
and polls DNS until its SPF/DKIM/DMARC records resolve or a timeout is reached; `vitops email
|
|
110
|
+
check` is the non-blocking deploy guard both `@getvitops/create` templates' `deploy`/`deploy:dev`
|
|
111
|
+
scripts now run first (`--strict` makes it a real gate; opt out per-site with
|
|
112
|
+
`site.notifications.email.verifySender: false`, or globally with `--skip` /
|
|
113
|
+
`VITOPS_SKIP_EMAIL_CHECK`).
|
|
114
|
+
|
|
115
|
+
Two things worth knowing before running it:
|
|
116
|
+
- **The onboarded entity is an exact name, and it is usually a subdomain**
|
|
117
|
+
(`send.yourdomain.com`), never the apex your `from` address names. When the exact domain isn't
|
|
118
|
+
onboarded but a sibling in the same zone already is, the command reports that as the fix — send
|
|
119
|
+
from the onboarded subdomain — rather than silently trying to onboard the apex, which rewrites
|
|
120
|
+
`_dmarc.<domain>` and can tighten DMARC policy on every other mail your domain sends.
|
|
121
|
+
- Candidates come from `site.notifications.email.from` (or its derived `noreply@<domains
|
|
122
|
+
.canonical>` default) first; a best-effort scan of `--src` only supplements it, and never guesses
|
|
123
|
+
at a computed `from` value.
|
|
124
|
+
|
|
125
|
+
New `@getvitops/utils/email` subpath and `site.notifications.email.verifySender` config field.
|
|
126
|
+
Needs `CLOUDFLARE_API_TOKEN` with an Email Sending scope in addition to the `Zone:Read` other
|
|
127
|
+
commands already use.
|
|
128
|
+
|
|
129
|
+
- 0574388: Fire a Google Ads conversion on a tel:/mailto: click, and fix a rate-limit bug on the conversion
|
|
130
|
+
route that could silently drop one of the two.
|
|
131
|
+
|
|
132
|
+
`site.ads.google.conversionLabel` used to be accepted by the config schema and typed all the way
|
|
133
|
+
through to the rendered tag, but nothing actually read it — no `gtag('event', 'conversion', …)`
|
|
134
|
+
was ever emitted. When set, the click tracker `<Tracking />` inlines now fires the conversion at
|
|
135
|
+
the moment a visitor taps a `tel:` or `mailto:` link, using the `AW-…/label` target Google Ads
|
|
136
|
+
expects. A `conversionLabel` that can't safely land inside the inline bootstrap script (the same
|
|
137
|
+
check `pixelId`/`accountId` already get) is dropped with a warning rather than firing something
|
|
138
|
+
malformed — the ad tag itself still loads.
|
|
139
|
+
|
|
140
|
+
Also: `createConversionRoute()`'s rate limiter previously keyed on IP address alone, so a call
|
|
141
|
+
beacon and an email-click beacon from the same visitor, seconds apart, competed for one window —
|
|
142
|
+
the second could 429 and vanish with no error anywhere. It's now keyed on IP _and_ event type, so
|
|
143
|
+
distinct conversion types never starve each other; repeats of the same type are still throttled
|
|
144
|
+
exactly as before.
|
|
145
|
+
|
|
146
|
+
No config changes — a site with `site.ads.google.conversionLabel` already set picks this up
|
|
147
|
+
automatically on the next build.
|
|
148
|
+
|
|
149
|
+
- 0574388: Track `mailto:` link clicks as conversions, alongside the existing `tel:` link tracking.
|
|
150
|
+
|
|
151
|
+
Before this, the capture script only beaconed `tel:` clicks; a `mailto:` link on the same page was
|
|
152
|
+
invisible to attribution — a visitor who emailed the business instead of calling generated no
|
|
153
|
+
conversion event at all. `<Tracking />`'s inlined script now listens for both, sending `{ event:
|
|
154
|
+
'email', email }` via `navigator.sendBeacon` the same way it already sends `{ event: 'call', phone
|
|
155
|
+
}`.
|
|
156
|
+
|
|
157
|
+
`ConversionEvent['type']` gains an `'email'` member alongside `'form'`/`'call'`, with a new
|
|
158
|
+
`email` field. `createConversionRoute()` handles it exactly like a call: no honeypot, Turnstile or
|
|
159
|
+
CRM checks (there are no form fields to validate or map), a 204 response, and a notification email
|
|
160
|
+
subject reading `[Email Click] Visitor emailing ...`.
|
|
161
|
+
|
|
162
|
+
No config changes and no new options — a site with `<Tracking />` already mounted picks this up
|
|
163
|
+
automatically the next time it rebuilds.
|
|
164
|
+
|
|
165
|
+
- dde3df3: **`site.ads.google.conversionLabel` now accepts a per-action map** —
|
|
166
|
+
`{ call?, email?, form? }` — instead of one bare string shared by every
|
|
167
|
+
interaction. A bare string still works (back-compat, treated as `call`), but a
|
|
168
|
+
call and an email click used to report as the **same** Google Ads conversion
|
|
169
|
+
action, indistinguishable in the account; each action now gets its own
|
|
170
|
+
`send_to` target.
|
|
171
|
+
|
|
172
|
+
**New: `vitopsFormConversion(formEl)`**, exposed as `window.vitopsFormConversion`
|
|
173
|
+
by `<Tracking />`'s capture script — call it from your own fetch-based form
|
|
174
|
+
submit handler once the POST to `createConversionRoute()` succeeds, to fire the
|
|
175
|
+
`form` Google Ads conversion action client-side (mirroring the existing
|
|
176
|
+
tel:/mailto: click handler, which now fires `call`/`email` instead of sharing
|
|
177
|
+
one label). Idempotent per form element. There's deliberately no server-side
|
|
178
|
+
equivalent for a no-JS `<form method="post">` submission — that's a full-page
|
|
179
|
+
redirect with no script to fire from, and a real server-side Google Ads
|
|
180
|
+
conversion needs the Ads API's click-conversion upload (developer token + MCC
|
|
181
|
+
access), tracked separately in the vNext backlog.
|
|
182
|
+
|
|
183
|
+
**Fixed: the conversion route's rate limiter no longer trusts the client's
|
|
184
|
+
raw `event` field.** It used to key on whatever string a client sent
|
|
185
|
+
(`` `${ip}:${body.event}` ``), so varying that value on every request let a
|
|
186
|
+
form submission dodge the per-IP rate limit entirely — only `'call'`/`'email'`
|
|
187
|
+
were ever recognized, and anything else got its own fresh bucket. The key now
|
|
188
|
+
uses the server's own classification (`call` | `email` | `form`), closing that
|
|
189
|
+
bypass without changing the existing call-vs-email-don't-compete behavior.
|
|
190
|
+
|
|
191
|
+
**New: `site.notifications.actionUrl`** — an optional action-link template
|
|
192
|
+
added to the conversion-notification email as an "Actions" section.
|
|
193
|
+
`{clickId}` is replaced with the ad click ID captured on `_ac` (gclid, fbclid,
|
|
194
|
+
etc.); the section is omitted when the event has no click ID to interpolate,
|
|
195
|
+
rather than emailing a broken link.
|
|
196
|
+
|
|
197
|
+
### Patch Changes
|
|
198
|
+
|
|
199
|
+
- 0574388: Bump `emdash`/`@emdash-cms/cloudflare` peers to `>=1.0.0 <2` and `vite-plus` to `^1.0.0`, following
|
|
200
|
+
both projects' GA releases.
|
|
201
|
+
|
|
202
|
+
**Breaking for `@getvitops/emdash` consumers on EmDash 0.x**: the peer range no longer admits
|
|
203
|
+
`emdash@0.31.x`/`@emdash-cms/cloudflare@0.31.x`. Upgrade `emdash` and `@emdash-cms/cloudflare` to
|
|
204
|
+
`^1.0.1` first — nothing in `@getvitops/emdash`'s own plugin surface (`admin.portableTextBlocks`,
|
|
205
|
+
the `hooks.page-fragments:register` capability, `definePlugin()`) changed against EmDash 1.0, so no
|
|
206
|
+
code here needed to change, only the declared range.
|
|
207
|
+
|
|
208
|
+
The `emdash` scaffold template (`vp create @getvitops:emdash`) picks up several fixes alongside
|
|
209
|
+
the version bump:
|
|
210
|
+
- `tailwindcss`/`@tailwindcss/vite` are now real dependencies — the template selected
|
|
211
|
+
`css.format: 'tailwind'` without installing either, which threw on build.
|
|
212
|
+
- `site.json` now sets `site.icons.sprite: true` plus the full `semantic` list the
|
|
213
|
+
`vitops.actionLink` block's icon picker offers — previously its sprite icons pointed at a file
|
|
214
|
+
the build never generated.
|
|
215
|
+
- `astro.config.mjs` sets `migrations: { runtime: 'check', dev: 'auto' }`, and
|
|
216
|
+
`site-deploy-emdash.yml` runs EmDash 1.0's documented `build → status → apply → deploy → check`
|
|
217
|
+
migration sequence explicitly, rather than relying solely on first-request auto-migration. This
|
|
218
|
+
needs a new GitHub Environment variable, `EMDASH_TARGET_FINGERPRINT` (one per environment),
|
|
219
|
+
which only a human can set — run `emdash migrate --status` against the real deployed database,
|
|
220
|
+
review the printed identity, then `gh variable set EMDASH_TARGET_FINGERPRINT --env <env> --body
|
|
221
|
+
<value>`. Deploys fail clearly on the migration-apply step until it's set.
|
|
222
|
+
- `lint.options.typeAware`/`typeCheck` in the template's own `vite.config.ts` are commented out,
|
|
223
|
+
not deleted: vite-plus 1.0 only honours that option in a project's actual root config, and hard
|
|
224
|
+
errors on it in a nested one (which this file is, sitting inside the `design-system` monorepo).
|
|
225
|
+
Uncomment it once scaffolded into its own repo.
|
|
226
|
+
|
|
227
|
+
No config schema changes. `@getvitops/generator` also switches from the `zod/mini` subpath import
|
|
228
|
+
to the standalone `@zod/mini` package (zod 4.5+; same API) and fixes `validateConfig()` silently
|
|
229
|
+
losing the offending key name on an `unrecognized_keys` error (e.g. a typo'd `site.ads` platform
|
|
230
|
+
now names the bad key instead of reporting a bare "Invalid input").
|
|
231
|
+
|
|
232
|
+
Also fixed: `vp check`'s `tsconfig-error` for `packages/create/templates/emdash/tsconfig.json`
|
|
233
|
+
(`extends: "astro/tsconfigs/strict"`, unresolvable from inside this monorepo since the template
|
|
234
|
+
has no `node_modules` of its own — a pre-existing, structural gap, newly surfaced as a hard error
|
|
235
|
+
by oxlint 1.85's deeper config discovery, and blocking on every commit since this repo's pre-commit
|
|
236
|
+
hook runs `vp check --fix`). Fixed with a new `scripts/link-template-deps.mjs`, wired as the root
|
|
237
|
+
`postinstall` script: symlinks `astro`/`emdash`/`@emdash-cms/cloudflare` into the template's own
|
|
238
|
+
`node_modules/` (resolved fresh from sibling packages that already depend on them for real, not a
|
|
239
|
+
hardcoded `.pnpm` hash) and writes a minimal `astro:content` ambient shim at `.astro/types.d.ts`
|
|
240
|
+
(the same path `astro sync` would generate for real). Both are gitignored and only ever exist
|
|
241
|
+
inside this monorepo's copy of the template — a real scaffold installs its own real copies instead.
|
|
242
|
+
Considered and rejected first: inlining astro's strict preset (drift risk against astro's own
|
|
243
|
+
evolving default) and making the template a real pnpm workspace member (would change how its
|
|
244
|
+
`@getvitops/*: "latest"` deps resolve locally, which `templates.test.ts` exists to protect).
|
|
245
|
+
|
|
246
|
+
**Known gap, not fixed here**: `packages/astro/src/components/container.test.ts` (the
|
|
247
|
+
`AstroContainer`-based render tests) fails to even load under vite-plus 1.0's Vite 8 — direct
|
|
248
|
+
`.astro` imports in a Vitest file need the Astro Vite plugin registered, which nothing in this
|
|
249
|
+
repo's test config actually does (Astro's own docs call for `getViteConfig()`, which doesn't
|
|
250
|
+
compose with vite-plus's own `defineConfig`). A type-level shim
|
|
251
|
+
(`packages/astro/src/components/astro-modules.d.ts`) fixes `vp check`'s type-aware pass, but the
|
|
252
|
+
runtime `vp test` failure for this one file is still open.
|
|
253
|
+
|
|
254
|
+
## 8.1.0
|
|
255
|
+
|
|
256
|
+
### Minor Changes
|
|
257
|
+
|
|
258
|
+
- **Added — `vitops init --site` and `vitops setup`, the scripted client-onboarding path.**
|
|
259
|
+
|
|
260
|
+
`vitops init --site [--name] [--url]` scaffolds a starter `site.json` (the umbrella `Config`,
|
|
261
|
+
embedding a starter design system at `designSystem.themes.default`) instead of a bare
|
|
262
|
+
`design-system.json` — exported as `defaultSiteConfig()` from `@getvitops/generator`, alongside a
|
|
263
|
+
new `CONFIG_SCHEMA_LOCAL_PATH` and an optional `$schema` field on `ConfigSchema` so a scaffolded
|
|
264
|
+
config gets editor autocomplete the same way `design-system.json` already does.
|
|
265
|
+
|
|
266
|
+
`vitops setup <repo|hosting|domain|search|ads|all>` (new `@getvitops/utils/hosting` subpath: a
|
|
267
|
+
pure planner + `wrangler`/`gh`-shelling executors, following the same idempotent
|
|
268
|
+
plan/observe/apply split as `onboarding`/`ads`) automates GitHub repo + branch policy and
|
|
269
|
+
Cloudflare dev D1/R2 + repo secrets, and plans/checks the workers.dev-vs-custom-domain
|
|
270
|
+
consistency without ever applying the cutover itself — see AGENTS.md's new "Client onboarding"
|
|
271
|
+
section for the full design rationale. `search`/`ads` forward to the existing `search setup`/
|
|
272
|
+
`ads setup` subcommands rather than reimplementing them.
|
|
273
|
+
|
|
274
|
+
No consumer-facing change in `@getvitops/vite` or `@getvitops/astro` in this release — bumped
|
|
275
|
+
only because they are in the `fixed` release group with `generator`/`utils`/`cli`.
|
|
276
|
+
`@getvitops/core` does have a real fix in this release; see the separate `banner-hidden-fix`
|
|
277
|
+
changeset.
|
|
278
|
+
|
|
3
279
|
## 8.0.1
|
|
4
280
|
|
|
5
281
|
## 8.0.0
|
|
@@ -11,7 +287,6 @@
|
|
|
11
287
|
|
|
12
288
|
**Breaking (`css`/`bricks` and `tailwind` formats — `.lightbox-dialog`'s markup
|
|
13
289
|
contract changed):**
|
|
14
|
-
|
|
15
290
|
- `<Gallery />` now emits ONE `<dialog class="lightbox-dialog">` per gallery,
|
|
16
291
|
wrapping a `<Carousel />`, instead of one dialog per image. A `.gallery__trigger`
|
|
17
292
|
is now a plain `<a href="#slide-id">`, not a `command="show-modal"` button.
|
|
@@ -73,7 +348,6 @@
|
|
|
73
348
|
### Minor Changes
|
|
74
349
|
|
|
75
350
|
- Add Google Business Profile reviews: fetch, cache, render.
|
|
76
|
-
|
|
77
351
|
- **`vitops reviews`** (new CLI command) fetches a client's GBP reviews (legacy v4 API — the only
|
|
78
352
|
one that has them) and writes them to `.vitops/reviews.json`, meant to be committed so a build
|
|
79
353
|
never needs a live Google credential. `--dry` prints the plan; `--check` diffs against the cache
|
|
@@ -108,7 +382,6 @@
|
|
|
108
382
|
`--site` for now.)
|
|
109
383
|
|
|
110
384
|
**Fixed:**
|
|
111
|
-
|
|
112
385
|
- `@getvitops/core`'s 12 runtime deps (`lit`, both `@oddbird` polyfills, both
|
|
113
386
|
`@webcomponents` packages, `colorjs.io`, etc.) were listed as `dependencies` despite
|
|
114
387
|
being bundled into `dist/elements.js`/`dist/polyfills.js`/`dist/deferred.js` — a
|
|
@@ -521,20 +794,28 @@ here changed, and no code that imports `@getvitops/utils` needs editing.
|
|
|
521
794
|
{
|
|
522
795
|
"designSystem": {
|
|
523
796
|
"themes": {
|
|
524
|
-
"default": {
|
|
797
|
+
"default": {
|
|
798
|
+
/* … */
|
|
799
|
+
},
|
|
525
800
|
},
|
|
526
801
|
},
|
|
527
802
|
"organization": {
|
|
528
803
|
"name": "Acme",
|
|
529
804
|
"contact": "hq",
|
|
530
805
|
"locations": {
|
|
531
|
-
"hq": {
|
|
806
|
+
"hq": {
|
|
807
|
+
/* … */
|
|
808
|
+
},
|
|
532
809
|
},
|
|
533
810
|
},
|
|
534
811
|
"site": {
|
|
535
812
|
"defaultLocale": "en",
|
|
536
|
-
"domains": {
|
|
537
|
-
|
|
813
|
+
"domains": {
|
|
814
|
+
/* … */
|
|
815
|
+
},
|
|
816
|
+
"analytics": {
|
|
817
|
+
/* … */
|
|
818
|
+
},
|
|
538
819
|
},
|
|
539
820
|
}
|
|
540
821
|
```
|
|
@@ -603,7 +884,7 @@ here changed, and no code that imports `@getvitops/utils` needs editing.
|
|
|
603
884
|
Also new, and worth wiring at the same time — **`gitLastmod()` for real sitemap dates**:
|
|
604
885
|
|
|
605
886
|
```js
|
|
606
|
-
import vitops, { gitLastmod } from
|
|
887
|
+
import vitops, { gitLastmod } from '@getvitops/astro';
|
|
607
888
|
vitops({ sitemap: { serialize: await gitLastmod() } });
|
|
608
889
|
```
|
|
609
890
|
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import { i as GoogleOAuth } from "./token-
|
|
2
|
-
|
|
1
|
+
import { i as GoogleOAuth } from "./token-DmmoQ7um.mjs";
|
|
3
2
|
//#region src/google/adc.d.ts
|
|
4
3
|
/** A parsed ADC user credential, or `undefined` if `raw` is not one. */
|
|
5
4
|
declare function parseAdcUser(raw: string): GoogleOAuth | undefined;
|
|
@@ -22,4 +21,4 @@ declare function adcCredentialsPath(opts: {
|
|
|
22
21
|
}): string | undefined;
|
|
23
22
|
//#endregion
|
|
24
23
|
export { adcQuotaProject as n, parseAdcUser as r, adcCredentialsPath as t };
|
|
25
|
-
//# sourceMappingURL=adc-
|
|
24
|
+
//# sourceMappingURL=adc-BoEoogyo.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"adc-BoEoogyo.d.mts","names":[],"sources":["../src/google/adc.ts"],"mappings":";;;iBAkBgB,aAAa,cAAc;;iBAwB3B,gBAAgB;;;;;;;;;;iBAiBhB,mBAAmB;EACjC,KAAK;EACL;EACA;EACA,UAAU"}
|
package/dist/ads/index.d.mts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { n as findZoneId, r as listApexTxt, t as createApexTxt } from "../cloudflare-BiK61Oq4.mjs";
|
|
2
|
-
|
|
3
2
|
//#region src/ads/types.d.ts
|
|
4
3
|
/**
|
|
5
4
|
* The ads module's own option surface.
|
|
@@ -25,6 +24,34 @@ import { n as findZoneId, r as listApexTxt, t as createApexTxt } from "../cloudf
|
|
|
25
24
|
type AdProvider = 'google' | 'meta' | 'linkedin' | 'reddit' | 'tiktok' | 'microsoft' | 'pinterest' | 'snapchat';
|
|
26
25
|
/** Which consent category gates a provider's tag. */
|
|
27
26
|
type AdConsentCategory = 'marketing' | 'analytics';
|
|
27
|
+
/**
|
|
28
|
+
* The three interactions Google Ads can report a distinct conversion action
|
|
29
|
+
* for. `form` is fired client-side via `vitopsFormConversion()`
|
|
30
|
+
* (`@getvitops/astro`'s tracking script), the other two from the tel:/mailto:
|
|
31
|
+
* click handler.
|
|
32
|
+
*/
|
|
33
|
+
type ConversionAction = 'call' | 'email' | 'form';
|
|
34
|
+
/**
|
|
35
|
+
* One label per action this site wants Google Ads to tell apart.
|
|
36
|
+
*
|
|
37
|
+
* Written out field by field, `?: string | undefined` — not
|
|
38
|
+
* `Partial<Record<ConversionAction, string>>` — matching this file's own
|
|
39
|
+
* convention above: `exactOptionalPropertyTypes` treats `{ email?: string }`
|
|
40
|
+
* (present-or-absent) and `{ email?: string | undefined }` (present,
|
|
41
|
+
* absent, or explicitly `undefined`) as different types, and a parsed JSON
|
|
42
|
+
* config's optional field is the latter. `Partial<Record<...>>` produces the
|
|
43
|
+
* former, which rejected the zod-inferred shape config.ts actually produces.
|
|
44
|
+
*/
|
|
45
|
+
interface ConversionLabelMap {
|
|
46
|
+
call?: string | undefined;
|
|
47
|
+
email?: string | undefined;
|
|
48
|
+
form?: string | undefined;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* A bare string is back-compat shorthand for `{ call: label }` — the only
|
|
52
|
+
* action that existed before this was a map. Absent input normalises to `{}`.
|
|
53
|
+
*/
|
|
54
|
+
export declare function normalizeConversionLabels(label: string | ConversionLabelMap | undefined): ConversionLabelMap;
|
|
28
55
|
/** One ad property to link, as the config declares it. */
|
|
29
56
|
interface AdPropertySetup {
|
|
30
57
|
provider: AdProvider;
|
|
@@ -35,8 +62,14 @@ interface AdPropertySetup {
|
|
|
35
62
|
accountId?: string | undefined;
|
|
36
63
|
/** The tag/pixel id, when the platform issues one distinct from the account. */
|
|
37
64
|
pixelId?: string | undefined;
|
|
38
|
-
/**
|
|
39
|
-
|
|
65
|
+
/**
|
|
66
|
+
* Google Ads only: the conversion label(s) paired with the conversion id —
|
|
67
|
+
* either one bare label (back-compat, treated as the `call` action) or a map
|
|
68
|
+
* of action → label, so a call, an email click and a form submission each
|
|
69
|
+
* fire their own Google Ads conversion action instead of sharing one. See
|
|
70
|
+
* `gtagBootstrap`/`normalizeConversionLabels` below.
|
|
71
|
+
*/
|
|
72
|
+
conversionLabel?: string | ConversionLabelMap | undefined;
|
|
40
73
|
/** The host to verify, bare (`acme.ca`). The CLI defaults it from the canonical domain. */
|
|
41
74
|
domain?: string | undefined;
|
|
42
75
|
/**
|
|
@@ -113,18 +146,18 @@ interface AdsPlan {
|
|
|
113
146
|
* since this command never edits or deletes DNS), or no token is configured yet
|
|
114
147
|
* (blocked, naming the field).
|
|
115
148
|
*/
|
|
116
|
-
declare function planProperty(setup: AdPropertySetup, state: AdDomainState): AdPropertyPlan;
|
|
149
|
+
export declare function planProperty(setup: AdPropertySetup, state: AdDomainState): AdPropertyPlan;
|
|
117
150
|
/** Plan every configured property. */
|
|
118
|
-
declare function plan(config: AdsConfig, states: Map<string, AdDomainState>): AdsPlan;
|
|
151
|
+
export declare function plan(config: AdsConfig, states: Map<string, AdDomainState>): AdsPlan;
|
|
119
152
|
/** Any step that isn't a skip — i.e. the live state or the config has drifted. */
|
|
120
|
-
declare function hasDrift(p: AdsPlan): boolean;
|
|
153
|
+
export declare function hasDrift(p: AdsPlan): boolean;
|
|
121
154
|
/** Every field a run still needs, per provider — what the CLI asks for. */
|
|
122
|
-
declare function missingFields(p: AdsPlan): {
|
|
155
|
+
export declare function missingFields(p: AdsPlan): {
|
|
123
156
|
provider: string;
|
|
124
157
|
needs: MissingField;
|
|
125
158
|
}[];
|
|
126
159
|
/** Render a plan as the block `--dry` and `--check` print. */
|
|
127
|
-
declare function formatPlan(p: AdsPlan): string;
|
|
160
|
+
export declare function formatPlan(p: AdsPlan): string;
|
|
128
161
|
/** The outcome of one property's run, for the summary table. */
|
|
129
162
|
interface AdPropertyResult {
|
|
130
163
|
provider: string;
|
|
@@ -134,7 +167,7 @@ interface AdPropertyResult {
|
|
|
134
167
|
reminders: string[];
|
|
135
168
|
}
|
|
136
169
|
/** Render the end-of-run summary: one row per property, plus any reminders. */
|
|
137
|
-
declare function formatSummary(results: AdPropertyResult[]): string;
|
|
170
|
+
export declare function formatSummary(results: AdPropertyResult[]): string;
|
|
138
171
|
//#endregion
|
|
139
172
|
//#region src/ads/providers.d.ts
|
|
140
173
|
/** How a platform proves you own the domain. */
|
|
@@ -191,11 +224,11 @@ interface AdPlatform {
|
|
|
191
224
|
/** Follow-ups no API covers, given this entry. */
|
|
192
225
|
manualSteps: (entry: AdPropertySetup) => string[];
|
|
193
226
|
}
|
|
194
|
-
declare const AD_PLATFORMS: Record<AdProvider, AdPlatform>;
|
|
227
|
+
export declare const AD_PLATFORMS: Record<AdProvider, AdPlatform>;
|
|
195
228
|
/** Every provider, in table order — the one place iteration order is decided. */
|
|
196
|
-
declare const AD_PROVIDER_KEYS: AdProvider[];
|
|
229
|
+
export declare const AD_PROVIDER_KEYS: AdProvider[];
|
|
197
230
|
/** Whether a string names a known provider (for CLI arg validation). */
|
|
198
|
-
declare const isAdProvider: (value: string) => value is AdProvider;
|
|
231
|
+
export declare const isAdProvider: (value: string) => value is AdProvider;
|
|
199
232
|
/**
|
|
200
233
|
* The DNS TXT record content for a token.
|
|
201
234
|
*
|
|
@@ -205,11 +238,11 @@ declare const isAdProvider: (value: string) => value is AdProvider;
|
|
|
205
238
|
* pasted the full record from the UI gets what they meant rather than a
|
|
206
239
|
* double-prefixed record that verifies against nothing.
|
|
207
240
|
*/
|
|
208
|
-
declare function txtRecord(provider: AdProvider, token: string): string;
|
|
241
|
+
export declare function txtRecord(provider: AdProvider, token: string): string;
|
|
209
242
|
/** The tag id for an entry, or undefined when the field it needs is unset. */
|
|
210
|
-
declare function tagId(entry: AdPropertySetup): string | undefined;
|
|
243
|
+
export declare function tagId(entry: AdPropertySetup): string | undefined;
|
|
211
244
|
/** The consent category gating an entry's tag. `marketing` unless stated otherwise. */
|
|
212
|
-
declare const categoryOf: (entry: AdPropertySetup) => AdConsentCategory;
|
|
245
|
+
export declare const categoryOf: (entry: AdPropertySetup) => AdConsentCategory;
|
|
213
246
|
/**
|
|
214
247
|
* One provider's tag, rendered as a **consent-gated, inert** `<script>`.
|
|
215
248
|
*
|
|
@@ -221,7 +254,7 @@ declare const categoryOf: (entry: AdPropertySetup) => AdConsentCategory;
|
|
|
221
254
|
* Returns null when the entry has no tag id, so a caller can report the gap rather
|
|
222
255
|
* than emit a snippet that initialises `undefined`.
|
|
223
256
|
*/
|
|
224
|
-
declare function renderTag(entry: AdPropertySetup, strategy?: 'idle' | 'async' | 'interaction'): string | null;
|
|
257
|
+
export declare function renderTag(entry: AdPropertySetup, strategy?: 'idle' | 'async' | 'interaction'): string | null;
|
|
225
258
|
//#endregion
|
|
226
|
-
export {
|
|
259
|
+
export { type AdConsentCategory, type AdDomainState, type AdPlatform, type AdPropertyPlan, type AdPropertyResult, type AdPropertySetup, type AdProvider, type AdTagSpec, type AdVerificationSpec, type AdsConfig, type AdsPlan, type ConversionAction, type ConversionLabelMap, type MissingField, type StepAction, type StepPlan, createApexTxt, findZoneId, listApexTxt };
|
|
227
260
|
//# sourceMappingURL=index.d.mts.map
|
package/dist/ads/index.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.mts","names":[],"sources":["../../src/ads/types.ts","../../src/ads/plan.ts","../../src/ads/providers.ts"],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../../src/ads/types.ts","../../src/ads/plan.ts","../../src/ads/providers.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;KAsBY;;KAWA;;;;;;;KAQA;;;;;;;;;;;;UAaK;EACf;EACA;EACA;;;;;;wBAOc,0BACd,gBAAgB,iCACf;;UAMc;EACf,UAAU;;;;;EAKV;;EAEA;;;;;;;;EAQA,2BAA2B;;EAE3B;;;;;;;;EAQA;;EAEA,WAAW;EACX;;;UAIe;;EAEf,YAAY;;;;;;;;;;;UAYG;EACf;;EAEA;;;;;KCjGU;;KAGA;UAEK;EACf,QAAQ;;EAER;;EAEA,QAAQ;;;UAIO;EACf,UAAU;;EAEV;;EAEA;;EAEA,KAAK;;EAEL,KAAK;;EAEL;;EAEA;;UAGe;EACf,YAAY;;EAEZ;;;;;;;;;;;wBAYc,aAAa,OAAO,iBAAiB,OAAO,gBAAgB;;wBAoD5D,KAAK,QAAQ,WAAW,QAAQ,YAAY,iBAAiB;;wBAe7D,SAAS,GAAG;;wBAKZ,cAAc,GAAG;EAAY;EAAkB,OAAO;;;wBAYtD,WAAW,GAAG;;UAcb;EACf;EACA;EACA;EACA;EACA;;;wBAIc,cAAc,SAAS;;;;UC7ItB;EACf;;;;;;;EAOA;;EAEA;;EAEA;;;UAIe;;EAEf;;EAEA,OAAO;;EAEP,YAAY,YAAY,OAAO;;;;;EAK/B;;EAEA;;EAEA;;UAGe;;EAEf;;;;;;;;;EASA;;EAEA;;EAEA;EACA,cAAc;EACd,KAAK;;EAEL,cAAc,OAAO;;qBA0CV,cAAc,OAAO,YAAY;;qBAuMjC,kBAAgD;;qBAGhD,eAAgB,kBAAgB,SAAS;;;;;;;;;;wBAWtC,UAAU,UAAU,YAAY;;wBAOhC,MAAM,OAAO;;qBAMhB,aAAc,OAAO,oBAAkB;;;;;;;;;;;;wBAiBpC,UACd,OAAO,iBACP"}
|
package/dist/ads/index.mjs
CHANGED
|
@@ -1,8 +1,74 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { t as table } from "../plan-format-A18SvJ7C.mjs";
|
|
2
|
+
import { n as findZoneId, r as listApexTxt, t as createApexTxt } from "../cloudflare-CD8vwrgj.mjs";
|
|
2
3
|
|
|
4
|
+
//#region src/ads/types.ts
|
|
5
|
+
/**
|
|
6
|
+
* A bare string is back-compat shorthand for `{ call: label }` — the only
|
|
7
|
+
* action that existed before this was a map. Absent input normalises to `{}`.
|
|
8
|
+
*/
|
|
9
|
+
function normalizeConversionLabels(label) {
|
|
10
|
+
if (label == null) return {};
|
|
11
|
+
return typeof label === "string" ? { call: label } : label;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
//#endregion
|
|
3
15
|
//#region src/ads/providers.ts
|
|
4
|
-
/**
|
|
5
|
-
|
|
16
|
+
/**
|
|
17
|
+
* What each ad platform actually lets a build tool do — the table every `vitops ads`
|
|
18
|
+
* subcommand reads.
|
|
19
|
+
*
|
|
20
|
+
* Written the way the indexing module was written: start from what the platforms
|
|
21
|
+
* accept, because it is much narrower than it looks, and record the *absence* of a
|
|
22
|
+
* path as a fact rather than leaving it as a gap.
|
|
23
|
+
*
|
|
24
|
+
* - **Four platforms verify a domain by DNS TXT** (Meta, TikTok, Pinterest,
|
|
25
|
+
* Snapchat). That record is the one thing here that can be created for you.
|
|
26
|
+
* - **Four have no domain verification at all** (Google Ads, LinkedIn, Reddit,
|
|
27
|
+
* Microsoft Ads). Linking is the tag and the account id, nothing else. Those
|
|
28
|
+
* resolve to a skip *plus* a named reason — a silent omission would read as
|
|
29
|
+
* "already done".
|
|
30
|
+
* - **No platform Marketing API is called.** Meta's Business Management
|
|
31
|
+
* `verify_domain` needs a system-user token; the Google Ads API needs an
|
|
32
|
+
* approved developer token and puts the consumer's own account on the line.
|
|
33
|
+
* That is the same reasoning that keeps the Google Indexing API unwired: a
|
|
34
|
+
* toolchain other people install must not ship a documented path to a terms
|
|
35
|
+
* violation. The final "Verify" click stays a reminder.
|
|
36
|
+
*
|
|
37
|
+
* The verification token is **not a secret** — it is published in DNS, and the
|
|
38
|
+
* platform fetching it back is the ownership proof, exactly like the IndexNow key.
|
|
39
|
+
* So it lives in the config while credentials stay in the environment.
|
|
40
|
+
*
|
|
41
|
+
* Each tag is the platform's documented bootstrap reduced to its queue stub, with
|
|
42
|
+
* the remote library on `data-src` for the consent gate to attach after the
|
|
43
|
+
* bootstrap has run (`@getvitops/core`'s `activateScript` emits the inline body
|
|
44
|
+
* first for exactly this reason). Two platforms — Microsoft and Pinterest — ship a
|
|
45
|
+
* snippet that creates its own `<script>`; those declare no `src`, and their body
|
|
46
|
+
* stays inert until activation, so an undecided visitor still issues no request.
|
|
47
|
+
*/
|
|
48
|
+
/**
|
|
49
|
+
* `gtag` is shared by every Google tag; the id shape (`AW-…`) is what differs.
|
|
50
|
+
*
|
|
51
|
+
* `gtag('config', id)` only loads the tag — it never reports a conversion. When
|
|
52
|
+
* `entry.conversionLabel` names one or more actions, this also exposes a
|
|
53
|
+
* `{action: 'id/label'}` map on `window.vitopsAdsConversions.google` — one entry
|
|
54
|
+
* per configured action, so a call, an email click and a form submission each
|
|
55
|
+
* carry their own `send_to` target instead of all three sharing one (that
|
|
56
|
+
* sharing was a real bug: a call and an email click used to report as the same
|
|
57
|
+
* Google Ads conversion action, indistinguishable in the account). Bootstrap
|
|
58
|
+
* time only loads the tag; `scripts/tracking.ts`'s click handler and
|
|
59
|
+
* `vitopsFormConversion()` each read their own action's key and fire
|
|
60
|
+
* `gtag('event', 'conversion', …)` at the moment of the actual interaction —
|
|
61
|
+
* bootstrap-time firing would count every pageview as a conversion.
|
|
62
|
+
*
|
|
63
|
+
* `entry` is trusted here — `resolveAds()` validates every configured label
|
|
64
|
+
* against `SAFE_ID` before it reaches this function, the same guard `id`
|
|
65
|
+
* already gets.
|
|
66
|
+
*/
|
|
67
|
+
const gtagBootstrap = (id, entry) => {
|
|
68
|
+
const labels = normalizeConversionLabels(entry.conversionLabel);
|
|
69
|
+
const actions = Object.entries(labels).filter((pair) => !!pair[1]);
|
|
70
|
+
return `window.dataLayer=window.dataLayer||[];function gtag(){dataLayer.push(arguments)}gtag('js',new Date());gtag('config','${id}');` + (actions.length ? `window.vitopsAdsConversions=window.vitopsAdsConversions||{};window.vitopsAdsConversions.google=${JSON.stringify(Object.fromEntries(actions.map(([action, label]) => [action, `${id}/${label}`])))};` : "");
|
|
71
|
+
};
|
|
6
72
|
/** The queue-stub shape shared by fbq/ttq/rdt/snaptr/pintrk. */
|
|
7
73
|
const queueStub = (global, methods) => `window.${global}=window.${global}||function(){(window.${global}.q=window.${global}.q||[]).push(arguments)};window.${global}.methods=${methods};`;
|
|
8
74
|
const AD_PLATFORMS = {
|
|
@@ -32,7 +98,7 @@ const AD_PLATFORMS = {
|
|
|
32
98
|
privacyUrl: "https://policies.google.com/privacy",
|
|
33
99
|
optOut: "https://adssettings.google.com/"
|
|
34
100
|
},
|
|
35
|
-
manualSteps: (e) => [...e.conversionLabel ? [] : ["create a conversion action in Google Ads and record its label as `conversionLabel`"], "link the Google Ads account to Search Console / GA4 in the UI if you want imported conversions"]
|
|
101
|
+
manualSteps: (e) => [...Object.keys(normalizeConversionLabels(e.conversionLabel)).length ? [] : ["create a conversion action in Google Ads and record its label as `conversionLabel`"], "link the Google Ads account to Search Console / GA4 in the UI if you want imported conversions"]
|
|
36
102
|
},
|
|
37
103
|
meta: {
|
|
38
104
|
name: "Meta",
|
|
@@ -353,19 +419,22 @@ function formatPlan(p) {
|
|
|
353
419
|
}
|
|
354
420
|
/** Render the end-of-run summary: one row per property, plus any reminders. */
|
|
355
421
|
function formatSummary(results) {
|
|
356
|
-
|
|
422
|
+
const headers = [
|
|
357
423
|
"PROVIDER",
|
|
358
424
|
"DOMAIN",
|
|
359
425
|
"VERIFY",
|
|
360
426
|
"TAG"
|
|
361
|
-
]
|
|
427
|
+
];
|
|
428
|
+
const rows = results.map((r) => [
|
|
362
429
|
r.provider,
|
|
363
430
|
r.domain,
|
|
364
431
|
r.txt,
|
|
365
432
|
r.tag
|
|
366
|
-
])
|
|
433
|
+
]);
|
|
434
|
+
const reminders = results.flatMap((r) => r.reminders.map((m) => ` ! ${r.provider}: ${m}`));
|
|
435
|
+
return table(headers, rows, reminders);
|
|
367
436
|
}
|
|
368
437
|
|
|
369
438
|
//#endregion
|
|
370
|
-
export { AD_PLATFORMS, AD_PROVIDER_KEYS, categoryOf, createApexTxt, findZoneId, formatPlan, formatSummary, hasDrift, isAdProvider, listApexTxt, missingFields, plan, planProperty, renderTag, tagId, txtRecord };
|
|
439
|
+
export { AD_PLATFORMS, AD_PROVIDER_KEYS, categoryOf, createApexTxt, findZoneId, formatPlan, formatSummary, hasDrift, isAdProvider, listApexTxt, missingFields, normalizeConversionLabels, plan, planProperty, renderTag, tagId, txtRecord };
|
|
371
440
|
//# sourceMappingURL=index.mjs.map
|