@getvitops/astro 8.1.0 → 8.2.0-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +361 -29
- package/dist/exports/tracking.d.mts +1 -1
- package/dist/exports/tracking.mjs +1 -1
- package/dist/index.d.mts +45 -18
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +153 -17
- package/dist/index.mjs.map +1 -1
- package/dist/middleware.d.mts +25 -0
- package/dist/middleware.d.mts.map +1 -0
- package/dist/middleware.mjs +35 -0
- package/dist/middleware.mjs.map +1 -0
- package/dist/routes/conversion.d.mts +70 -11
- package/dist/routes/conversion.d.mts.map +1 -1
- package/dist/routes/conversion.mjs +114 -20
- package/dist/routes/conversion.mjs.map +1 -1
- package/dist/{tracking-DdEKExlE.d.mts → tracking-BNKKBqOe.d.mts} +2 -2
- package/dist/tracking-BNKKBqOe.d.mts.map +1 -0
- package/dist/{tracking-CmSfxWC3.mjs → tracking-M2PqlMpw.mjs} +2 -2
- package/dist/tracking-M2PqlMpw.mjs.map +1 -0
- package/package.json +10 -7
- package/src/ads.ts +46 -3
- package/src/analytics.ts +98 -11
- package/src/components/Analytics.astro +5 -1
- package/src/components/Icon.astro +8 -1
- package/src/components/Tracking.astro +2 -1
- package/src/scripts/tracking.ts +114 -21
- package/src/tracking.ts +1 -1
- package/virtual.d.ts +17 -1
- package/dist/tracking-CmSfxWC3.mjs.map +0 -1
- package/dist/tracking-DdEKExlE.d.mts.map +0 -1
package/dist/index.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/analytics.ts","../src/seo.ts","../src/integration.ts","../src/ads.ts","../src/lastmod.ts"],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":["MediaOutputKind"],"sources":["../src/analytics.ts","../src/seo.ts","../src/integration.ts","../src/ads.ts","../src/lastmod.ts"],"mappings":"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"}
|
package/dist/index.mjs
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import { n as TRACKING_ENDPOINT, r as resolveTracking, t as TRACKING_COOKIE } from "./tracking-
|
|
1
|
+
import { n as TRACKING_ENDPOINT, r as resolveTracking, t as TRACKING_COOKIE } from "./tracking-M2PqlMpw.mjs";
|
|
2
2
|
import { cpSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { dirname, join, relative, resolve } from "node:path";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
import { isConfig, renderRobotsTxt, resolveConfig } from "@getvitops/generator";
|
|
6
6
|
import { collectIconRefs, faviconLinks, generateFavicons, generateIconInclude, resolveIcon, scanFiles, writeFaviconManifest } from "@getvitops/utils";
|
|
7
7
|
import vitopsPlugin from "@getvitops/vite";
|
|
8
|
-
import { AD_PLATFORMS, AD_PROVIDER_KEYS } from "@getvitops/utils/ads";
|
|
8
|
+
import { AD_PLATFORMS, AD_PROVIDER_KEYS, normalizeConversionLabels } from "@getvitops/utils/ads";
|
|
9
9
|
import { fontProviders } from "astro/config";
|
|
10
10
|
import { execFile } from "node:child_process";
|
|
11
11
|
import { promisify } from "node:util";
|
|
@@ -14,6 +14,20 @@ export * from "@getvitops/utils"
|
|
|
14
14
|
|
|
15
15
|
//#region src/analytics.ts
|
|
16
16
|
/**
|
|
17
|
+
* A small inline snippet reading each experiment's cookie into a plain
|
|
18
|
+
* object, skipping any experiment whose cookie isn't set (visitor not yet
|
|
19
|
+
* assigned, or the experiment disabled after this page was cached). Shared
|
|
20
|
+
* across providers so the cookie-parsing logic — and its `SAFE_ID` guard on
|
|
21
|
+
* both the experiment key and cookie name, the same discipline `bad()`
|
|
22
|
+
* already applies to provider IDs landing in an inline `<script>` — exists
|
|
23
|
+
* exactly once.
|
|
24
|
+
*/
|
|
25
|
+
function experimentCookieReads(experiments) {
|
|
26
|
+
const entries = Object.entries(experiments).filter(([key, e]) => !bad(key) && !bad(e.cookieName));
|
|
27
|
+
if (!entries.length) return "";
|
|
28
|
+
return "function _abrc(n){var m=document.cookie.match(new RegExp('(?:^|; )'+n+'=([^;]*)'));return m?decodeURIComponent(m[1]):null;}var _ab={};" + entries.map(([key, e]) => `_ab[${JSON.stringify(key)}]=_abrc(${JSON.stringify(e.cookieName)});`).join("") + `for(var _k in _ab){if(_ab[_k]==null)delete _ab[_k];}`;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
17
31
|
* IDs land inside an inline `<script>`, so they are checked rather than trusted.
|
|
18
32
|
* A `<\/script>` in a measurement ID is not a plausible accident, but the cost of
|
|
19
33
|
* the guard is one regex and the cost of not having it is arbitrary script
|
|
@@ -27,10 +41,12 @@ function bad(value) {
|
|
|
27
41
|
function literal(value) {
|
|
28
42
|
return JSON.stringify(value).replaceAll("<", "\\u003c");
|
|
29
43
|
}
|
|
30
|
-
function googleAnalytics(input) {
|
|
44
|
+
function googleAnalytics(input, experiments = {}) {
|
|
31
45
|
const opts = typeof input === "string" ? { id: input } : input;
|
|
32
46
|
if (bad(opts.id)) return `googleAnalytics: "${opts.id}" is not a valid measurement ID. Skipped.`;
|
|
33
|
-
const
|
|
47
|
+
const staticConfig = opts.config ?? {};
|
|
48
|
+
const reads = experimentCookieReads(experiments);
|
|
49
|
+
const gtagConfig = reads ? `${reads}gtag('config','${opts.id}',Object.assign(${literal(staticConfig)},_ab));` : `gtag('config','${opts.id}'${opts.config ? `,${literal(staticConfig)}` : ""});`;
|
|
34
50
|
return {
|
|
35
51
|
key: "googleAnalytics",
|
|
36
52
|
provider: "Google Analytics",
|
|
@@ -45,7 +61,7 @@ function googleAnalytics(input) {
|
|
|
45
61
|
],
|
|
46
62
|
strategy: "idle",
|
|
47
63
|
gated: true,
|
|
48
|
-
inline:
|
|
64
|
+
inline: "window.dataLayer=window.dataLayer||[];function gtag(){dataLayer.push(arguments)}gtag('js',new Date());" + gtagConfig,
|
|
49
65
|
src: `https://www.googletagmanager.com/gtag/js?id=${encodeURIComponent(opts.id)}`,
|
|
50
66
|
attrs: {}
|
|
51
67
|
};
|
|
@@ -74,7 +90,7 @@ function clarity(input) {
|
|
|
74
90
|
attrs: {}
|
|
75
91
|
};
|
|
76
92
|
}
|
|
77
|
-
function matomo(opts) {
|
|
93
|
+
function matomo(opts, experiments = {}, dimensions = {}) {
|
|
78
94
|
const siteId = String(opts.siteId);
|
|
79
95
|
if (bad(siteId)) return `matomo: "${siteId}" is not a valid site ID. Skipped.`;
|
|
80
96
|
let base;
|
|
@@ -85,6 +101,9 @@ function matomo(opts) {
|
|
|
85
101
|
}
|
|
86
102
|
const url = `${base.origin}${base.pathname.replace(/\/*$/, "")}/`;
|
|
87
103
|
const cookies = opts.cookies === true;
|
|
104
|
+
const dimensionEntries = Object.entries(experiments).filter(([key]) => dimensions[key] != null);
|
|
105
|
+
const reads = experimentCookieReads(Object.fromEntries(dimensionEntries));
|
|
106
|
+
const dimensionPushes = reads ? reads + dimensionEntries.map(([key]) => `if(_ab[${JSON.stringify(key)}]!=null)_paq.push(['setCustomDimension',${dimensions[key]},_ab[${JSON.stringify(key)}]]);`).join("") : "";
|
|
88
107
|
return {
|
|
89
108
|
key: "matomo",
|
|
90
109
|
provider: "Matomo",
|
|
@@ -99,14 +118,15 @@ function matomo(opts) {
|
|
|
99
118
|
] : [],
|
|
100
119
|
strategy: "idle",
|
|
101
120
|
gated: true,
|
|
102
|
-
inline: `var _paq=window._paq=window._paq||[];` + (cookies ? "" : `_paq.push(['disableCookies']);`) + `_paq.push(['trackPageView']);_paq.push(['enableLinkTracking']);(function(){var u="${url}";_paq.push(['setTrackerUrl',u+'matomo.php']);_paq.push(['setSiteId','${siteId}']);var d=document,g=d.createElement('script'),s=d.getElementsByTagName('script')[0];g.async=true;g.src=u+'matomo.js';s.parentNode.insertBefore(g,s)})();`,
|
|
121
|
+
inline: `var _paq=window._paq=window._paq||[];` + (cookies ? "" : `_paq.push(['disableCookies']);`) + dimensionPushes + `_paq.push(['trackPageView']);_paq.push(['enableLinkTracking']);(function(){var u="${url}";_paq.push(['setTrackerUrl',u+'matomo.php']);_paq.push(['setSiteId','${siteId}']);var d=document,g=d.createElement('script'),s=d.getElementsByTagName('script')[0];g.async=true;g.src=u+'matomo.js';s.parentNode.insertBefore(g,s)})();`,
|
|
103
122
|
src: null,
|
|
104
123
|
attrs: {}
|
|
105
124
|
};
|
|
106
125
|
}
|
|
107
|
-
function plausible(input) {
|
|
126
|
+
function plausible(input, experiments = {}) {
|
|
108
127
|
const opts = typeof input === "string" ? { domain: input } : input;
|
|
109
128
|
if (bad(opts.domain)) return `plausible: "${opts.domain}" is not a valid domain. Skipped.`;
|
|
129
|
+
const reads = experimentCookieReads(experiments);
|
|
110
130
|
return {
|
|
111
131
|
key: "plausible",
|
|
112
132
|
provider: "Plausible Analytics",
|
|
@@ -115,7 +135,7 @@ function plausible(input) {
|
|
|
115
135
|
cookies: [],
|
|
116
136
|
strategy: "idle",
|
|
117
137
|
gated: true,
|
|
118
|
-
inline: "",
|
|
138
|
+
inline: reads ? `window.plausible=window.plausible||function(){(window.plausible.q=window.plausible.q||[]).push(arguments)};plausible.init({customProperties:function(){${reads}return _ab;}});` : "",
|
|
119
139
|
src: opts.src ?? "https://plausible.io/js/script.js",
|
|
120
140
|
attrs: { "data-domain": opts.domain }
|
|
121
141
|
};
|
|
@@ -136,11 +156,12 @@ function resolveAnalytics(opts, ctx = {}) {
|
|
|
136
156
|
warnings
|
|
137
157
|
};
|
|
138
158
|
const strategy = opts.strategy ?? "idle";
|
|
159
|
+
const experiments = ctx.experiments ?? {};
|
|
139
160
|
const results = [
|
|
140
|
-
opts.googleAnalytics === void 0 ? null : googleAnalytics(opts.googleAnalytics),
|
|
161
|
+
opts.googleAnalytics === void 0 ? null : googleAnalytics(opts.googleAnalytics, experiments),
|
|
141
162
|
opts.clarity === void 0 ? null : clarity(opts.clarity),
|
|
142
|
-
opts.matomo === void 0 ? null : matomo(opts.matomo),
|
|
143
|
-
opts.plausible === void 0 ? null : plausible(opts.plausible)
|
|
163
|
+
opts.matomo === void 0 ? null : matomo(opts.matomo, experiments, ctx.matomoAbVariantDimensions ?? {}),
|
|
164
|
+
opts.plausible === void 0 ? null : plausible(opts.plausible, experiments)
|
|
144
165
|
];
|
|
145
166
|
for (const result of results) {
|
|
146
167
|
if (result === null) continue;
|
|
@@ -239,6 +260,18 @@ function resolveAds(ads, ctx = {}) {
|
|
|
239
260
|
warnings.push(`ads: ${provider} ${platform.tag.needs} "${id}" is not a valid ID. Skipped.`);
|
|
240
261
|
continue;
|
|
241
262
|
}
|
|
263
|
+
let safeEntry = entry;
|
|
264
|
+
const labels = normalizeConversionLabels(entry.conversionLabel);
|
|
265
|
+
const invalidActions = Object.entries(labels).filter(([, label]) => label && !SAFE_ID.test(label)).map(([action]) => action);
|
|
266
|
+
if (invalidActions.length) {
|
|
267
|
+
for (const action of invalidActions) warnings.push(`ads: ${provider} conversionLabel.${action} "${labels[action]}" is not a valid label. Conversion firing skipped for that action.`);
|
|
268
|
+
const cleanLabels = Object.fromEntries(Object.entries(labels).filter(([action]) => !invalidActions.includes(action)));
|
|
269
|
+
const { conversionLabel: _drop, ...rest } = entry;
|
|
270
|
+
safeEntry = Object.keys(cleanLabels).length ? {
|
|
271
|
+
...rest,
|
|
272
|
+
conversionLabel: cleanLabels
|
|
273
|
+
} : rest;
|
|
274
|
+
}
|
|
242
275
|
const category = entry.category ?? "marketing";
|
|
243
276
|
tags.push({
|
|
244
277
|
key: provider,
|
|
@@ -250,7 +283,7 @@ function resolveAds(ads, ctx = {}) {
|
|
|
250
283
|
gated: ctx.consent === true,
|
|
251
284
|
inline: platform.tag.bootstrap(id, {
|
|
252
285
|
provider,
|
|
253
|
-
...
|
|
286
|
+
...safeEntry
|
|
254
287
|
}),
|
|
255
288
|
src: platform.tag.src ? platform.tag.src(id) : null,
|
|
256
289
|
attrs: {}
|
|
@@ -291,7 +324,7 @@ const PROVIDERS = new Set(Object.keys(fontProviders));
|
|
|
291
324
|
* key into a committed file or inventing an interpolation syntax, so Adobe families are
|
|
292
325
|
* declared in `astro.config` directly — see the error thrown below.
|
|
293
326
|
*/
|
|
294
|
-
const NEEDS_OPTIONS = new Set(["adobe"]);
|
|
327
|
+
const NEEDS_OPTIONS = /* @__PURE__ */ new Set(["adobe"]);
|
|
295
328
|
/**
|
|
296
329
|
* Map declarations onto Astro's `fonts:` entries.
|
|
297
330
|
*
|
|
@@ -326,6 +359,31 @@ function resolveFonts(decls) {
|
|
|
326
359
|
};
|
|
327
360
|
}
|
|
328
361
|
|
|
362
|
+
//#endregion
|
|
363
|
+
//#region src/i18n.ts
|
|
364
|
+
function codesOf(entry) {
|
|
365
|
+
return typeof entry === "string" ? [entry] : entry.codes;
|
|
366
|
+
}
|
|
367
|
+
/** Every BCP-47 code Astro's `i18n` block actually recognises. */
|
|
368
|
+
function astroLocaleCodes(i18n) {
|
|
369
|
+
return new Set(i18n.locales.flatMap(codesOf));
|
|
370
|
+
}
|
|
371
|
+
/**
|
|
372
|
+
* Warning strings for a mismatch — `[]` when either side is absent (nothing
|
|
373
|
+
* to reconcile) or the two agree.
|
|
374
|
+
*/
|
|
375
|
+
function localeConsistencyWarnings(site, i18n) {
|
|
376
|
+
if (!site || !i18n) return [];
|
|
377
|
+
const warnings = [];
|
|
378
|
+
const astroCodes = astroLocaleCodes(i18n);
|
|
379
|
+
const siteCodes = new Set(Object.keys(site.locales));
|
|
380
|
+
const onlyInSite = [...siteCodes].filter((c) => !astroCodes.has(c));
|
|
381
|
+
const onlyInAstro = [...astroCodes].filter((c) => !siteCodes.has(c));
|
|
382
|
+
if (onlyInSite.length || onlyInAstro.length) warnings.push(`i18n: site.locales and astro.config.mjs's i18n.locales disagree` + (onlyInSite.length ? ` — site.locales has ${onlyInSite.map((c) => `"${c}"`).join(", ")} that astro's i18n doesn't` : "") + (onlyInAstro.length ? `${onlyInSite.length ? ", and" : " —"} astro's i18n has ${onlyInAstro.map((c) => `"${c}"`).join(", ")} that site.locales doesn't` : "") + ". EmDash reads locales from astro.config.mjs directly, so a locale missing there never reaches its admin regardless of what site.locales says.");
|
|
383
|
+
if (i18n.defaultLocale !== site.defaultLocale) warnings.push(`i18n: astro.config.mjs's i18n.defaultLocale ("${i18n.defaultLocale}") does not match site.defaultLocale ("${site.defaultLocale}") — SEO/legal prose renders in one locale by default while EmDash's own routing defaults to the other.`);
|
|
384
|
+
return warnings;
|
|
385
|
+
}
|
|
386
|
+
|
|
329
387
|
//#endregion
|
|
330
388
|
//#region src/integration.ts
|
|
331
389
|
/**
|
|
@@ -367,6 +425,25 @@ function undisclosedProviders(configPath, analytics) {
|
|
|
367
425
|
return Object.keys(SITE_CONFIG_KEYS).filter((key) => analytics[key] !== void 0 && !declared[SITE_CONFIG_KEYS[key]]);
|
|
368
426
|
}
|
|
369
427
|
/**
|
|
428
|
+
* Read `site.locales`/`site.defaultLocale` out of the site config, for
|
|
429
|
+
* `localeConsistencyWarnings` to check against Astro's own `i18n` block.
|
|
430
|
+
*
|
|
431
|
+
* Best-effort, matching `readSiteAds`'s shape exactly: a malformed or missing
|
|
432
|
+
* config is silence here, since every other consumer of this file already
|
|
433
|
+
* validates it loudly elsewhere in the same hook.
|
|
434
|
+
*/
|
|
435
|
+
function readSiteLocales(configPath, siteEnv) {
|
|
436
|
+
try {
|
|
437
|
+
const cfg = resolveConfig(JSON.parse(readFileSync(configPath, "utf8")), siteEnv);
|
|
438
|
+
return {
|
|
439
|
+
locales: cfg.site.locales,
|
|
440
|
+
defaultLocale: cfg.site.defaultLocale
|
|
441
|
+
};
|
|
442
|
+
} catch {
|
|
443
|
+
return null;
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
/**
|
|
370
447
|
* Read `site.ads` and this environment's ads switch out of the site config.
|
|
371
448
|
*
|
|
372
449
|
* Best-effort, like `undisclosedProviders`: every other consumer of this file
|
|
@@ -395,6 +472,55 @@ function readSiteAds(configPath, siteEnv) {
|
|
|
395
472
|
}
|
|
396
473
|
}
|
|
397
474
|
/**
|
|
475
|
+
* Read `site.experiments` + `site.analytics.matomo.abVariantDimensions` out of
|
|
476
|
+
* the site config, resolving each experiment's cookie name (default
|
|
477
|
+
* `_ab_<key>`, matching `vitopsAbTesting()`'s own default) so `<Analytics />`
|
|
478
|
+
* doesn't need to know the fallback rule itself.
|
|
479
|
+
*
|
|
480
|
+
* Mirrors `readSiteAds`'s shape: best-effort, only enabled experiments
|
|
481
|
+
* (a disabled one has no cookie to read and shouldn't cost every provider's
|
|
482
|
+
* bootstrap a no-op check), `null` when nothing's configured.
|
|
483
|
+
*/
|
|
484
|
+
function readSiteExperiments(configPath, siteEnv) {
|
|
485
|
+
try {
|
|
486
|
+
const cfg = resolveConfig(JSON.parse(readFileSync(configPath, "utf8")), siteEnv);
|
|
487
|
+
const configured = cfg.site.experiments;
|
|
488
|
+
if (!configured || Object.keys(configured).length === 0) return null;
|
|
489
|
+
const experiments = {};
|
|
490
|
+
let primaryCookie = null;
|
|
491
|
+
for (const [key, experiment] of Object.entries(configured)) {
|
|
492
|
+
if (!experiment.enabled) continue;
|
|
493
|
+
const cookieName = experiment.cookieName ?? `_ab_${key}`;
|
|
494
|
+
experiments[key] = { cookieName };
|
|
495
|
+
primaryCookie ??= cookieName;
|
|
496
|
+
}
|
|
497
|
+
if (Object.keys(experiments).length === 0) return null;
|
|
498
|
+
return {
|
|
499
|
+
experiments,
|
|
500
|
+
matomoAbVariantDimensions: cfg.site.analytics?.matomo?.abVariantDimensions ?? {},
|
|
501
|
+
primaryCookie
|
|
502
|
+
};
|
|
503
|
+
} catch {
|
|
504
|
+
return null;
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
/**
|
|
508
|
+
* Is `site.crm` configured and not explicitly disabled?
|
|
509
|
+
*
|
|
510
|
+
* Mirrors `readSiteAds`'s "read the config, decide nothing else" shape — this
|
|
511
|
+
* only answers the one question `hasConversionRoute`'s warning needs, not the
|
|
512
|
+
* whole `CrmConfig`, which the consumer's own route file builds from the same
|
|
513
|
+
* config however it likes.
|
|
514
|
+
*/
|
|
515
|
+
function siteCrmEnabled(configPath, siteEnv) {
|
|
516
|
+
try {
|
|
517
|
+
const cfg = resolveConfig(JSON.parse(readFileSync(configPath, "utf8")), siteEnv);
|
|
518
|
+
return Boolean(cfg.site.crm && cfg.site.crm.enabled !== false);
|
|
519
|
+
} catch {
|
|
520
|
+
return false;
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
/**
|
|
398
524
|
* The consent + tracking facts a site config already states.
|
|
399
525
|
*
|
|
400
526
|
* These were the one pair the integration did not read. `site.tracking` and
|
|
@@ -547,6 +673,10 @@ function vitops(opts = {}) {
|
|
|
547
673
|
if (opts.site && !opts.css) logger.warn("site: needs a `css` config — it is read during generation, which runs in the Vite plugin that `css` registers. Without it the site config changes nothing here.");
|
|
548
674
|
const legalInput = opts.legal?.input ?? siteInput;
|
|
549
675
|
if (opts.legal && !legalInput) throw new Error("[vitops] legal: needs `legal.input` (a site config path), the top-level `site` option, or a `css.input` that is itself a site config.");
|
|
676
|
+
if (siteInput) {
|
|
677
|
+
const siteLocales = readSiteLocales(resolve(root, siteInput), opts.site?.siteEnv ?? "production");
|
|
678
|
+
for (const warning of localeConsistencyWarnings(siteLocales, config.i18n)) logger.warn(warning);
|
|
679
|
+
}
|
|
550
680
|
if (opts.seo) {
|
|
551
681
|
if (!config.site) logger.warn("seo: needs the `site` astro.config option (your deployed URL) — without it <Seo /> omits canonical, og:url and any relative og:image rather than guessing an origin.");
|
|
552
682
|
if (config.integrations.some((i) => i.name === "emdash")) logger.warn("seo: emdash() is registered, and its <EmDashHead> emits the same title/description/canonical/og tags from the CMS. Use one or the other — rendering both duplicates every tag.");
|
|
@@ -561,6 +691,7 @@ function vitops(opts = {}) {
|
|
|
561
691
|
const analytics = resolveAnalytics(opts.analytics, { consent: consentEnabled });
|
|
562
692
|
const trackingOpts = opts.tracking === true ? { enabled: true } : opts.tracking || (siteConsent?.tracking?.enabled ? siteConsent.tracking : void 0);
|
|
563
693
|
const adsConfig = siteInput ? readSiteAds(resolve(root, siteInput), opts.site?.siteEnv ?? "production") : null;
|
|
694
|
+
const experimentsConfig = siteInput ? readSiteExperiments(resolve(root, siteInput), opts.site?.siteEnv ?? "production") : null;
|
|
564
695
|
const detectedCategories = consentCategories([...analytics.tags, ...resolveAds(adsConfig?.properties, {
|
|
565
696
|
consent: consentEnabled,
|
|
566
697
|
enabled: true
|
|
@@ -576,6 +707,7 @@ function vitops(opts = {}) {
|
|
|
576
707
|
});
|
|
577
708
|
for (const warning of tracking.warnings) logger.warn(warning);
|
|
578
709
|
if (tracking.enabled && !hasConversionRoute(root)) logger.warn(`tracking is on, so the capture script beacons \`tel:\` conversions to ${TRACKING_ENDPOINT}, but no route answers that path. Add \`src/pages/api/track.ts\` exporting \`createConversionRoute(...)\` from \`@getvitops/astro/routes\` — otherwise every call conversion is silently lost.`);
|
|
710
|
+
if (siteInput && siteCrmEnabled(resolve(root, siteInput), opts.site?.siteEnv ?? "production") && !hasConversionRoute(root)) logger.warn(`site.crm is configured, but no route answers ${TRACKING_ENDPOINT}. Add \`src/pages/api/track.ts\` exporting \`createConversionRoute({ crm: {...}, ... })\` from \`@getvitops/astro/routes\` — otherwise submissions never reach the CRM.`);
|
|
579
711
|
const ads = resolveAds(adsConfig?.properties, {
|
|
580
712
|
consent: consentEnabled,
|
|
581
713
|
consentCategories: offeredCategories,
|
|
@@ -633,7 +765,8 @@ function vitops(opts = {}) {
|
|
|
633
765
|
}
|
|
634
766
|
const consentRuntime = consentEnabled || analytics.needsRuntime;
|
|
635
767
|
if (webComponents || consentRuntime) {
|
|
636
|
-
const
|
|
768
|
+
const corePkg = fileURLToPath(import.meta.resolve("@getvitops/core/package.json"));
|
|
769
|
+
const coreDist = join(dirname(corePkg), "dist");
|
|
637
770
|
const dest = join(publicDir, "vitops");
|
|
638
771
|
if (existsSync(coreDist)) {
|
|
639
772
|
const bundles = webComponents ? [
|
|
@@ -790,8 +923,11 @@ function vitops(opts = {}) {
|
|
|
790
923
|
tracking: tracking.enabled ? {
|
|
791
924
|
enabled: true,
|
|
792
925
|
category: tracking.category,
|
|
793
|
-
cookies: tracking.cookies
|
|
926
|
+
cookies: tracking.cookies,
|
|
927
|
+
abVariantCookie: experimentsConfig?.primaryCookie ?? null
|
|
794
928
|
} : null,
|
|
929
|
+
experiments: experimentsConfig?.experiments ?? {},
|
|
930
|
+
matomoAbVariantDimensions: experimentsConfig?.matomoAbVariantDimensions ?? {},
|
|
795
931
|
ads: adsConfig
|
|
796
932
|
})] } });
|
|
797
933
|
if (opts.css) {
|
|
@@ -924,7 +1060,7 @@ async function commitDates(dirs, cwd) {
|
|
|
924
1060
|
...dirs
|
|
925
1061
|
], {
|
|
926
1062
|
cwd,
|
|
927
|
-
maxBuffer:
|
|
1063
|
+
maxBuffer: 67108864
|
|
928
1064
|
});
|
|
929
1065
|
let current;
|
|
930
1066
|
for (const line of stdout.split("\n")) {
|