@getvitops/astro 4.0.0 → 5.0.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 +303 -0
- package/README.md +22 -0
- package/dist/exports/tracking.d.mts +3 -0
- package/dist/exports/tracking.mjs +5 -0
- package/dist/index.d.mts +66 -56
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +221 -41
- package/dist/index.mjs.map +1 -1
- package/dist/routes/conversion.d.mts +6 -3
- package/dist/routes/conversion.d.mts.map +1 -1
- package/dist/routes/conversion.mjs +21 -3
- package/dist/routes/conversion.mjs.map +1 -1
- package/dist/tracking-CmSfxWC3.mjs +47 -0
- package/dist/tracking-CmSfxWC3.mjs.map +1 -0
- package/dist/tracking-DdEKExlE.d.mts +65 -0
- package/dist/tracking-DdEKExlE.d.mts.map +1 -0
- package/package.json +15 -6
- 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/Cards.astro +38 -24
- package/src/components/GatedTags.astro +46 -0
- package/src/components/Subgrid.astro +39 -21
- package/src/components/Tree.astro +2 -2
- package/src/components/TreeNode.astro +1 -1
- package/src/exports/tracking.ts +26 -0
- package/src/scripts/tracking.ts +13 -1
- package/src/tracking.ts +13 -0
- package/virtual.d.ts +6 -0
package/dist/index.mjs
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
|
+
import { n as TRACKING_ENDPOINT, r as resolveTracking, t as TRACKING_COOKIE } from "./tracking-CmSfxWC3.mjs";
|
|
1
2
|
import { cpSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
3
|
import { dirname, join, relative, resolve } from "node:path";
|
|
3
4
|
import { fileURLToPath } from "node:url";
|
|
4
5
|
import { isConfig, resolveConfig } from "@getvitops/generator";
|
|
5
6
|
import { collectIconRefs, faviconLinks, generateFavicons, generateIconInclude, resolveIcon, scanFiles, writeFaviconManifest } from "@getvitops/utils";
|
|
6
7
|
import vitopsPlugin from "@getvitops/vite";
|
|
8
|
+
import { AD_PLATFORMS, AD_PROVIDER_KEYS } from "@getvitops/utils/ads";
|
|
7
9
|
import { fontProviders } from "astro/config";
|
|
8
10
|
import { execFile } from "node:child_process";
|
|
9
11
|
import { promisify } from "node:util";
|
|
@@ -17,9 +19,9 @@ export * from "@getvitops/utils"
|
|
|
17
19
|
* the guard is one regex and the cost of not having it is arbitrary script
|
|
18
20
|
* injection from a config file.
|
|
19
21
|
*/
|
|
20
|
-
const SAFE_ID = /^[\w.-]+$/;
|
|
22
|
+
const SAFE_ID$1 = /^[\w.-]+$/;
|
|
21
23
|
function bad(value) {
|
|
22
|
-
return !SAFE_ID.test(value);
|
|
24
|
+
return !SAFE_ID$1.test(value);
|
|
23
25
|
}
|
|
24
26
|
/** JSON is a subset of JS object-literal syntax; `<` can't appear unescaped. */
|
|
25
27
|
function literal(value) {
|
|
@@ -172,6 +174,105 @@ function consentCategories(tags) {
|
|
|
172
174
|
].filter((c) => used.has(c));
|
|
173
175
|
}
|
|
174
176
|
|
|
177
|
+
//#endregion
|
|
178
|
+
//#region src/ads.ts
|
|
179
|
+
/**
|
|
180
|
+
* The `<Ads />` resolution layer — the site's ad properties in, the exact tags to
|
|
181
|
+
* emit out.
|
|
182
|
+
*
|
|
183
|
+
* A sibling of `analytics.ts`, deliberately not part of it. The two render the
|
|
184
|
+
* same gated markup, but they answer different questions and four things differ:
|
|
185
|
+
*
|
|
186
|
+
* - **Where they come from.** Analytics providers are integration options
|
|
187
|
+
* (`vitops({ analytics })`); ad properties are `site.ads` in the site config,
|
|
188
|
+
* because the ad account is a fact about the site that `vitops ads setup`,
|
|
189
|
+
* `vitops ads lint` and the generated cookie notice all read too.
|
|
190
|
+
* - **Who decides the consent category.** `resolveAnalytics` *derives* it from
|
|
191
|
+
* whether the provider sets cookies, so a consumer cannot mark Google Analytics
|
|
192
|
+
* `necessary` to dodge a banner. Every ad pixel sets cookies and is advertising,
|
|
193
|
+
* so the answer is `marketing` unless the config says `analytics` — a stated
|
|
194
|
+
* fact, not a derivation.
|
|
195
|
+
* - **What the key means.** `ResolvedTag['key']` is the four analytics providers,
|
|
196
|
+
* and `analytics.ts` pairs that set against the generator's processor table.
|
|
197
|
+
* Widening it to eight ad platforms would make that check answer for tags it
|
|
198
|
+
* does not govern.
|
|
199
|
+
* - **When they fire.** Analytics is gated per environment by
|
|
200
|
+
* `environments.<env>.analytics`; a preview deployment sending pageviews is
|
|
201
|
+
* survivable, one firing conversion pixels is not, so ads gets its own switch.
|
|
202
|
+
*
|
|
203
|
+
* Everything decidable is here and unit-tested; `Ads.astro` renders what this
|
|
204
|
+
* returns and chooses nothing.
|
|
205
|
+
*/
|
|
206
|
+
/**
|
|
207
|
+
* IDs land inside an inline `<script>`, so they are checked rather than trusted —
|
|
208
|
+
* the same guard, for the same reason, as `analytics.ts`'s `SAFE_ID`. A `<\/script>`
|
|
209
|
+
* in a pixel ID is not a plausible accident; arbitrary script injection from a
|
|
210
|
+
* config file is not a plausible thing to allow either.
|
|
211
|
+
*/
|
|
212
|
+
const SAFE_ID = /^[\w.-]+$/;
|
|
213
|
+
/** Resolve the configured ad properties into the tags `<Ads />` emits. */
|
|
214
|
+
function resolveAds(ads, ctx = {}) {
|
|
215
|
+
const warnings = [];
|
|
216
|
+
const tags = [];
|
|
217
|
+
const strategy = ctx.strategy ?? "idle";
|
|
218
|
+
const configured = AD_PROVIDER_KEYS.filter((p) => ads?.[p] != null);
|
|
219
|
+
if (configured.length === 0) return {
|
|
220
|
+
tags,
|
|
221
|
+
needsRuntime: false,
|
|
222
|
+
warnings
|
|
223
|
+
};
|
|
224
|
+
if (ctx.enabled === false) return {
|
|
225
|
+
tags,
|
|
226
|
+
needsRuntime: false,
|
|
227
|
+
warnings
|
|
228
|
+
};
|
|
229
|
+
for (const provider of configured) {
|
|
230
|
+
const entry = ads[provider];
|
|
231
|
+
if (entry.enabled === false) continue;
|
|
232
|
+
const platform = AD_PLATFORMS[provider];
|
|
233
|
+
const id = entry[platform.tag.needs];
|
|
234
|
+
if (!id) {
|
|
235
|
+
warnings.push(`ads: ${provider} has no ${platform.tag.needs}, so no tag is emitted. Run \`vitops ads setup\` to be prompted for it.`);
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
if (!SAFE_ID.test(id)) {
|
|
239
|
+
warnings.push(`ads: ${provider} ${platform.tag.needs} "${id}" is not a valid ID. Skipped.`);
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
242
|
+
const category = entry.category ?? "marketing";
|
|
243
|
+
tags.push({
|
|
244
|
+
key: provider,
|
|
245
|
+
provider: platform.name,
|
|
246
|
+
category,
|
|
247
|
+
setsCookies: true,
|
|
248
|
+
cookies: platform.tag.cookies,
|
|
249
|
+
strategy,
|
|
250
|
+
gated: ctx.consent === true,
|
|
251
|
+
inline: platform.tag.bootstrap(id, {
|
|
252
|
+
provider,
|
|
253
|
+
...entry
|
|
254
|
+
}),
|
|
255
|
+
src: platform.tag.src ? platform.tag.src(id) : null,
|
|
256
|
+
attrs: {}
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
if (tags.length === 0) return {
|
|
260
|
+
tags,
|
|
261
|
+
needsRuntime: false,
|
|
262
|
+
warnings
|
|
263
|
+
};
|
|
264
|
+
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\`.`);
|
|
265
|
+
else {
|
|
266
|
+
const missing = [...new Set(tags.map((t) => t.category))].filter((c) => ctx.consentCategories && !ctx.consentCategories.includes(c));
|
|
267
|
+
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.`);
|
|
268
|
+
}
|
|
269
|
+
return {
|
|
270
|
+
tags,
|
|
271
|
+
needsRuntime: true,
|
|
272
|
+
warnings
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
|
|
175
276
|
//#endregion
|
|
176
277
|
//#region src/fonts.ts
|
|
177
278
|
/**
|
|
@@ -225,39 +326,6 @@ function resolveFonts(decls) {
|
|
|
225
326
|
};
|
|
226
327
|
}
|
|
227
328
|
|
|
228
|
-
//#endregion
|
|
229
|
-
//#region src/tracking.ts
|
|
230
|
-
/** The cookie the capture script writes. Mirrors `TRACKING_COOKIE` in utils. */
|
|
231
|
-
const TRACKING_COOKIE = "_ac";
|
|
232
|
-
/**
|
|
233
|
-
* Resolve the tracking config into what the component emits.
|
|
234
|
-
*
|
|
235
|
-
* Two warnings, both about the same failure — a persistent identifier written
|
|
236
|
-
* with no way for the visitor to decline, or a permission asked for with no way
|
|
237
|
-
* to answer. Returned as data rather than logged here so the check is a test
|
|
238
|
-
* rather than a line someone might scroll past, exactly as `resolveAnalytics`
|
|
239
|
-
* does.
|
|
240
|
-
*/
|
|
241
|
-
function resolveTracking(opts, ctx = {}) {
|
|
242
|
-
const warnings = [];
|
|
243
|
-
const category = opts?.category ?? "marketing";
|
|
244
|
-
const enabled = opts?.enabled === true;
|
|
245
|
-
if (!enabled) return {
|
|
246
|
-
enabled: false,
|
|
247
|
-
category,
|
|
248
|
-
cookies: [],
|
|
249
|
-
warnings
|
|
250
|
-
};
|
|
251
|
-
if (!ctx.consent) warnings.push(`tracking is on, so this site writes the \`${"_ac"}\` cookie — a 90-day identifier tying a visitor to the ad that brought them — and \`consent\` is off, so it is written for every visitor with no way to decline. Set \`consent: true\`.`);
|
|
252
|
-
else if (ctx.consentCategories && !ctx.consentCategories.includes(category)) warnings.push(`tracking demands the \`${category}\` consent category, but the banner offers ${ctx.consentCategories.join(", ") || "nothing"}. Add \`${category}\` to \`consent.categories\`, or the visitor is asked about something the banner never names.`);
|
|
253
|
-
return {
|
|
254
|
-
enabled,
|
|
255
|
-
category,
|
|
256
|
-
cookies: ["_ac"],
|
|
257
|
-
warnings
|
|
258
|
-
};
|
|
259
|
-
}
|
|
260
|
-
|
|
261
329
|
//#endregion
|
|
262
330
|
//#region src/integration.ts
|
|
263
331
|
/**
|
|
@@ -298,6 +366,101 @@ function undisclosedProviders(configPath, analytics) {
|
|
|
298
366
|
}
|
|
299
367
|
return Object.keys(SITE_CONFIG_KEYS).filter((key) => analytics[key] !== void 0 && !declared[SITE_CONFIG_KEYS[key]]);
|
|
300
368
|
}
|
|
369
|
+
/**
|
|
370
|
+
* Read `site.ads` and this environment's ads switch out of the site config.
|
|
371
|
+
*
|
|
372
|
+
* Best-effort, like `undisclosedProviders`: every other consumer of this file
|
|
373
|
+
* validates it loudly elsewhere in the same hook, and a second copy of that error
|
|
374
|
+
* helps nobody. `resolveConfig` is used rather than a bare `JSON.parse` so an A/B
|
|
375
|
+
* variant's `overrides` are applied first — a variant that changes which pixels
|
|
376
|
+
* run is exactly the kind of thing overrides exist for.
|
|
377
|
+
*
|
|
378
|
+
* The environment switch cascades `ads` → `analytics` → true. An environment that
|
|
379
|
+
* has already said "no analytics here" has said the interesting half of it; making
|
|
380
|
+
* a consumer repeat themselves to stop conversion pixels would mean a preview
|
|
381
|
+
* deploy fires them by default, which is the wrong way for this to fail.
|
|
382
|
+
*/
|
|
383
|
+
function readSiteAds(configPath, siteEnv) {
|
|
384
|
+
try {
|
|
385
|
+
const cfg = resolveConfig(JSON.parse(readFileSync(configPath, "utf8")), siteEnv);
|
|
386
|
+
const properties = cfg.site.ads;
|
|
387
|
+
if (!properties || Object.keys(properties).length === 0) return null;
|
|
388
|
+
const env = cfg.site.environments?.[siteEnv];
|
|
389
|
+
return {
|
|
390
|
+
enabled: env?.ads ?? env?.analytics ?? true,
|
|
391
|
+
properties
|
|
392
|
+
};
|
|
393
|
+
} catch {
|
|
394
|
+
return null;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
/**
|
|
398
|
+
* The consent + tracking facts a site config already states.
|
|
399
|
+
*
|
|
400
|
+
* These were the one pair the integration did not read. `site.tracking` and
|
|
401
|
+
* `site.legal.cookieConsent` drive the **generated cookie notice**, while
|
|
402
|
+
* `vitops({ consent, tracking })` drove the **runtime** — two hand-synced
|
|
403
|
+
* declarations of the same fact with nothing checking them against each other.
|
|
404
|
+
* The failure that produces is precisely the one the rest of the toolchain is
|
|
405
|
+
* fastidious about avoiding: a site can ship a cookie notice naming categories
|
|
406
|
+
* its banner never offers, or promising attribution that no capture script
|
|
407
|
+
* implements, and the build is clean. That is a compliance defect, not a
|
|
408
|
+
* documentation gap.
|
|
409
|
+
*
|
|
410
|
+
* Read as DEFAULTS, the same as `css` / `fonts` / `favicon` / `ads` — the
|
|
411
|
+
* config is where a site's facts live, and an integration option is how you
|
|
412
|
+
* override one. A genuine contradiction is an error rather than a warning; see
|
|
413
|
+
* `consentTrackingConflicts`.
|
|
414
|
+
*/
|
|
415
|
+
function readSiteConsentTracking(configPath, siteEnv) {
|
|
416
|
+
try {
|
|
417
|
+
const cfg = resolveConfig(JSON.parse(readFileSync(configPath, "utf8")), siteEnv);
|
|
418
|
+
const t = cfg.site.tracking;
|
|
419
|
+
const cc = cfg.site.legal?.cookieConsent;
|
|
420
|
+
if (!t && !cc) return null;
|
|
421
|
+
return {
|
|
422
|
+
...t ? { tracking: t } : {},
|
|
423
|
+
...cc ? { consentEnabled: cc.enabled } : {},
|
|
424
|
+
...cc?.categories ? { categories: cc.categories } : {}
|
|
425
|
+
};
|
|
426
|
+
} catch {
|
|
427
|
+
return null;
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
/**
|
|
431
|
+
* Contradictions between the config and an explicit integration option.
|
|
432
|
+
*
|
|
433
|
+
* Absent-vs-present is a default and resolves silently. `true` vs `false` on the
|
|
434
|
+
* same fact is not resolvable — one of the two documents is wrong, and picking
|
|
435
|
+
* either quietly is how a notice and a banner get out of step. The message names
|
|
436
|
+
* both sides so the fix does not require guessing which one won.
|
|
437
|
+
*/
|
|
438
|
+
function consentTrackingConflicts(site, opts, configPath) {
|
|
439
|
+
const out = [];
|
|
440
|
+
const optConsent = opts.consent === void 0 ? void 0 : !!opts.consent;
|
|
441
|
+
if (site.consentEnabled !== void 0 && optConsent !== void 0 && site.consentEnabled !== optConsent) out.push(`consent: ${configPath} sets site.legal.cookieConsent.enabled: ${site.consentEnabled}, but vitops({ consent: ${optConsent} }) says the opposite. The generated cookie notice follows the config and the banner follows the option, so one of them would describe a site that does not exist. Set them the same, or drop the option and let the config decide.`);
|
|
442
|
+
const optTracking = opts.tracking === void 0 ? void 0 : opts.tracking === true ? true : opts.tracking === false ? false : opts.tracking.enabled ?? true;
|
|
443
|
+
if (site.tracking?.enabled !== void 0 && optTracking !== void 0 && site.tracking.enabled !== optTracking) out.push(`tracking: ${configPath} sets site.tracking.enabled: ${site.tracking.enabled}, but vitops({ tracking: ${optTracking} }) says the opposite. The cookie notice discloses \`_ac\` from the config while the capture script ships from the option — so the site would either disclose a cookie it never sets, or set one it never discloses.`);
|
|
444
|
+
return out;
|
|
445
|
+
}
|
|
446
|
+
/**
|
|
447
|
+
* Does a route answer `TRACKING_ENDPOINT`?
|
|
448
|
+
*
|
|
449
|
+
* Checked by filename rather than through Astro's route manifest, because this
|
|
450
|
+
* runs in `astro:config:setup` — before routes are collected. The extension set
|
|
451
|
+
* covers the ways a consumer can author the same endpoint; a false negative
|
|
452
|
+
* costs a warning, a false positive costs the whole point of the check, so this
|
|
453
|
+
* errs toward looking in more places.
|
|
454
|
+
*/
|
|
455
|
+
function hasConversionRoute(root) {
|
|
456
|
+
const base = resolve(root, "src/pages", TRACKING_ENDPOINT.replace(/^\//, ""));
|
|
457
|
+
return [
|
|
458
|
+
".ts",
|
|
459
|
+
".js",
|
|
460
|
+
".mjs",
|
|
461
|
+
".astro"
|
|
462
|
+
].some((ext) => existsSync(`${base}${ext}`) || existsSync(join(base, `index${ext}`)));
|
|
463
|
+
}
|
|
301
464
|
const VIRTUAL_ID = "virtual:getvitops/head";
|
|
302
465
|
const RESOLVED_ID = `\0${VIRTUAL_ID}`;
|
|
303
466
|
function virtualHeadPlugin(data) {
|
|
@@ -388,11 +551,20 @@ function vitops(opts = {}) {
|
|
|
388
551
|
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.");
|
|
389
552
|
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.");
|
|
390
553
|
}
|
|
391
|
-
const
|
|
392
|
-
|
|
554
|
+
const siteConsent = siteInput ? readSiteConsentTracking(resolve(root, siteInput), opts.site?.siteEnv ?? "production") : null;
|
|
555
|
+
if (siteConsent) {
|
|
556
|
+
const conflicts = consentTrackingConflicts(siteConsent, opts, siteInput);
|
|
557
|
+
if (conflicts.length) throw new Error(`vitops: the site config and the integration options disagree about consent.\n\n` + conflicts.map((c) => ` • ${c}`).join("\n\n"));
|
|
558
|
+
}
|
|
559
|
+
const consentEnabled = opts.consent !== void 0 ? !!opts.consent : !!siteConsent?.consentEnabled;
|
|
560
|
+
const consentOpts = typeof opts.consent === "object" ? opts.consent : siteConsent?.categories ? { categories: siteConsent.categories } : {};
|
|
393
561
|
const analytics = resolveAnalytics(opts.analytics, { consent: consentEnabled });
|
|
394
|
-
const trackingOpts = opts.tracking === true ? { enabled: true } : opts.tracking || void 0;
|
|
395
|
-
const
|
|
562
|
+
const trackingOpts = opts.tracking === true ? { enabled: true } : opts.tracking || (siteConsent?.tracking?.enabled ? siteConsent.tracking : void 0);
|
|
563
|
+
const adsConfig = siteInput ? readSiteAds(resolve(root, siteInput), opts.site?.siteEnv ?? "production") : null;
|
|
564
|
+
const detectedCategories = consentCategories([...analytics.tags, ...resolveAds(adsConfig?.properties, {
|
|
565
|
+
consent: consentEnabled,
|
|
566
|
+
enabled: true
|
|
567
|
+
}).tags]);
|
|
396
568
|
const defaultCategories = ["analytics", "preferences"];
|
|
397
569
|
const trackingCategory = trackingOpts?.enabled ? trackingOpts.category ?? "marketing" : null;
|
|
398
570
|
if (detectedCategories.includes("marketing") || trackingCategory === "marketing") defaultCategories.push("marketing");
|
|
@@ -403,6 +575,13 @@ function vitops(opts = {}) {
|
|
|
403
575
|
consentCategories: offeredCategories
|
|
404
576
|
});
|
|
405
577
|
for (const warning of tracking.warnings) logger.warn(warning);
|
|
578
|
+
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.`);
|
|
579
|
+
const ads = resolveAds(adsConfig?.properties, {
|
|
580
|
+
consent: consentEnabled,
|
|
581
|
+
consentCategories: offeredCategories,
|
|
582
|
+
enabled: adsConfig?.enabled ?? true
|
|
583
|
+
});
|
|
584
|
+
for (const warning of ads.warnings) logger.warn(warning);
|
|
406
585
|
if (opts.analytics && legalInput) {
|
|
407
586
|
const undisclosed = undisclosedProviders(resolve(root, legalInput), opts.analytics);
|
|
408
587
|
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 +784,8 @@ function vitops(opts = {}) {
|
|
|
605
784
|
enabled: true,
|
|
606
785
|
category: tracking.category,
|
|
607
786
|
cookies: tracking.cookies
|
|
608
|
-
} : null
|
|
787
|
+
} : null,
|
|
788
|
+
ads: adsConfig
|
|
609
789
|
})] } });
|
|
610
790
|
if (opts.css) {
|
|
611
791
|
const format = opts.css.format ?? "tailwind";
|
|
@@ -933,5 +1113,5 @@ function resolveSeo(defaults, props, ctx) {
|
|
|
933
1113
|
}
|
|
934
1114
|
|
|
935
1115
|
//#endregion
|
|
936
|
-
export { TRACKING_COOKIE, consentCategories, vitops as default, gitLastmod, resolveAnalytics, resolveSeo, resolveTracking, routeFromPage, slugFromContent };
|
|
1116
|
+
export { TRACKING_COOKIE, consentCategories, vitops as default, gitLastmod, resolveAds, resolveAnalytics, resolveSeo, resolveTracking, routeFromPage, slugFromContent };
|
|
937
1117
|
//# sourceMappingURL=index.mjs.map
|