@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/dist/index.mjs
CHANGED
|
@@ -1,18 +1,34 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { t as resolveOrganizationName } from "./config-adapters-CbTywgEY.mjs";
|
|
2
|
+
import { n as TRACKING_ENDPOINT, r as resolveTracking, t as TRACKING_COOKIE } from "./tracking-Cb3A0BrC.mjs";
|
|
2
3
|
import { cpSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
4
|
import { dirname, join, relative, resolve } from "node:path";
|
|
4
5
|
import { fileURLToPath } from "node:url";
|
|
5
6
|
import { isConfig, renderRobotsTxt, resolveConfig } from "@getvitops/generator";
|
|
6
7
|
import { collectIconRefs, faviconLinks, generateFavicons, generateIconInclude, resolveIcon, scanFiles, writeFaviconManifest } from "@getvitops/utils";
|
|
7
8
|
import vitopsPlugin from "@getvitops/vite";
|
|
8
|
-
import { AD_PLATFORMS, AD_PROVIDER_KEYS } from "@getvitops/utils/ads";
|
|
9
|
+
import { AD_PLATFORMS, AD_PROVIDER_KEYS, normalizeConversionLabels } from "@getvitops/utils/ads";
|
|
9
10
|
import { fontProviders } from "astro/config";
|
|
11
|
+
import { t } from "@getvitops/utils/content";
|
|
10
12
|
import { execFile } from "node:child_process";
|
|
11
13
|
import { promisify } from "node:util";
|
|
12
14
|
|
|
13
15
|
export * from "@getvitops/utils"
|
|
14
16
|
|
|
15
|
-
|
|
17
|
+
// src/analytics.ts
|
|
18
|
+
/**
|
|
19
|
+
* A small inline snippet reading each experiment's cookie into a plain
|
|
20
|
+
* object, skipping any experiment whose cookie isn't set (visitor not yet
|
|
21
|
+
* assigned, or the experiment disabled after this page was cached). Shared
|
|
22
|
+
* across providers so the cookie-parsing logic — and its `SAFE_ID` guard on
|
|
23
|
+
* both the experiment key and cookie name, the same discipline `bad()`
|
|
24
|
+
* already applies to provider IDs landing in an inline `<script>` — exists
|
|
25
|
+
* exactly once.
|
|
26
|
+
*/
|
|
27
|
+
function experimentCookieReads(experiments) {
|
|
28
|
+
const entries = Object.entries(experiments).filter(([key, e]) => !bad(key) && !bad(e.cookieName));
|
|
29
|
+
if (!entries.length) return "";
|
|
30
|
+
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];}`;
|
|
31
|
+
}
|
|
16
32
|
/**
|
|
17
33
|
* IDs land inside an inline `<script>`, so they are checked rather than trusted.
|
|
18
34
|
* A `<\/script>` in a measurement ID is not a plausible accident, but the cost of
|
|
@@ -27,10 +43,12 @@ function bad(value) {
|
|
|
27
43
|
function literal(value) {
|
|
28
44
|
return JSON.stringify(value).replaceAll("<", "\\u003c");
|
|
29
45
|
}
|
|
30
|
-
function googleAnalytics(input) {
|
|
46
|
+
function googleAnalytics(input, experiments = {}) {
|
|
31
47
|
const opts = typeof input === "string" ? { id: input } : input;
|
|
32
48
|
if (bad(opts.id)) return `googleAnalytics: "${opts.id}" is not a valid measurement ID. Skipped.`;
|
|
33
|
-
const
|
|
49
|
+
const staticConfig = opts.config ?? {};
|
|
50
|
+
const reads = experimentCookieReads(experiments);
|
|
51
|
+
const gtagConfig = reads ? `${reads}gtag('config','${opts.id}',Object.assign(${literal(staticConfig)},_ab));` : `gtag('config','${opts.id}'${opts.config ? `,${literal(staticConfig)}` : ""});`;
|
|
34
52
|
return {
|
|
35
53
|
key: "googleAnalytics",
|
|
36
54
|
provider: "Google Analytics",
|
|
@@ -45,7 +63,7 @@ function googleAnalytics(input) {
|
|
|
45
63
|
],
|
|
46
64
|
strategy: "idle",
|
|
47
65
|
gated: true,
|
|
48
|
-
inline:
|
|
66
|
+
inline: "window.dataLayer=window.dataLayer||[];function gtag(){dataLayer.push(arguments)}gtag('js',new Date());" + gtagConfig,
|
|
49
67
|
src: `https://www.googletagmanager.com/gtag/js?id=${encodeURIComponent(opts.id)}`,
|
|
50
68
|
attrs: {}
|
|
51
69
|
};
|
|
@@ -74,7 +92,7 @@ function clarity(input) {
|
|
|
74
92
|
attrs: {}
|
|
75
93
|
};
|
|
76
94
|
}
|
|
77
|
-
function matomo(opts) {
|
|
95
|
+
function matomo(opts, experiments = {}, dimensions = {}) {
|
|
78
96
|
const siteId = String(opts.siteId);
|
|
79
97
|
if (bad(siteId)) return `matomo: "${siteId}" is not a valid site ID. Skipped.`;
|
|
80
98
|
let base;
|
|
@@ -85,6 +103,9 @@ function matomo(opts) {
|
|
|
85
103
|
}
|
|
86
104
|
const url = `${base.origin}${base.pathname.replace(/\/*$/, "")}/`;
|
|
87
105
|
const cookies = opts.cookies === true;
|
|
106
|
+
const dimensionEntries = Object.entries(experiments).filter(([key]) => dimensions[key] != null);
|
|
107
|
+
const reads = experimentCookieReads(Object.fromEntries(dimensionEntries));
|
|
108
|
+
const dimensionPushes = reads ? reads + dimensionEntries.map(([key]) => `if(_ab[${JSON.stringify(key)}]!=null)_paq.push(['setCustomDimension',${dimensions[key]},_ab[${JSON.stringify(key)}]]);`).join("") : "";
|
|
88
109
|
return {
|
|
89
110
|
key: "matomo",
|
|
90
111
|
provider: "Matomo",
|
|
@@ -99,14 +120,15 @@ function matomo(opts) {
|
|
|
99
120
|
] : [],
|
|
100
121
|
strategy: "idle",
|
|
101
122
|
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)})();`,
|
|
123
|
+
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
124
|
src: null,
|
|
104
125
|
attrs: {}
|
|
105
126
|
};
|
|
106
127
|
}
|
|
107
|
-
function plausible(input) {
|
|
128
|
+
function plausible(input, experiments = {}) {
|
|
108
129
|
const opts = typeof input === "string" ? { domain: input } : input;
|
|
109
130
|
if (bad(opts.domain)) return `plausible: "${opts.domain}" is not a valid domain. Skipped.`;
|
|
131
|
+
const reads = experimentCookieReads(experiments);
|
|
110
132
|
return {
|
|
111
133
|
key: "plausible",
|
|
112
134
|
provider: "Plausible Analytics",
|
|
@@ -115,7 +137,7 @@ function plausible(input) {
|
|
|
115
137
|
cookies: [],
|
|
116
138
|
strategy: "idle",
|
|
117
139
|
gated: true,
|
|
118
|
-
inline: "",
|
|
140
|
+
inline: reads ? `window.plausible=window.plausible||function(){(window.plausible.q=window.plausible.q||[]).push(arguments)};plausible.init({customProperties:function(){${reads}return _ab;}});` : "",
|
|
119
141
|
src: opts.src ?? "https://plausible.io/js/script.js",
|
|
120
142
|
attrs: { "data-domain": opts.domain }
|
|
121
143
|
};
|
|
@@ -136,11 +158,12 @@ function resolveAnalytics(opts, ctx = {}) {
|
|
|
136
158
|
warnings
|
|
137
159
|
};
|
|
138
160
|
const strategy = opts.strategy ?? "idle";
|
|
161
|
+
const experiments = ctx.experiments ?? {};
|
|
139
162
|
const results = [
|
|
140
|
-
opts.googleAnalytics === void 0 ? null : googleAnalytics(opts.googleAnalytics),
|
|
163
|
+
opts.googleAnalytics === void 0 ? null : googleAnalytics(opts.googleAnalytics, experiments),
|
|
141
164
|
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)
|
|
165
|
+
opts.matomo === void 0 ? null : matomo(opts.matomo, experiments, ctx.matomoAbVariantDimensions ?? {}),
|
|
166
|
+
opts.plausible === void 0 ? null : plausible(opts.plausible, experiments)
|
|
144
167
|
];
|
|
145
168
|
for (const result of results) {
|
|
146
169
|
if (result === null) continue;
|
|
@@ -174,8 +197,7 @@ function consentCategories(tags) {
|
|
|
174
197
|
].filter((c) => used.has(c));
|
|
175
198
|
}
|
|
176
199
|
|
|
177
|
-
|
|
178
|
-
//#region src/ads.ts
|
|
200
|
+
// src/ads.ts
|
|
179
201
|
/**
|
|
180
202
|
* The `<Ads />` resolution layer — the site's ad properties in, the exact tags to
|
|
181
203
|
* emit out.
|
|
@@ -239,6 +261,18 @@ function resolveAds(ads, ctx = {}) {
|
|
|
239
261
|
warnings.push(`ads: ${provider} ${platform.tag.needs} "${id}" is not a valid ID. Skipped.`);
|
|
240
262
|
continue;
|
|
241
263
|
}
|
|
264
|
+
let safeEntry = entry;
|
|
265
|
+
const labels = normalizeConversionLabels(entry.conversionLabel);
|
|
266
|
+
const invalidActions = Object.entries(labels).filter(([, label]) => label && !SAFE_ID.test(label)).map(([action]) => action);
|
|
267
|
+
if (invalidActions.length) {
|
|
268
|
+
for (const action of invalidActions) warnings.push(`ads: ${provider} conversionLabel.${action} "${labels[action]}" is not a valid label. Conversion firing skipped for that action.`);
|
|
269
|
+
const cleanLabels = Object.fromEntries(Object.entries(labels).filter(([action]) => !invalidActions.includes(action)));
|
|
270
|
+
const { conversionLabel: _drop, ...rest } = entry;
|
|
271
|
+
safeEntry = Object.keys(cleanLabels).length ? {
|
|
272
|
+
...rest,
|
|
273
|
+
conversionLabel: cleanLabels
|
|
274
|
+
} : rest;
|
|
275
|
+
}
|
|
242
276
|
const category = entry.category ?? "marketing";
|
|
243
277
|
tags.push({
|
|
244
278
|
key: provider,
|
|
@@ -250,7 +284,7 @@ function resolveAds(ads, ctx = {}) {
|
|
|
250
284
|
gated: ctx.consent === true,
|
|
251
285
|
inline: platform.tag.bootstrap(id, {
|
|
252
286
|
provider,
|
|
253
|
-
...
|
|
287
|
+
...safeEntry
|
|
254
288
|
}),
|
|
255
289
|
src: platform.tag.src ? platform.tag.src(id) : null,
|
|
256
290
|
attrs: {}
|
|
@@ -273,8 +307,7 @@ function resolveAds(ads, ctx = {}) {
|
|
|
273
307
|
};
|
|
274
308
|
}
|
|
275
309
|
|
|
276
|
-
|
|
277
|
-
//#region src/fonts.ts
|
|
310
|
+
// src/fonts.ts
|
|
278
311
|
/**
|
|
279
312
|
* Provider names, matching `fontProviders`' keys 1:1 (verified against astro@7.1.2).
|
|
280
313
|
* The generator's `SiteFontSchema` enumerates the same set, so a mismatch is a
|
|
@@ -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,8 +359,93 @@ function resolveFonts(decls) {
|
|
|
326
359
|
};
|
|
327
360
|
}
|
|
328
361
|
|
|
329
|
-
|
|
330
|
-
|
|
362
|
+
// src/i18n.ts
|
|
363
|
+
function codesOf(entry) {
|
|
364
|
+
return typeof entry === "string" ? [entry] : entry.codes;
|
|
365
|
+
}
|
|
366
|
+
/** Every BCP-47 code Astro's `i18n` block actually recognises. */
|
|
367
|
+
function astroLocaleCodes(i18n) {
|
|
368
|
+
return new Set(i18n.locales.flatMap(codesOf));
|
|
369
|
+
}
|
|
370
|
+
/**
|
|
371
|
+
* Warning strings for a mismatch — `[]` when either side is absent (nothing
|
|
372
|
+
* to reconcile) or the two agree.
|
|
373
|
+
*/
|
|
374
|
+
function localeConsistencyWarnings(site, i18n) {
|
|
375
|
+
if (!site || !i18n) return [];
|
|
376
|
+
const warnings = [];
|
|
377
|
+
const astroCodes = astroLocaleCodes(i18n);
|
|
378
|
+
const siteCodes = new Set(Object.keys(site.locales));
|
|
379
|
+
const onlyInSite = [...siteCodes].filter((c) => !astroCodes.has(c));
|
|
380
|
+
const onlyInAstro = [...astroCodes].filter((c) => !siteCodes.has(c));
|
|
381
|
+
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.");
|
|
382
|
+
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.`);
|
|
383
|
+
return warnings;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// src/seo-defaults.ts
|
|
387
|
+
/** Drop `undefined`/empty-string keys so the result carries only what the config actually states. */
|
|
388
|
+
function compact(value) {
|
|
389
|
+
return Object.fromEntries(Object.entries(value).filter(([, v]) => v !== void 0 && v !== ""));
|
|
390
|
+
}
|
|
391
|
+
function resolveSeoDefaults(config, options = {}) {
|
|
392
|
+
const seo = config.site.seo;
|
|
393
|
+
const defaultLocale = config.site.defaultLocale;
|
|
394
|
+
const locale = options.locale ?? defaultLocale;
|
|
395
|
+
const text = (value) => t(value, locale, defaultLocale) || void 0;
|
|
396
|
+
const robots = config.site.environments?.[options.env ?? "production"]?.robots ?? seo?.robots;
|
|
397
|
+
const image = seo?.openGraph?.image;
|
|
398
|
+
const openGraph = compact({
|
|
399
|
+
type: seo?.openGraph?.type,
|
|
400
|
+
locale: seo?.openGraph?.locale,
|
|
401
|
+
localeAlternates: seo?.openGraph?.localeAlternates,
|
|
402
|
+
image: image === void 0 ? void 0 : typeof image === "string" ? { url: image } : compact({
|
|
403
|
+
url: image.src,
|
|
404
|
+
alt: image.alt,
|
|
405
|
+
width: image.width,
|
|
406
|
+
height: image.height,
|
|
407
|
+
type: image.type
|
|
408
|
+
})
|
|
409
|
+
});
|
|
410
|
+
const twitter = compact({
|
|
411
|
+
card: seo?.twitter?.card,
|
|
412
|
+
site: seo?.twitter?.site,
|
|
413
|
+
creator: seo?.twitter?.creator
|
|
414
|
+
});
|
|
415
|
+
return compact({
|
|
416
|
+
siteName: text(seo?.siteName) ?? resolveOrganizationName(config, locale),
|
|
417
|
+
titleTemplate: text(seo?.titleTemplate),
|
|
418
|
+
defaultTitle: text(seo?.defaultTitle),
|
|
419
|
+
defaultDescription: text(seo?.defaultDescription),
|
|
420
|
+
robots,
|
|
421
|
+
googleSiteVerification: seo?.googleSiteVerification,
|
|
422
|
+
bingSiteVerification: seo?.bingSiteVerification,
|
|
423
|
+
...Object.keys(openGraph).length ? { openGraph } : {},
|
|
424
|
+
...Object.keys(twitter).length ? { twitter } : {}
|
|
425
|
+
});
|
|
426
|
+
}
|
|
427
|
+
/**
|
|
428
|
+
* Merge `vitops({ seo })` over config-derived defaults. The two nested objects merge key by key, so
|
|
429
|
+
* setting `openGraph.locale` in code doesn't discard the config's fallback image.
|
|
430
|
+
*/
|
|
431
|
+
function mergeSeoDefaults(fromConfig, override) {
|
|
432
|
+
if (!override) return fromConfig;
|
|
433
|
+
const merged = {
|
|
434
|
+
...fromConfig,
|
|
435
|
+
...override
|
|
436
|
+
};
|
|
437
|
+
if (fromConfig.openGraph || override.openGraph) merged.openGraph = {
|
|
438
|
+
...fromConfig.openGraph,
|
|
439
|
+
...override.openGraph
|
|
440
|
+
};
|
|
441
|
+
if (fromConfig.twitter || override.twitter) merged.twitter = {
|
|
442
|
+
...fromConfig.twitter,
|
|
443
|
+
...override.twitter
|
|
444
|
+
};
|
|
445
|
+
return merged;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// src/integration.ts
|
|
331
449
|
/**
|
|
332
450
|
* @getvitops/astro — the Astro integration. One entry wires the design system's
|
|
333
451
|
* `<head>` contributions into a consumer site:
|
|
@@ -367,6 +485,39 @@ function undisclosedProviders(configPath, analytics) {
|
|
|
367
485
|
return Object.keys(SITE_CONFIG_KEYS).filter((key) => analytics[key] !== void 0 && !declared[SITE_CONFIG_KEYS[key]]);
|
|
368
486
|
}
|
|
369
487
|
/**
|
|
488
|
+
* Read `site.locales`/`site.defaultLocale` out of the site config, for
|
|
489
|
+
* `localeConsistencyWarnings` to check against Astro's own `i18n` block.
|
|
490
|
+
*
|
|
491
|
+
* Best-effort, matching `readSiteAds`'s shape exactly: a malformed or missing
|
|
492
|
+
* config is silence here, since every other consumer of this file already
|
|
493
|
+
* validates it loudly elsewhere in the same hook.
|
|
494
|
+
*/
|
|
495
|
+
function readSiteLocales(configPath, siteEnv) {
|
|
496
|
+
try {
|
|
497
|
+
const cfg = resolveConfig(JSON.parse(readFileSync(configPath, "utf8")), siteEnv);
|
|
498
|
+
return {
|
|
499
|
+
locales: cfg.site.locales,
|
|
500
|
+
defaultLocale: cfg.site.defaultLocale
|
|
501
|
+
};
|
|
502
|
+
} catch {
|
|
503
|
+
return null;
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
/**
|
|
507
|
+
* Resolve `site.seo` (plus `organization.name` as the site-name fallback) out of the site config.
|
|
508
|
+
*
|
|
509
|
+
* Best-effort, like `readSiteLocales`: a malformed or missing config is silence here, since every
|
|
510
|
+
* other consumer of the file validates it loudly in the same hook. An empty object means "the
|
|
511
|
+
* config states no SEO defaults", which is also what no site config at all means.
|
|
512
|
+
*/
|
|
513
|
+
function readSiteSeo(configPath, siteEnv) {
|
|
514
|
+
try {
|
|
515
|
+
return resolveSeoDefaults(resolveConfig(JSON.parse(readFileSync(configPath, "utf8")), siteEnv), { env: siteEnv });
|
|
516
|
+
} catch {
|
|
517
|
+
return {};
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
/**
|
|
370
521
|
* Read `site.ads` and this environment's ads switch out of the site config.
|
|
371
522
|
*
|
|
372
523
|
* Best-effort, like `undisclosedProviders`: every other consumer of this file
|
|
@@ -395,6 +546,55 @@ function readSiteAds(configPath, siteEnv) {
|
|
|
395
546
|
}
|
|
396
547
|
}
|
|
397
548
|
/**
|
|
549
|
+
* Read `site.experiments` + `site.analytics.matomo.abVariantDimensions` out of
|
|
550
|
+
* the site config, resolving each experiment's cookie name (default
|
|
551
|
+
* `_ab_<key>`, matching `vitopsAbTesting()`'s own default) so `<Analytics />`
|
|
552
|
+
* doesn't need to know the fallback rule itself.
|
|
553
|
+
*
|
|
554
|
+
* Mirrors `readSiteAds`'s shape: best-effort, only enabled experiments
|
|
555
|
+
* (a disabled one has no cookie to read and shouldn't cost every provider's
|
|
556
|
+
* bootstrap a no-op check), `null` when nothing's configured.
|
|
557
|
+
*/
|
|
558
|
+
function readSiteExperiments(configPath, siteEnv) {
|
|
559
|
+
try {
|
|
560
|
+
const cfg = resolveConfig(JSON.parse(readFileSync(configPath, "utf8")), siteEnv);
|
|
561
|
+
const configured = cfg.site.experiments;
|
|
562
|
+
if (!configured || Object.keys(configured).length === 0) return null;
|
|
563
|
+
const experiments = {};
|
|
564
|
+
let primaryCookie = null;
|
|
565
|
+
for (const [key, experiment] of Object.entries(configured)) {
|
|
566
|
+
if (!experiment.enabled) continue;
|
|
567
|
+
const cookieName = experiment.cookieName ?? `_ab_${key}`;
|
|
568
|
+
experiments[key] = { cookieName };
|
|
569
|
+
primaryCookie ??= cookieName;
|
|
570
|
+
}
|
|
571
|
+
if (Object.keys(experiments).length === 0) return null;
|
|
572
|
+
return {
|
|
573
|
+
experiments,
|
|
574
|
+
matomoAbVariantDimensions: cfg.site.analytics?.matomo?.abVariantDimensions ?? {},
|
|
575
|
+
primaryCookie
|
|
576
|
+
};
|
|
577
|
+
} catch {
|
|
578
|
+
return null;
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
/**
|
|
582
|
+
* Is `site.crm` configured and not explicitly disabled?
|
|
583
|
+
*
|
|
584
|
+
* Mirrors `readSiteAds`'s "read the config, decide nothing else" shape — this
|
|
585
|
+
* only answers the one question `hasConversionRoute`'s warning needs, not the
|
|
586
|
+
* whole `CrmConfig`, which the consumer's own route file builds from the same
|
|
587
|
+
* config however it likes.
|
|
588
|
+
*/
|
|
589
|
+
function siteCrmEnabled(configPath, siteEnv) {
|
|
590
|
+
try {
|
|
591
|
+
const cfg = resolveConfig(JSON.parse(readFileSync(configPath, "utf8")), siteEnv);
|
|
592
|
+
return Boolean(cfg.site.crm && cfg.site.crm.enabled !== false);
|
|
593
|
+
} catch {
|
|
594
|
+
return false;
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
/**
|
|
398
598
|
* The consent + tracking facts a site config already states.
|
|
399
599
|
*
|
|
400
600
|
* These were the one pair the integration did not read. `site.tracking` and
|
|
@@ -547,7 +747,12 @@ function vitops(opts = {}) {
|
|
|
547
747
|
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
748
|
const legalInput = opts.legal?.input ?? siteInput;
|
|
549
749
|
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.");
|
|
550
|
-
if (
|
|
750
|
+
if (siteInput) {
|
|
751
|
+
const siteLocales = readSiteLocales(resolve(root, siteInput), opts.site?.siteEnv ?? "production");
|
|
752
|
+
for (const warning of localeConsistencyWarnings(siteLocales, config.i18n)) logger.warn(warning);
|
|
753
|
+
}
|
|
754
|
+
const seoDefaults = mergeSeoDefaults(siteInput ? readSiteSeo(resolve(root, siteInput), opts.site?.siteEnv ?? "production") : {}, opts.seo);
|
|
755
|
+
if (opts.seo || Object.keys(seoDefaults).some((k) => k !== "siteName")) {
|
|
551
756
|
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
757
|
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.");
|
|
553
758
|
}
|
|
@@ -561,6 +766,7 @@ function vitops(opts = {}) {
|
|
|
561
766
|
const analytics = resolveAnalytics(opts.analytics, { consent: consentEnabled });
|
|
562
767
|
const trackingOpts = opts.tracking === true ? { enabled: true } : opts.tracking || (siteConsent?.tracking?.enabled ? siteConsent.tracking : void 0);
|
|
563
768
|
const adsConfig = siteInput ? readSiteAds(resolve(root, siteInput), opts.site?.siteEnv ?? "production") : null;
|
|
769
|
+
const experimentsConfig = siteInput ? readSiteExperiments(resolve(root, siteInput), opts.site?.siteEnv ?? "production") : null;
|
|
564
770
|
const detectedCategories = consentCategories([...analytics.tags, ...resolveAds(adsConfig?.properties, {
|
|
565
771
|
consent: consentEnabled,
|
|
566
772
|
enabled: true
|
|
@@ -576,6 +782,7 @@ function vitops(opts = {}) {
|
|
|
576
782
|
});
|
|
577
783
|
for (const warning of tracking.warnings) logger.warn(warning);
|
|
578
784
|
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.`);
|
|
785
|
+
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
786
|
const ads = resolveAds(adsConfig?.properties, {
|
|
580
787
|
consent: consentEnabled,
|
|
581
788
|
consentCategories: offeredCategories,
|
|
@@ -633,7 +840,8 @@ function vitops(opts = {}) {
|
|
|
633
840
|
}
|
|
634
841
|
const consentRuntime = consentEnabled || analytics.needsRuntime;
|
|
635
842
|
if (webComponents || consentRuntime) {
|
|
636
|
-
const
|
|
843
|
+
const corePkg = fileURLToPath(import.meta.resolve("@getvitops/core/package.json"));
|
|
844
|
+
const coreDist = join(dirname(corePkg), "dist");
|
|
637
845
|
const dest = join(publicDir, "vitops");
|
|
638
846
|
if (existsSync(coreDist)) {
|
|
639
847
|
const bundles = webComponents ? [
|
|
@@ -781,7 +989,7 @@ function vitops(opts = {}) {
|
|
|
781
989
|
sitemap: sitemapHref,
|
|
782
990
|
fonts: headFonts,
|
|
783
991
|
site: config.site ?? null,
|
|
784
|
-
seo:
|
|
992
|
+
seo: seoDefaults,
|
|
785
993
|
analytics: opts.analytics ?? {},
|
|
786
994
|
consent: consentEnabled,
|
|
787
995
|
consentCategories: offeredCategories,
|
|
@@ -790,8 +998,11 @@ function vitops(opts = {}) {
|
|
|
790
998
|
tracking: tracking.enabled ? {
|
|
791
999
|
enabled: true,
|
|
792
1000
|
category: tracking.category,
|
|
793
|
-
cookies: tracking.cookies
|
|
1001
|
+
cookies: tracking.cookies,
|
|
1002
|
+
abVariantCookie: experimentsConfig?.primaryCookie ?? null
|
|
794
1003
|
} : null,
|
|
1004
|
+
experiments: experimentsConfig?.experiments ?? {},
|
|
1005
|
+
matomoAbVariantDimensions: experimentsConfig?.matomoAbVariantDimensions ?? {},
|
|
795
1006
|
ads: adsConfig
|
|
796
1007
|
})] } });
|
|
797
1008
|
if (opts.css) {
|
|
@@ -837,8 +1048,7 @@ function vitops(opts = {}) {
|
|
|
837
1048
|
};
|
|
838
1049
|
}
|
|
839
1050
|
|
|
840
|
-
|
|
841
|
-
//#region src/lastmod.ts
|
|
1051
|
+
// src/lastmod.ts
|
|
842
1052
|
/**
|
|
843
1053
|
* Real per-page `<lastmod>` for the sitemap, derived from git.
|
|
844
1054
|
*
|
|
@@ -924,7 +1134,7 @@ async function commitDates(dirs, cwd) {
|
|
|
924
1134
|
...dirs
|
|
925
1135
|
], {
|
|
926
1136
|
cwd,
|
|
927
|
-
maxBuffer:
|
|
1137
|
+
maxBuffer: 67108864
|
|
928
1138
|
});
|
|
929
1139
|
let current;
|
|
930
1140
|
for (const line of stdout.split("\n")) {
|
|
@@ -997,8 +1207,7 @@ async function gitLastmod(options = {}) {
|
|
|
997
1207
|
};
|
|
998
1208
|
}
|
|
999
1209
|
|
|
1000
|
-
|
|
1001
|
-
//#region src/seo.ts
|
|
1210
|
+
// src/seo.ts
|
|
1002
1211
|
/** `en-CA` → `en_CA`; Open Graph wants the underscore form. */
|
|
1003
1212
|
const ogLocale = (locale) => locale.replace("-", "_");
|
|
1004
1213
|
/**
|
|
@@ -1119,6 +1328,5 @@ function resolveSeo(defaults, props, ctx) {
|
|
|
1119
1328
|
};
|
|
1120
1329
|
}
|
|
1121
1330
|
|
|
1122
|
-
|
|
1123
|
-
export { TRACKING_COOKIE, consentCategories, vitops as default, gitLastmod, resolveAds, resolveAnalytics, resolveSeo, resolveTracking, routeFromPage, slugFromContent };
|
|
1331
|
+
export { TRACKING_COOKIE, consentCategories, vitops as default, gitLastmod, mergeSeoDefaults, resolveAds, resolveAnalytics, resolveSeo, resolveSeoDefaults, resolveTracking, routeFromPage, slugFromContent };
|
|
1124
1332
|
//# sourceMappingURL=index.mjs.map
|