@getvitops/astro 4.0.0 → 4.1.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 +78 -0
- package/dist/index.d.mts +65 -5
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +144 -5
- package/dist/index.mjs.map +1 -1
- package/package.json +9 -5
- package/src/ads.ts +156 -0
- package/src/analytics.ts +22 -4
- package/src/components/Ads.astro +47 -0
- package/src/components/Analytics.astro +7 -30
- package/src/components/GatedTags.astro +46 -0
- package/src/scripts/tracking.ts +2 -0
- package/virtual.d.ts +6 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,83 @@
|
|
|
1
1
|
# @getvitops/astro
|
|
2
2
|
|
|
3
|
+
## 4.1.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 2c890c0: Link a site to its ad properties: `site.ads`, `vitops ads`, and `<Ads />`.
|
|
8
|
+
|
|
9
|
+
`vitops search` covers the whole Search Console relationship. Nothing covered the equivalent one
|
|
10
|
+
with ad platforms, and a site's ad accounts had nowhere to live — so a Meta pixel pasted into a
|
|
11
|
+
template was invisible to the rest of the toolchain. It set `_fbp` on a site whose generated cookie
|
|
12
|
+
notice never mentioned it, whose consent gate never cleared it on revoke, and — for LinkedIn and
|
|
13
|
+
Pinterest — whose attribution never captured the click ID at all, so every conversion from those
|
|
14
|
+
platforms arrived indistinguishable from organic traffic.
|
|
15
|
+
|
|
16
|
+
**New `site.ads` block**, keyed by platform (`google`, `meta`, `linkedin`, `reddit`, `tiktok`,
|
|
17
|
+
`microsoft`, `pinterest`, `snapchat`):
|
|
18
|
+
|
|
19
|
+
```jsonc
|
|
20
|
+
{
|
|
21
|
+
"site": {
|
|
22
|
+
"ads": {
|
|
23
|
+
"meta": { "pixelId": "123456789", "domainVerification": "abc123" },
|
|
24
|
+
"google": {
|
|
25
|
+
"accountId": "123-456-7890",
|
|
26
|
+
"pixelId": "AW-987654321",
|
|
27
|
+
"conversionLabel": "xyz",
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
}
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
**`vitops ads setup`** ensures each platform's domain-verification DNS record. Only four platforms
|
|
35
|
+
verify a domain at all — Meta, TikTok, Pinterest and Snapchat, by apex DNS TXT — and that record is
|
|
36
|
+
the one thing created for you (in Cloudflare, via `CLOUDFLARE_API_TOKEN`; created only, never edited
|
|
37
|
+
or deleted). Google Ads, LinkedIn, Reddit and Microsoft Ads have no domain verification: linking
|
|
38
|
+
there is the tag and the account id, and the run says so rather than skipping in silence. No
|
|
39
|
+
platform Marketing API is called — Meta's needs a system-user token and Google's an approved
|
|
40
|
+
developer token with your own account on the line — so the final "Verify" click is surfaced as a
|
|
41
|
+
reminder. `--dry` prints the plan, `--check` reports drift and exits non-zero.
|
|
42
|
+
|
|
43
|
+
**It asks for what the config is missing.** A verification token does not exist until someone
|
|
44
|
+
fetches it from the platform UI, so a first run prompts for it, naming the exact UI path, folds the
|
|
45
|
+
answer into the plan and writes it to your config. The token is not a secret — it is published in
|
|
46
|
+
DNS, and the platform fetching it back is the ownership proof, exactly like the IndexNow key.
|
|
47
|
+
Prompting requires a TTY: with `--dry`, `--check`, `--no-prompt`, or in CI, you get a named error
|
|
48
|
+
instead and the run never hangs. `--no-write` keeps the answer out of the config.
|
|
49
|
+
|
|
50
|
+
**`vitops ads tags`** prints each pixel as an inert, consent-gated `<script>` — `type="text/plain"`
|
|
51
|
+
with the library URL on `data-src`, so an undecided visitor's page issues no third-party request.
|
|
52
|
+
For Bricks, WordPress, Eleventy: any stack without the Astro integration.
|
|
53
|
+
|
|
54
|
+
**`vitops ads lint`** reports the gaps that are invisible at runtime: a click ID the platform stamps
|
|
55
|
+
that attribution does not capture, a pixel while `site.tracking` is off, a property with no tag id.
|
|
56
|
+
|
|
57
|
+
**`<Ads />` in `@getvitops/astro`**, a sibling of `<Analytics />` rather than part of it — ad
|
|
58
|
+
properties come from the site config, state their own consent category (`marketing` by default,
|
|
59
|
+
rather than being derived), and switch per environment on their own
|
|
60
|
+
(`environments.<env>.ads`, defaulting to `analytics`, then true, so a preview deployment can send
|
|
61
|
+
pageviews without firing conversion pixels). Both components now render through one `<GatedTags />`,
|
|
62
|
+
so the inert markup has a single implementation.
|
|
63
|
+
|
|
64
|
+
**Your cookie notice now discloses every configured pixel** — name, cookies and opt-out — from the
|
|
65
|
+
same table that writes `data-consent-cookies`, so the notice and the revoke cannot disagree. If you
|
|
66
|
+
add `site.ads` to a config whose privacy policy or cookie notice you have already published,
|
|
67
|
+
re-generate and re-read them.
|
|
68
|
+
|
|
69
|
+
`li_fat_id` (LinkedIn) and `epik` (Pinterest) join the click-ID capture vocabulary; a site running
|
|
70
|
+
either platform starts attributing conversions it previously recorded as organic.
|
|
71
|
+
|
|
72
|
+
### Patch Changes
|
|
73
|
+
|
|
74
|
+
- Updated dependencies [2c890c0]
|
|
75
|
+
- Updated dependencies [e764090]
|
|
76
|
+
- @getvitops/generator@4.1.0
|
|
77
|
+
- @getvitops/utils@4.1.0
|
|
78
|
+
- @getvitops/vite@4.1.0
|
|
79
|
+
- @getvitops/core@4.1.0
|
|
80
|
+
|
|
3
81
|
## 4.0.0
|
|
4
82
|
|
|
5
83
|
### Major Changes
|
package/dist/index.d.mts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { LegalOutput, SiteFont, StylesheetFormat } from "@getvitops/generator";
|
|
2
2
|
import { generateIconInclude } from "@getvitops/utils";
|
|
3
|
+
import { AdProvider } from "@getvitops/utils/ads";
|
|
3
4
|
import { OutputKind } from "@getvitops/utils/media";
|
|
4
5
|
import { AstroIntegration } from "astro";
|
|
5
6
|
export * from "@getvitops/utils";
|
|
@@ -102,9 +103,22 @@ interface GetvitopsConsentOptions {
|
|
|
102
103
|
/** Link the banner to the cookie notice (`vitops legal` renders one). */
|
|
103
104
|
policyUrl?: string;
|
|
104
105
|
}
|
|
105
|
-
/**
|
|
106
|
-
|
|
107
|
-
|
|
106
|
+
/**
|
|
107
|
+
* A third-party tag, fully resolved — everything `<GatedTags />` needs to render
|
|
108
|
+
* one, with nothing left to decide.
|
|
109
|
+
*
|
|
110
|
+
* Shared by `<Analytics />` and `<Ads />` on purpose: the inert-markup rule
|
|
111
|
+
* (`type="text/plain"`, the URL on `data-src`, never a live `src`) is the whole
|
|
112
|
+
* consent gate, and it must have exactly one implementation. What differs between
|
|
113
|
+
* the two is where the tag comes from and who decides its category, which is why
|
|
114
|
+
* they have separate resolvers over one shape rather than one resolver over two
|
|
115
|
+
* vocabularies.
|
|
116
|
+
*
|
|
117
|
+
* `key` is left to each resolver: analytics keys are its four providers, ads keys
|
|
118
|
+
* are `AdProvider`. Widening it into one union here would let a provider from
|
|
119
|
+
* either side satisfy a check meant for the other.
|
|
120
|
+
*/
|
|
121
|
+
interface GatedTag {
|
|
108
122
|
/** The provider as a reader would recognise it — used in warnings. */
|
|
109
123
|
provider: string;
|
|
110
124
|
category: ConsentCategory;
|
|
@@ -123,6 +137,10 @@ interface ResolvedTag {
|
|
|
123
137
|
src: string | null;
|
|
124
138
|
attrs: Record<string, string>;
|
|
125
139
|
}
|
|
140
|
+
/** One analytics tag. */
|
|
141
|
+
interface ResolvedTag extends GatedTag {
|
|
142
|
+
key: 'googleAnalytics' | 'clarity' | 'matomo' | 'plausible';
|
|
143
|
+
}
|
|
126
144
|
interface ResolvedAnalytics {
|
|
127
145
|
tags: ResolvedTag[];
|
|
128
146
|
/** Does anything here need `@getvitops/core/consent` loaded? */
|
|
@@ -143,7 +161,7 @@ interface ResolveAnalyticsContext {
|
|
|
143
161
|
*/
|
|
144
162
|
declare function resolveAnalytics(opts: GetvitopsAnalyticsOptions | undefined, ctx?: ResolveAnalyticsContext): ResolvedAnalytics;
|
|
145
163
|
/** Which optional categories a set of tags actually needs a visitor to decide. */
|
|
146
|
-
declare function consentCategories(tags:
|
|
164
|
+
declare function consentCategories(tags: GatedTag[]): OptionalConsentCategory[];
|
|
147
165
|
//#endregion
|
|
148
166
|
//#region src/seo.d.ts
|
|
149
167
|
/**
|
|
@@ -784,6 +802,48 @@ interface GetvitopsIconsOptions {
|
|
|
784
802
|
}
|
|
785
803
|
declare function vitops(opts?: GetvitopsOptions): AstroIntegration;
|
|
786
804
|
//#endregion
|
|
805
|
+
//#region src/ads.d.ts
|
|
806
|
+
/**
|
|
807
|
+
* One ad property, mirroring a `site.ads` entry field for field.
|
|
808
|
+
*
|
|
809
|
+
* Structural rather than `SiteAdProperty` imported from the generator, matching
|
|
810
|
+
* how `GetvitopsSeoOptions` and `GetvitopsTrackingOptions` already work: the
|
|
811
|
+
* component surface must stay usable by a consumer who passes a plain object.
|
|
812
|
+
*/
|
|
813
|
+
interface GetvitopsAdProperty {
|
|
814
|
+
accountId?: string;
|
|
815
|
+
pixelId?: string;
|
|
816
|
+
conversionLabel?: string;
|
|
817
|
+
/** Default `marketing`. `analytics` for a property used purely for measurement. */
|
|
818
|
+
category?: 'marketing' | 'analytics';
|
|
819
|
+
/** False keeps the property on record without emitting its tag. */
|
|
820
|
+
enabled?: boolean;
|
|
821
|
+
}
|
|
822
|
+
type GetvitopsAdsOptions = Partial<Record<AdProvider, GetvitopsAdProperty>>;
|
|
823
|
+
/** One ad tag. `key` is the platform, so a caller can find one without matching prose. */
|
|
824
|
+
interface ResolvedAdTag extends GatedTag {
|
|
825
|
+
key: AdProvider;
|
|
826
|
+
}
|
|
827
|
+
interface ResolvedAds {
|
|
828
|
+
tags: ResolvedAdTag[];
|
|
829
|
+
/** Does anything here need `@getvitops/core/consent` loaded? */
|
|
830
|
+
needsRuntime: boolean;
|
|
831
|
+
/** Configuration problems, as prose. The integration logs these. */
|
|
832
|
+
warnings: string[];
|
|
833
|
+
}
|
|
834
|
+
interface ResolveAdsContext {
|
|
835
|
+
/** Is the consent gate enabled on this site (`vitops({ consent })`)? */
|
|
836
|
+
consent?: boolean;
|
|
837
|
+
/** Categories the banner offers, so we can catch a demand with no row. */
|
|
838
|
+
consentCategories?: readonly string[];
|
|
839
|
+
/** `environments.<env>.ads` — false switches every tag off for this build. */
|
|
840
|
+
enabled?: boolean;
|
|
841
|
+
/** When the tags load. Default `idle`: a pixel is never worth the critical path. */
|
|
842
|
+
strategy?: AnalyticsStrategy;
|
|
843
|
+
}
|
|
844
|
+
/** Resolve the configured ad properties into the tags `<Ads />` emits. */
|
|
845
|
+
declare function resolveAds(ads: GetvitopsAdsOptions | undefined, ctx?: ResolveAdsContext): ResolvedAds;
|
|
846
|
+
//#endregion
|
|
787
847
|
//#region src/lastmod.d.ts
|
|
788
848
|
interface GitLastmodOptions {
|
|
789
849
|
/** Directories to read history from (default `src/pages`, `src/content`, `src/data`). */
|
|
@@ -813,5 +873,5 @@ declare function slugFromContent(file: string): string | undefined;
|
|
|
813
873
|
*/
|
|
814
874
|
declare function gitLastmod(options?: GitLastmodOptions): Promise<(item: GetvitopsSitemapEntry) => GetvitopsSitemapEntry>;
|
|
815
875
|
//#endregion
|
|
816
|
-
export { type AnalyticsStrategy, type ClarityOptions, type ConsentCategory, 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 ResolveAnalyticsContext, type ResolveTrackingContext, type ResolvedAnalytics, type ResolvedSeo, type ResolvedTag, type ResolvedTracking, type SeoContext, type SeoNameTag, type SeoOgType, type SeoPropertyTag, type SeoProps, type SeoTwitterCard, TRACKING_COOKIE, type TrackingCategory, consentCategories, vitops as default, gitLastmod, resolveAnalytics, resolveSeo, resolveTracking, routeFromPage, slugFromContent };
|
|
876
|
+
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, consentCategories, vitops as default, gitLastmod, resolveAds, resolveAnalytics, resolveSeo, resolveTracking, routeFromPage, slugFromContent };
|
|
817
877
|
//# 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/tracking.ts","../src/integration.ts","../src/lastmod.ts"],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/analytics.ts","../src/seo.ts","../src/tracking.ts","../src/integration.ts","../src/ads.ts","../src/lastmod.ts"],"mappings":"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"}
|
package/dist/index.mjs
CHANGED
|
@@ -4,6 +4,7 @@ import { fileURLToPath } from "node:url";
|
|
|
4
4
|
import { isConfig, resolveConfig } from "@getvitops/generator";
|
|
5
5
|
import { collectIconRefs, faviconLinks, generateFavicons, generateIconInclude, resolveIcon, scanFiles, writeFaviconManifest } from "@getvitops/utils";
|
|
6
6
|
import vitopsPlugin from "@getvitops/vite";
|
|
7
|
+
import { AD_PLATFORMS, AD_PROVIDER_KEYS } from "@getvitops/utils/ads";
|
|
7
8
|
import { fontProviders } from "astro/config";
|
|
8
9
|
import { execFile } from "node:child_process";
|
|
9
10
|
import { promisify } from "node:util";
|
|
@@ -17,9 +18,9 @@ export * from "@getvitops/utils"
|
|
|
17
18
|
* the guard is one regex and the cost of not having it is arbitrary script
|
|
18
19
|
* injection from a config file.
|
|
19
20
|
*/
|
|
20
|
-
const SAFE_ID = /^[\w.-]+$/;
|
|
21
|
+
const SAFE_ID$1 = /^[\w.-]+$/;
|
|
21
22
|
function bad(value) {
|
|
22
|
-
return !SAFE_ID.test(value);
|
|
23
|
+
return !SAFE_ID$1.test(value);
|
|
23
24
|
}
|
|
24
25
|
/** JSON is a subset of JS object-literal syntax; `<` can't appear unescaped. */
|
|
25
26
|
function literal(value) {
|
|
@@ -172,6 +173,105 @@ function consentCategories(tags) {
|
|
|
172
173
|
].filter((c) => used.has(c));
|
|
173
174
|
}
|
|
174
175
|
|
|
176
|
+
//#endregion
|
|
177
|
+
//#region src/ads.ts
|
|
178
|
+
/**
|
|
179
|
+
* The `<Ads />` resolution layer — the site's ad properties in, the exact tags to
|
|
180
|
+
* emit out.
|
|
181
|
+
*
|
|
182
|
+
* A sibling of `analytics.ts`, deliberately not part of it. The two render the
|
|
183
|
+
* same gated markup, but they answer different questions and four things differ:
|
|
184
|
+
*
|
|
185
|
+
* - **Where they come from.** Analytics providers are integration options
|
|
186
|
+
* (`vitops({ analytics })`); ad properties are `site.ads` in the site config,
|
|
187
|
+
* because the ad account is a fact about the site that `vitops ads setup`,
|
|
188
|
+
* `vitops ads lint` and the generated cookie notice all read too.
|
|
189
|
+
* - **Who decides the consent category.** `resolveAnalytics` *derives* it from
|
|
190
|
+
* whether the provider sets cookies, so a consumer cannot mark Google Analytics
|
|
191
|
+
* `necessary` to dodge a banner. Every ad pixel sets cookies and is advertising,
|
|
192
|
+
* so the answer is `marketing` unless the config says `analytics` — a stated
|
|
193
|
+
* fact, not a derivation.
|
|
194
|
+
* - **What the key means.** `ResolvedTag['key']` is the four analytics providers,
|
|
195
|
+
* and `analytics.ts` pairs that set against the generator's processor table.
|
|
196
|
+
* Widening it to eight ad platforms would make that check answer for tags it
|
|
197
|
+
* does not govern.
|
|
198
|
+
* - **When they fire.** Analytics is gated per environment by
|
|
199
|
+
* `environments.<env>.analytics`; a preview deployment sending pageviews is
|
|
200
|
+
* survivable, one firing conversion pixels is not, so ads gets its own switch.
|
|
201
|
+
*
|
|
202
|
+
* Everything decidable is here and unit-tested; `Ads.astro` renders what this
|
|
203
|
+
* returns and chooses nothing.
|
|
204
|
+
*/
|
|
205
|
+
/**
|
|
206
|
+
* IDs land inside an inline `<script>`, so they are checked rather than trusted —
|
|
207
|
+
* the same guard, for the same reason, as `analytics.ts`'s `SAFE_ID`. A `<\/script>`
|
|
208
|
+
* in a pixel ID is not a plausible accident; arbitrary script injection from a
|
|
209
|
+
* config file is not a plausible thing to allow either.
|
|
210
|
+
*/
|
|
211
|
+
const SAFE_ID = /^[\w.-]+$/;
|
|
212
|
+
/** Resolve the configured ad properties into the tags `<Ads />` emits. */
|
|
213
|
+
function resolveAds(ads, ctx = {}) {
|
|
214
|
+
const warnings = [];
|
|
215
|
+
const tags = [];
|
|
216
|
+
const strategy = ctx.strategy ?? "idle";
|
|
217
|
+
const configured = AD_PROVIDER_KEYS.filter((p) => ads?.[p] != null);
|
|
218
|
+
if (configured.length === 0) return {
|
|
219
|
+
tags,
|
|
220
|
+
needsRuntime: false,
|
|
221
|
+
warnings
|
|
222
|
+
};
|
|
223
|
+
if (ctx.enabled === false) return {
|
|
224
|
+
tags,
|
|
225
|
+
needsRuntime: false,
|
|
226
|
+
warnings
|
|
227
|
+
};
|
|
228
|
+
for (const provider of configured) {
|
|
229
|
+
const entry = ads[provider];
|
|
230
|
+
if (entry.enabled === false) continue;
|
|
231
|
+
const platform = AD_PLATFORMS[provider];
|
|
232
|
+
const id = entry[platform.tag.needs];
|
|
233
|
+
if (!id) {
|
|
234
|
+
warnings.push(`ads: ${provider} has no ${platform.tag.needs}, so no tag is emitted. Run \`vitops ads setup\` to be prompted for it.`);
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
237
|
+
if (!SAFE_ID.test(id)) {
|
|
238
|
+
warnings.push(`ads: ${provider} ${platform.tag.needs} "${id}" is not a valid ID. Skipped.`);
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
241
|
+
const category = entry.category ?? "marketing";
|
|
242
|
+
tags.push({
|
|
243
|
+
key: provider,
|
|
244
|
+
provider: platform.name,
|
|
245
|
+
category,
|
|
246
|
+
setsCookies: true,
|
|
247
|
+
cookies: platform.tag.cookies,
|
|
248
|
+
strategy,
|
|
249
|
+
gated: ctx.consent === true,
|
|
250
|
+
inline: platform.tag.bootstrap(id, {
|
|
251
|
+
provider,
|
|
252
|
+
...entry
|
|
253
|
+
}),
|
|
254
|
+
src: platform.tag.src ? platform.tag.src(id) : null,
|
|
255
|
+
attrs: {}
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
if (tags.length === 0) return {
|
|
259
|
+
tags,
|
|
260
|
+
needsRuntime: false,
|
|
261
|
+
warnings
|
|
262
|
+
};
|
|
263
|
+
if (ctx.consent !== true) warnings.push(`ads: ${tags.map((t) => t.provider).join(", ")} ${tags.length === 1 ? "sets" : "set"} advertising cookies, and \`consent\` is off — so they load for every visitor with no way to decline. Set \`consent: true\`.`);
|
|
264
|
+
else {
|
|
265
|
+
const missing = [...new Set(tags.map((t) => t.category))].filter((c) => ctx.consentCategories && !ctx.consentCategories.includes(c));
|
|
266
|
+
if (missing.length) warnings.push(`ads: the tags demand the ${missing.join(", ")} consent categor${missing.length === 1 ? "y" : "ies"}, but the banner offers ${ctx.consentCategories?.join(", ") || "nothing"}. Add ${missing.join(", ")} to \`consent.categories\`, or the tags wait on a question the banner never asks.`);
|
|
267
|
+
}
|
|
268
|
+
return {
|
|
269
|
+
tags,
|
|
270
|
+
needsRuntime: true,
|
|
271
|
+
warnings
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
|
|
175
275
|
//#endregion
|
|
176
276
|
//#region src/fonts.ts
|
|
177
277
|
/**
|
|
@@ -298,6 +398,34 @@ function undisclosedProviders(configPath, analytics) {
|
|
|
298
398
|
}
|
|
299
399
|
return Object.keys(SITE_CONFIG_KEYS).filter((key) => analytics[key] !== void 0 && !declared[SITE_CONFIG_KEYS[key]]);
|
|
300
400
|
}
|
|
401
|
+
/**
|
|
402
|
+
* Read `site.ads` and this environment's ads switch out of the site config.
|
|
403
|
+
*
|
|
404
|
+
* Best-effort, like `undisclosedProviders`: every other consumer of this file
|
|
405
|
+
* validates it loudly elsewhere in the same hook, and a second copy of that error
|
|
406
|
+
* helps nobody. `resolveConfig` is used rather than a bare `JSON.parse` so an A/B
|
|
407
|
+
* variant's `overrides` are applied first — a variant that changes which pixels
|
|
408
|
+
* run is exactly the kind of thing overrides exist for.
|
|
409
|
+
*
|
|
410
|
+
* The environment switch cascades `ads` → `analytics` → true. An environment that
|
|
411
|
+
* has already said "no analytics here" has said the interesting half of it; making
|
|
412
|
+
* a consumer repeat themselves to stop conversion pixels would mean a preview
|
|
413
|
+
* deploy fires them by default, which is the wrong way for this to fail.
|
|
414
|
+
*/
|
|
415
|
+
function readSiteAds(configPath, siteEnv) {
|
|
416
|
+
try {
|
|
417
|
+
const cfg = resolveConfig(JSON.parse(readFileSync(configPath, "utf8")), siteEnv);
|
|
418
|
+
const properties = cfg.site.ads;
|
|
419
|
+
if (!properties || Object.keys(properties).length === 0) return null;
|
|
420
|
+
const env = cfg.site.environments?.[siteEnv];
|
|
421
|
+
return {
|
|
422
|
+
enabled: env?.ads ?? env?.analytics ?? true,
|
|
423
|
+
properties
|
|
424
|
+
};
|
|
425
|
+
} catch {
|
|
426
|
+
return null;
|
|
427
|
+
}
|
|
428
|
+
}
|
|
301
429
|
const VIRTUAL_ID = "virtual:getvitops/head";
|
|
302
430
|
const RESOLVED_ID = `\0${VIRTUAL_ID}`;
|
|
303
431
|
function virtualHeadPlugin(data) {
|
|
@@ -392,7 +520,11 @@ function vitops(opts = {}) {
|
|
|
392
520
|
const consentOpts = typeof opts.consent === "object" ? opts.consent : {};
|
|
393
521
|
const analytics = resolveAnalytics(opts.analytics, { consent: consentEnabled });
|
|
394
522
|
const trackingOpts = opts.tracking === true ? { enabled: true } : opts.tracking || void 0;
|
|
395
|
-
const
|
|
523
|
+
const adsConfig = siteInput ? readSiteAds(resolve(root, siteInput), opts.site?.siteEnv ?? "production") : null;
|
|
524
|
+
const detectedCategories = consentCategories([...analytics.tags, ...resolveAds(adsConfig?.properties, {
|
|
525
|
+
consent: consentEnabled,
|
|
526
|
+
enabled: true
|
|
527
|
+
}).tags]);
|
|
396
528
|
const defaultCategories = ["analytics", "preferences"];
|
|
397
529
|
const trackingCategory = trackingOpts?.enabled ? trackingOpts.category ?? "marketing" : null;
|
|
398
530
|
if (detectedCategories.includes("marketing") || trackingCategory === "marketing") defaultCategories.push("marketing");
|
|
@@ -403,6 +535,12 @@ function vitops(opts = {}) {
|
|
|
403
535
|
consentCategories: offeredCategories
|
|
404
536
|
});
|
|
405
537
|
for (const warning of tracking.warnings) logger.warn(warning);
|
|
538
|
+
const ads = resolveAds(adsConfig?.properties, {
|
|
539
|
+
consent: consentEnabled,
|
|
540
|
+
consentCategories: offeredCategories,
|
|
541
|
+
enabled: adsConfig?.enabled ?? true
|
|
542
|
+
});
|
|
543
|
+
for (const warning of ads.warnings) logger.warn(warning);
|
|
406
544
|
if (opts.analytics && legalInput) {
|
|
407
545
|
const undisclosed = undisclosedProviders(resolve(root, legalInput), opts.analytics);
|
|
408
546
|
if (undisclosed.length) logger.warn(`analytics: ${undisclosed.join(", ")} ${undisclosed.length === 1 ? "is" : "are"} configured here but absent from your site config's \`analytics\` block, so the cookie notice \`legal\` generates will not disclose ${undisclosed.length === 1 ? "it" : "them"}. Add ${undisclosed.map((k) => SITE_CONFIG_KEYS[k]).join(", ")} to ${legalInput}.`);
|
|
@@ -605,7 +743,8 @@ function vitops(opts = {}) {
|
|
|
605
743
|
enabled: true,
|
|
606
744
|
category: tracking.category,
|
|
607
745
|
cookies: tracking.cookies
|
|
608
|
-
} : null
|
|
746
|
+
} : null,
|
|
747
|
+
ads: adsConfig
|
|
609
748
|
})] } });
|
|
610
749
|
if (opts.css) {
|
|
611
750
|
const format = opts.css.format ?? "tailwind";
|
|
@@ -933,5 +1072,5 @@ function resolveSeo(defaults, props, ctx) {
|
|
|
933
1072
|
}
|
|
934
1073
|
|
|
935
1074
|
//#endregion
|
|
936
|
-
export { TRACKING_COOKIE, consentCategories, vitops as default, gitLastmod, resolveAnalytics, resolveSeo, resolveTracking, routeFromPage, slugFromContent };
|
|
1075
|
+
export { TRACKING_COOKIE, consentCategories, vitops as default, gitLastmod, resolveAds, resolveAnalytics, resolveSeo, resolveTracking, routeFromPage, slugFromContent };
|
|
937
1076
|
//# sourceMappingURL=index.mjs.map
|