@getvitops/astro 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 +395 -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/Marquee.astro +12 -2
- 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
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
|
-
import { i as TRACKING_COOKIE, n as ResolveTrackingContext, o as TrackingCategory, r as ResolvedTracking, s as resolveTracking, t as GetvitopsTrackingOptions } from "./tracking-
|
|
1
|
+
import { i as TRACKING_COOKIE, n as ResolveTrackingContext, o as TrackingCategory, r as ResolvedTracking, s as resolveTracking, t as GetvitopsTrackingOptions } from "./tracking-BNKKBqOe.mjs";
|
|
2
2
|
import { LegalOutput, SiteFont, StylesheetFormat } from "@getvitops/generator";
|
|
3
3
|
import { generateIconInclude } from "@getvitops/utils";
|
|
4
|
-
import { AdProvider } from "@getvitops/utils/ads";
|
|
4
|
+
import { AdProvider, ConversionLabelMap } from "@getvitops/utils/ads";
|
|
5
5
|
import { OutputKind } from "@getvitops/utils/media";
|
|
6
6
|
import { AstroIntegration } from "astro";
|
|
7
7
|
export * from "@getvitops/utils";
|
|
8
|
-
|
|
9
8
|
//#region src/analytics.d.ts
|
|
10
9
|
/**
|
|
11
10
|
* The `<Analytics />` resolution layer — provider config in, the exact tags to
|
|
@@ -152,6 +151,19 @@ interface ResolvedAnalytics {
|
|
|
152
151
|
interface ResolveAnalyticsContext {
|
|
153
152
|
/** Is the consent gate enabled on this site (`vitops({ consent })`)? */
|
|
154
153
|
consent?: boolean;
|
|
154
|
+
/**
|
|
155
|
+
* Enabled `site.experiments` entries, keyed the same way, each carrying the
|
|
156
|
+
* resolved cookie name `vitopsAbTesting()`'s middleware assigns
|
|
157
|
+
* (`_ab_<key>` unless overridden). When set, each provider's bootstrap
|
|
158
|
+
* reads these cookies client-side and reports the assignment via that
|
|
159
|
+
* provider's own custom-dimension/property mechanism — a no-op when this
|
|
160
|
+
* is empty, so a site not using experiments sees no behavior change.
|
|
161
|
+
*/
|
|
162
|
+
experiments?: Record<string, {
|
|
163
|
+
cookieName: string;
|
|
164
|
+
}>;
|
|
165
|
+
/** `site.analytics.matomo.abVariantDimensions` — Matomo's pre-created custom-dimension index per experiment key. */
|
|
166
|
+
matomoAbVariantDimensions?: Record<string, number>;
|
|
155
167
|
}
|
|
156
168
|
/**
|
|
157
169
|
* Resolve the configured providers into the tags to emit.
|
|
@@ -160,9 +172,9 @@ interface ResolveAnalyticsContext {
|
|
|
160
172
|
* malformed ID should not take down a build, and a missing tag is visible in the
|
|
161
173
|
* output while a failed build is just a red wall.
|
|
162
174
|
*/
|
|
163
|
-
declare function resolveAnalytics(opts: GetvitopsAnalyticsOptions | undefined, ctx?: ResolveAnalyticsContext): ResolvedAnalytics;
|
|
175
|
+
export declare function resolveAnalytics(opts: GetvitopsAnalyticsOptions | undefined, ctx?: ResolveAnalyticsContext): ResolvedAnalytics;
|
|
164
176
|
/** Which optional categories a set of tags actually needs a visitor to decide. */
|
|
165
|
-
declare function consentCategories(tags: GatedTag[]): OptionalConsentCategory[];
|
|
177
|
+
export declare function consentCategories(tags: GatedTag[]): OptionalConsentCategory[];
|
|
166
178
|
//#endregion
|
|
167
179
|
//#region src/seo.d.ts
|
|
168
180
|
/**
|
|
@@ -216,9 +228,13 @@ interface GetvitopsSeoOptions {
|
|
|
216
228
|
/** Bing Webmaster Tools verification token. */
|
|
217
229
|
bingSiteVerification?: string;
|
|
218
230
|
openGraph?: {
|
|
219
|
-
/** Default `og:type` (default `'website'`). */
|
|
220
|
-
|
|
221
|
-
|
|
231
|
+
/** Default `og:type` (default `'website'`). */
|
|
232
|
+
type?: SeoOgType;
|
|
233
|
+
/** `og:locale`. Written either way — `en-CA` and `en_CA` both emit `en_CA`. */
|
|
234
|
+
locale?: string;
|
|
235
|
+
/** Additional locales the same content exists in → `og:locale:alternate`. */
|
|
236
|
+
localeAlternates?: string[];
|
|
237
|
+
/** Fallback share image. Relative URLs resolve against `site`. */
|
|
222
238
|
image?: {
|
|
223
239
|
url: string;
|
|
224
240
|
alt?: string;
|
|
@@ -228,8 +244,11 @@ interface GetvitopsSeoOptions {
|
|
|
228
244
|
};
|
|
229
245
|
};
|
|
230
246
|
twitter?: {
|
|
231
|
-
/** Card type when no image resolves (default `'summary'`). */
|
|
232
|
-
|
|
247
|
+
/** Card type when no image resolves (default `'summary'`). */
|
|
248
|
+
card?: SeoTwitterCard;
|
|
249
|
+
/** The site's own handle, `@acme`. */
|
|
250
|
+
site?: string;
|
|
251
|
+
/** Default author handle, `@ada`. */
|
|
233
252
|
creator?: string;
|
|
234
253
|
};
|
|
235
254
|
}
|
|
@@ -341,7 +360,7 @@ interface SeoContext {
|
|
|
341
360
|
* through its three arguments, which is what makes it testable and what keeps the
|
|
342
361
|
* package free of the consumer-global coupling that sank the previous site model.
|
|
343
362
|
*/
|
|
344
|
-
declare function resolveSeo(defaults: GetvitopsSeoOptions, props: SeoProps, ctx: SeoContext): ResolvedSeo;
|
|
363
|
+
export declare function resolveSeo(defaults: GetvitopsSeoOptions, props: SeoProps, ctx: SeoContext): ResolvedSeo;
|
|
345
364
|
//#endregion
|
|
346
365
|
//#region src/integration.d.ts
|
|
347
366
|
interface GetvitopsFaviconOptions {
|
|
@@ -750,7 +769,7 @@ interface GetvitopsIconsOptions {
|
|
|
750
769
|
/** Aliases applied before the semantic map — legacy names, or values stored in CMS content. */
|
|
751
770
|
overrides?: Record<string, string>;
|
|
752
771
|
}
|
|
753
|
-
|
|
772
|
+
export default function vitops(opts?: GetvitopsOptions): AstroIntegration;
|
|
754
773
|
//#endregion
|
|
755
774
|
//#region src/ads.d.ts
|
|
756
775
|
/**
|
|
@@ -763,7 +782,15 @@ declare function vitops(opts?: GetvitopsOptions): AstroIntegration;
|
|
|
763
782
|
interface GetvitopsAdProperty {
|
|
764
783
|
accountId?: string;
|
|
765
784
|
pixelId?: string;
|
|
766
|
-
|
|
785
|
+
/**
|
|
786
|
+
* Google Ads only: the conversion action label(s) paired with `pixelId` (the
|
|
787
|
+
* `AW-…` conversion id) — a bare string (back-compat, treated as `call`) or a
|
|
788
|
+
* map of action → label. `<Tracking />`'s tel:/mailto: click handler and
|
|
789
|
+
* `vitopsFormConversion()` each fire `gtag('event', 'conversion', …)` for
|
|
790
|
+
* their own action at the moment of the interaction — see `gtagBootstrap` in
|
|
791
|
+
* `@getvitops/utils/ads`.
|
|
792
|
+
*/
|
|
793
|
+
conversionLabel?: string | ConversionLabelMap;
|
|
767
794
|
/** Default `marketing`. `analytics` for a property used purely for measurement. */
|
|
768
795
|
category?: 'marketing' | 'analytics';
|
|
769
796
|
/** False keeps the property on record without emitting its tag. */
|
|
@@ -792,7 +819,7 @@ interface ResolveAdsContext {
|
|
|
792
819
|
strategy?: AnalyticsStrategy;
|
|
793
820
|
}
|
|
794
821
|
/** Resolve the configured ad properties into the tags `<Ads />` emits. */
|
|
795
|
-
declare function resolveAds(ads: GetvitopsAdsOptions | undefined, ctx?: ResolveAdsContext): ResolvedAds;
|
|
822
|
+
export declare function resolveAds(ads: GetvitopsAdsOptions | undefined, ctx?: ResolveAdsContext): ResolvedAds;
|
|
796
823
|
//#endregion
|
|
797
824
|
//#region src/lastmod.d.ts
|
|
798
825
|
interface GitLastmodOptions {
|
|
@@ -810,9 +837,9 @@ interface GitLastmodOptions {
|
|
|
810
837
|
* commit date describes the template rather than any of them. Stamping every
|
|
811
838
|
* generated page with the template's date is exactly the "wrong date" case above.
|
|
812
839
|
*/
|
|
813
|
-
declare function routeFromPage(file: string): string | undefined;
|
|
840
|
+
export declare function routeFromPage(file: string): string | undefined;
|
|
814
841
|
/** The slug a content entry is most likely published under. */
|
|
815
|
-
declare function slugFromContent(file: string): string | undefined;
|
|
842
|
+
export declare function slugFromContent(file: string): string | undefined;
|
|
816
843
|
/**
|
|
817
844
|
* Build a `serialize` function that stamps `lastmod` from git history.
|
|
818
845
|
*
|
|
@@ -821,7 +848,7 @@ declare function slugFromContent(file: string): string | undefined;
|
|
|
821
848
|
* is dropped rather than guessed at, since picking either one would be a coin
|
|
822
849
|
* flip presented as a fact.
|
|
823
850
|
*/
|
|
824
|
-
declare function gitLastmod(options?: GitLastmodOptions): Promise<(item: GetvitopsSitemapEntry) => GetvitopsSitemapEntry>;
|
|
851
|
+
export declare function gitLastmod(options?: GitLastmodOptions): Promise<(item: GetvitopsSitemapEntry) => GetvitopsSitemapEntry>;
|
|
825
852
|
//#endregion
|
|
826
|
-
export { type AnalyticsStrategy, type ClarityOptions, type ConsentCategory, type GatedTag, type GetvitopsAdProperty, type GetvitopsAdsOptions, type GetvitopsAnalyticsOptions, type GetvitopsConsentOptions, type GetvitopsCssOptions, type GetvitopsFaviconOptions, type GetvitopsMediaOptions, type GetvitopsOptions, type GetvitopsSeoOptions, type GetvitopsSitemapChangeFreq, type GetvitopsSitemapEntry, type GetvitopsSitemapOptions, type GetvitopsTrackingOptions, type GitLastmodOptions, type GoogleAnalyticsOptions, type MatomoOptions, type OptionalConsentCategory, type PlausibleOptions, type ResolveAdsContext, type ResolveAnalyticsContext, type ResolveTrackingContext, type ResolvedAdTag, type ResolvedAds, type ResolvedAnalytics, type ResolvedSeo, type ResolvedTag, type ResolvedTracking, type SeoContext, type SeoNameTag, type SeoOgType, type SeoPropertyTag, type SeoProps, type SeoTwitterCard, TRACKING_COOKIE, type TrackingCategory,
|
|
853
|
+
export { type AnalyticsStrategy, type ClarityOptions, type ConsentCategory, type GatedTag, type GetvitopsAdProperty, type GetvitopsAdsOptions, type GetvitopsAnalyticsOptions, type GetvitopsConsentOptions, type GetvitopsCssOptions, type GetvitopsFaviconOptions, type GetvitopsMediaOptions, type GetvitopsOptions, type GetvitopsSeoOptions, type GetvitopsSitemapChangeFreq, type GetvitopsSitemapEntry, type GetvitopsSitemapOptions, type GetvitopsTrackingOptions, type GitLastmodOptions, type GoogleAnalyticsOptions, type MatomoOptions, type OptionalConsentCategory, type PlausibleOptions, type ResolveAdsContext, type ResolveAnalyticsContext, type ResolveTrackingContext, type ResolvedAdTag, type ResolvedAds, type ResolvedAnalytics, type ResolvedSeo, type ResolvedTag, type ResolvedTracking, type SeoContext, type SeoNameTag, type SeoOgType, type SeoPropertyTag, type SeoProps, type SeoTwitterCard, TRACKING_COOKIE, type TrackingCategory, resolveTracking };
|
|
827
854
|
//# sourceMappingURL=index.d.mts.map
|
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")) {
|