@getvitops/astro 1.0.0 → 2.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 +599 -0
- package/README.md +222 -3
- package/dist/index.d.mts +422 -5
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +637 -15
- package/dist/index.mjs.map +1 -1
- package/package.json +12 -5
- package/src/analytics.ts +325 -0
- package/src/components/Analytics.astro +59 -0
- package/src/components/CookieConsent.astro +106 -0
- package/src/components/Details.astro +10 -4
- package/src/components/Drawer.astro +9 -3
- package/src/components/Head.astro +46 -1
- package/src/components/Icon.astro +94 -0
- package/src/components/Popover.astro +9 -3
- package/src/seo.ts +33 -3
- package/virtual.d.ts +49 -0
package/dist/index.mjs
CHANGED
|
@@ -1,11 +1,231 @@
|
|
|
1
|
-
import { cpSync, existsSync } from "node:fs";
|
|
2
|
-
import { dirname, join, resolve } from "node:path";
|
|
1
|
+
import { cpSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { dirname, join, relative, resolve } from "node:path";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
|
-
import {
|
|
4
|
+
import { isSiteConfig, resolveSiteConfig } from "@getvitops/generator";
|
|
5
|
+
import { collectIconRefs, faviconLinks, generateFavicons, generateIconInclude, resolveIcon, scanFiles, writeFaviconManifest } from "@getvitops/utils";
|
|
5
6
|
import vitops from "@getvitops/vite";
|
|
7
|
+
import { fontProviders } from "astro/config";
|
|
8
|
+
import { execFile } from "node:child_process";
|
|
9
|
+
import { promisify } from "node:util";
|
|
6
10
|
|
|
7
11
|
export * from "@getvitops/utils"
|
|
8
12
|
|
|
13
|
+
//#region src/analytics.ts
|
|
14
|
+
/**
|
|
15
|
+
* IDs land inside an inline `<script>`, so they are checked rather than trusted.
|
|
16
|
+
* A `<\/script>` in a measurement ID is not a plausible accident, but the cost of
|
|
17
|
+
* the guard is one regex and the cost of not having it is arbitrary script
|
|
18
|
+
* injection from a config file.
|
|
19
|
+
*/
|
|
20
|
+
const SAFE_ID = /^[\w.-]+$/;
|
|
21
|
+
function bad(value) {
|
|
22
|
+
return !SAFE_ID.test(value);
|
|
23
|
+
}
|
|
24
|
+
/** JSON is a subset of JS object-literal syntax; `<` can't appear unescaped. */
|
|
25
|
+
function literal(value) {
|
|
26
|
+
return JSON.stringify(value).replaceAll("<", "\\u003c");
|
|
27
|
+
}
|
|
28
|
+
function googleAnalytics(input) {
|
|
29
|
+
const opts = typeof input === "string" ? { id: input } : input;
|
|
30
|
+
if (bad(opts.id)) return `googleAnalytics: "${opts.id}" is not a valid measurement ID. Skipped.`;
|
|
31
|
+
const config = opts.config ? `,${literal(opts.config)}` : "";
|
|
32
|
+
return {
|
|
33
|
+
key: "googleAnalytics",
|
|
34
|
+
provider: "Google Analytics",
|
|
35
|
+
category: opts.category ?? "analytics",
|
|
36
|
+
setsCookies: true,
|
|
37
|
+
cookies: [
|
|
38
|
+
"_ga",
|
|
39
|
+
"_ga_*",
|
|
40
|
+
"_gid",
|
|
41
|
+
"_gat",
|
|
42
|
+
"_gat_*"
|
|
43
|
+
],
|
|
44
|
+
strategy: "idle",
|
|
45
|
+
gated: true,
|
|
46
|
+
inline: `window.dataLayer=window.dataLayer||[];function gtag(){dataLayer.push(arguments)}gtag('js',new Date());gtag('config','${opts.id}'${config});`,
|
|
47
|
+
src: `https://www.googletagmanager.com/gtag/js?id=${encodeURIComponent(opts.id)}`,
|
|
48
|
+
attrs: {}
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
function clarity(input) {
|
|
52
|
+
const opts = typeof input === "string" ? { id: input } : input;
|
|
53
|
+
if (bad(opts.id)) return `clarity: "${opts.id}" is not a valid project ID. Skipped.`;
|
|
54
|
+
const category = opts.category ?? "analytics";
|
|
55
|
+
return {
|
|
56
|
+
key: "clarity",
|
|
57
|
+
provider: "Microsoft Clarity",
|
|
58
|
+
category,
|
|
59
|
+
setsCookies: true,
|
|
60
|
+
cookies: [
|
|
61
|
+
"_clck",
|
|
62
|
+
"_clsk",
|
|
63
|
+
"MUID",
|
|
64
|
+
"CLID",
|
|
65
|
+
"ANONCHK",
|
|
66
|
+
"SM"
|
|
67
|
+
],
|
|
68
|
+
strategy: "idle",
|
|
69
|
+
gated: true,
|
|
70
|
+
inline: `(function(c,l,a,r,i,t,y){c[a]=c[a]||function(){(c[a].q=c[a].q||[]).push(arguments)};t=l.createElement(r);t.async=1;t.src="https://www.clarity.ms/tag/"+i;y=l.getElementsByTagName(r)[0];y.parentNode.insertBefore(t,y)})(window,document,"clarity","script","${opts.id}");window.clarity('consentv2',{ad_Storage:'${category === "marketing" ? "granted" : "denied"}',analytics_Storage:'granted'});`,
|
|
71
|
+
src: null,
|
|
72
|
+
attrs: {}
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
function matomo(opts) {
|
|
76
|
+
const siteId = String(opts.siteId);
|
|
77
|
+
if (bad(siteId)) return `matomo: "${siteId}" is not a valid site ID. Skipped.`;
|
|
78
|
+
let base;
|
|
79
|
+
try {
|
|
80
|
+
base = new URL(opts.url);
|
|
81
|
+
} catch {
|
|
82
|
+
return `matomo: "${opts.url}" is not a valid URL. Skipped.`;
|
|
83
|
+
}
|
|
84
|
+
const url = `${base.origin}${base.pathname.replace(/\/*$/, "")}/`;
|
|
85
|
+
const cookies = opts.cookies === true;
|
|
86
|
+
return {
|
|
87
|
+
key: "matomo",
|
|
88
|
+
provider: "Matomo",
|
|
89
|
+
category: cookies ? "analytics" : "necessary",
|
|
90
|
+
setsCookies: cookies,
|
|
91
|
+
cookies: cookies ? [
|
|
92
|
+
"_pk_id*",
|
|
93
|
+
"_pk_ses*",
|
|
94
|
+
"_pk_ref*",
|
|
95
|
+
"_pk_cvar*",
|
|
96
|
+
"_pk_hsr*"
|
|
97
|
+
] : [],
|
|
98
|
+
strategy: "idle",
|
|
99
|
+
gated: true,
|
|
100
|
+
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)})();`,
|
|
101
|
+
src: null,
|
|
102
|
+
attrs: {}
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
function plausible(input) {
|
|
106
|
+
const opts = typeof input === "string" ? { domain: input } : input;
|
|
107
|
+
if (bad(opts.domain)) return `plausible: "${opts.domain}" is not a valid domain. Skipped.`;
|
|
108
|
+
return {
|
|
109
|
+
key: "plausible",
|
|
110
|
+
provider: "Plausible Analytics",
|
|
111
|
+
category: "necessary",
|
|
112
|
+
setsCookies: false,
|
|
113
|
+
cookies: [],
|
|
114
|
+
strategy: "idle",
|
|
115
|
+
gated: true,
|
|
116
|
+
inline: "",
|
|
117
|
+
src: opts.src ?? "https://plausible.io/js/script.js",
|
|
118
|
+
attrs: { "data-domain": opts.domain }
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Resolve the configured providers into the tags to emit.
|
|
123
|
+
*
|
|
124
|
+
* An invalid provider is dropped with a warning rather than throwing: one
|
|
125
|
+
* malformed ID should not take down a build, and a missing tag is visible in the
|
|
126
|
+
* output while a failed build is just a red wall.
|
|
127
|
+
*/
|
|
128
|
+
function resolveAnalytics(opts, ctx = {}) {
|
|
129
|
+
const warnings = [];
|
|
130
|
+
const tags = [];
|
|
131
|
+
if (!opts) return {
|
|
132
|
+
tags,
|
|
133
|
+
needsRuntime: false,
|
|
134
|
+
warnings
|
|
135
|
+
};
|
|
136
|
+
const strategy = opts.strategy ?? "idle";
|
|
137
|
+
const results = [
|
|
138
|
+
opts.googleAnalytics === void 0 ? null : googleAnalytics(opts.googleAnalytics),
|
|
139
|
+
opts.clarity === void 0 ? null : clarity(opts.clarity),
|
|
140
|
+
opts.matomo === void 0 ? null : matomo(opts.matomo),
|
|
141
|
+
opts.plausible === void 0 ? null : plausible(opts.plausible)
|
|
142
|
+
];
|
|
143
|
+
for (const result of results) {
|
|
144
|
+
if (result === null) continue;
|
|
145
|
+
if (typeof result === "string") {
|
|
146
|
+
warnings.push(result);
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
const gated = result.category !== "necessary" || strategy !== "async";
|
|
150
|
+
tags.push({
|
|
151
|
+
...result,
|
|
152
|
+
strategy,
|
|
153
|
+
gated
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
const setting = tags.filter((t) => t.setsCookies);
|
|
157
|
+
if (setting.length && !ctx.consent) warnings.push(`${setting.map((t) => t.provider).join(" and ")} ${setting.length === 1 ? "sets" : "set"} cookies (${setting.flatMap((t) => t.cookies).join(", ")}) and \`consent\` is off, so ${setting.length === 1 ? "it loads" : "they load"} for every visitor with no way to decline. Set \`consent: true\`, or switch to a cookieless provider (Plausible, or Matomo with its default \`cookies: false\`).`);
|
|
158
|
+
return {
|
|
159
|
+
tags,
|
|
160
|
+
needsRuntime: tags.some((t) => t.gated),
|
|
161
|
+
warnings
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
/** Which optional categories a set of tags actually needs a visitor to decide. */
|
|
165
|
+
function consentCategories(tags) {
|
|
166
|
+
const used = /* @__PURE__ */ new Set();
|
|
167
|
+
for (const tag of tags) if (tag.category !== "necessary") used.add(tag.category);
|
|
168
|
+
return [
|
|
169
|
+
"analytics",
|
|
170
|
+
"marketing",
|
|
171
|
+
"preferences"
|
|
172
|
+
].filter((c) => used.has(c));
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
//#endregion
|
|
176
|
+
//#region src/fonts.ts
|
|
177
|
+
/**
|
|
178
|
+
* Provider names, matching `fontProviders`' keys 1:1 (verified against astro@7.1.2).
|
|
179
|
+
* The generator's `SiteFontSchema` enumerates the same set, so a mismatch is a
|
|
180
|
+
* compile-time-invisible drift — `resolveFonts` throws on an unknown name rather than
|
|
181
|
+
* quietly dropping the family and leaving the token resolving to a fallback stack.
|
|
182
|
+
*/
|
|
183
|
+
const PROVIDERS = new Set(Object.keys(fontProviders));
|
|
184
|
+
/**
|
|
185
|
+
* Providers a declaration can construct with no arguments.
|
|
186
|
+
*
|
|
187
|
+
* `adobe` is the one that can't: `fontProviders.adobe({ id })` takes a project API id,
|
|
188
|
+
* and the point of that id being an argument is that it comes from the environment. A
|
|
189
|
+
* JSON declaration has nowhere to put `process.env.ADOBE_ID` without either inlining the
|
|
190
|
+
* key into a committed file or inventing an interpolation syntax, so Adobe families are
|
|
191
|
+
* declared in `astro.config` directly — see the error thrown below.
|
|
192
|
+
*/
|
|
193
|
+
const NEEDS_OPTIONS = new Set(["adobe"]);
|
|
194
|
+
/**
|
|
195
|
+
* Map declarations onto Astro's `fonts:` entries.
|
|
196
|
+
*
|
|
197
|
+
* `preload` is dropped from the Astro entry on purpose — it is not part of the config
|
|
198
|
+
* schema there, it is a prop on the `<Font />` component. It rides along in `head`
|
|
199
|
+
* instead, which is what makes preload a property of the declaration rather than
|
|
200
|
+
* something the consumer hand-picks in their layout.
|
|
201
|
+
*/
|
|
202
|
+
function resolveFonts(decls) {
|
|
203
|
+
const fonts = [];
|
|
204
|
+
const head = [];
|
|
205
|
+
const seen = /* @__PURE__ */ new Set();
|
|
206
|
+
for (const decl of decls) {
|
|
207
|
+
if (!PROVIDERS.has(decl.provider)) throw new Error(`[getvitops] fonts: unknown provider "${decl.provider}" for "${decl.name}". Astro provides: ${[...PROVIDERS].sort().join(", ")}.`);
|
|
208
|
+
if (NEEDS_OPTIONS.has(decl.provider)) throw new Error(`[getvitops] fonts: provider "${decl.provider}" needs an API id from your environment ("${decl.name}"), and a JSON declaration has nowhere to hold one. Declare this family in astro.config instead — fonts: [{ provider: fontProviders.${decl.provider}({ id: process.env.ADOBE_ID }), name: '${decl.name}', cssVariable: '${decl.cssVariable}' }] — and render its <Font /> yourself; the design-system token can still point at it.`);
|
|
209
|
+
if (!decl.cssVariable.startsWith("--")) throw new Error(`[getvitops] fonts: cssVariable "${decl.cssVariable}" (${decl.name}) must start with "--".`);
|
|
210
|
+
if (seen.has(decl.cssVariable)) throw new Error(`[getvitops] fonts: two families both declare ${decl.cssVariable} (the second is "${decl.name}"). Astro resolves one family per variable — give them separate variables, or drop one.`);
|
|
211
|
+
seen.add(decl.cssVariable);
|
|
212
|
+
const { provider, preload, ...rest } = decl;
|
|
213
|
+
fonts.push({
|
|
214
|
+
...rest,
|
|
215
|
+
provider: fontProviders[provider]()
|
|
216
|
+
});
|
|
217
|
+
head.push({
|
|
218
|
+
cssVariable: decl.cssVariable,
|
|
219
|
+
preload: preload ?? false
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
return {
|
|
223
|
+
fonts,
|
|
224
|
+
head
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
//#endregion
|
|
9
229
|
//#region src/integration.ts
|
|
10
230
|
/**
|
|
11
231
|
* @getvitops/astro — the Astro integration. One entry wires the design system's
|
|
@@ -16,6 +236,35 @@ export * from "@getvitops/utils"
|
|
|
16
236
|
* The `<Head />` component (shipped alongside) renders the tags, reading resolved
|
|
17
237
|
* config from the `virtual:getvitops/head` module this integration provides.
|
|
18
238
|
*/
|
|
239
|
+
/**
|
|
240
|
+
* Which providers `getvitops({ analytics })` configures that the site config's
|
|
241
|
+
* own `analytics` block does not.
|
|
242
|
+
*
|
|
243
|
+
* The two are separate surfaces on purpose — the integration must not import
|
|
244
|
+
* `SiteConfig` — but they describe the same site, and a disagreement between them
|
|
245
|
+
* is a compliance defect rather than a style issue: `vitops legal` derives the
|
|
246
|
+
* cookie notice from the site config, so a provider missing there is a tag the
|
|
247
|
+
* site runs and its own notice never mentions. That is precisely what the
|
|
248
|
+
* generator's processor table exists to prevent.
|
|
249
|
+
*
|
|
250
|
+
* Read is best-effort — the `legal` step already reports an unreadable config,
|
|
251
|
+
* and a second copy of that error helps nobody.
|
|
252
|
+
*/
|
|
253
|
+
const SITE_CONFIG_KEYS = {
|
|
254
|
+
googleAnalytics: "googleAnalyticsId",
|
|
255
|
+
clarity: "clarityId",
|
|
256
|
+
matomo: "matomo",
|
|
257
|
+
plausible: "plausibleDomain"
|
|
258
|
+
};
|
|
259
|
+
function undisclosedProviders(configPath, analytics) {
|
|
260
|
+
let declared;
|
|
261
|
+
try {
|
|
262
|
+
declared = JSON.parse(readFileSync(configPath, "utf8")).analytics ?? {};
|
|
263
|
+
} catch {
|
|
264
|
+
return [];
|
|
265
|
+
}
|
|
266
|
+
return Object.keys(SITE_CONFIG_KEYS).filter((key) => analytics[key] !== void 0 && !declared[SITE_CONFIG_KEYS[key]]);
|
|
267
|
+
}
|
|
19
268
|
const VIRTUAL_ID = "virtual:getvitops/head";
|
|
20
269
|
const RESOLVED_ID = `\0${VIRTUAL_ID}`;
|
|
21
270
|
function virtualHeadPlugin(data) {
|
|
@@ -29,6 +278,63 @@ function virtualHeadPlugin(data) {
|
|
|
29
278
|
}
|
|
30
279
|
};
|
|
31
280
|
}
|
|
281
|
+
const ICONS_ID = "virtual:getvitops/icons";
|
|
282
|
+
const RESOLVED_ICONS_ID = `\0${ICONS_ID}`;
|
|
283
|
+
function virtualIconsPlugin(data) {
|
|
284
|
+
return {
|
|
285
|
+
name: "@getvitops/astro:virtual-icons",
|
|
286
|
+
resolveId(id) {
|
|
287
|
+
return id === ICONS_ID ? RESOLVED_ICONS_ID : null;
|
|
288
|
+
},
|
|
289
|
+
load(id) {
|
|
290
|
+
return id === RESOLVED_ICONS_ID ? `export default ${JSON.stringify(data)};` : null;
|
|
291
|
+
}
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
/** Defaults live here so the component and the integration can't drift. */
|
|
295
|
+
const ICON_DEFAULTS = {
|
|
296
|
+
ui: "fa7-solid",
|
|
297
|
+
brand: "simple-icons",
|
|
298
|
+
sprite: "/vitops/icons.svg",
|
|
299
|
+
scan: ["src"]
|
|
300
|
+
};
|
|
301
|
+
/**
|
|
302
|
+
* Probe an optional icon integration so it can be registered for the consumer's
|
|
303
|
+
* OWN `<Icon>` usage. Nothing here decides how our `<Icon />` renders — that is
|
|
304
|
+
* `engine`, and it never needs a peer.
|
|
305
|
+
*/
|
|
306
|
+
/**
|
|
307
|
+
* Indirection so the bundler can't resolve the specifier statically. See the
|
|
308
|
+
* import site in the `tailwind` branch for why that matters.
|
|
309
|
+
*/
|
|
310
|
+
const TAILWIND_VITE = "@tailwindcss/vite";
|
|
311
|
+
async function probeIconPackage(name) {
|
|
312
|
+
try {
|
|
313
|
+
await import(
|
|
314
|
+
/* @vite-ignore */
|
|
315
|
+
name
|
|
316
|
+
);
|
|
317
|
+
return true;
|
|
318
|
+
} catch {
|
|
319
|
+
return false;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
/**
|
|
323
|
+
* Does the file at `path` hold a site config rather than a bare design system?
|
|
324
|
+
*
|
|
325
|
+
* Swallows a missing or unparseable file and answers `false`: this only decides
|
|
326
|
+
* whether the other options may default their paths to this one, and the real
|
|
327
|
+
* read happens in the Vite plugin, which reports a parse error properly. Failing
|
|
328
|
+
* the build here would turn "your JSON has a trailing comma" into an error about
|
|
329
|
+
* an option the consumer never set.
|
|
330
|
+
*/
|
|
331
|
+
function readsAsSiteConfig(path) {
|
|
332
|
+
try {
|
|
333
|
+
return isSiteConfig(JSON.parse(readFileSync(path, "utf8")));
|
|
334
|
+
} catch {
|
|
335
|
+
return false;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
32
338
|
function getvitops(opts = {}) {
|
|
33
339
|
return {
|
|
34
340
|
name: "@getvitops/astro",
|
|
@@ -39,10 +345,26 @@ function getvitops(opts = {}) {
|
|
|
39
345
|
const editor = opts.editor === true;
|
|
40
346
|
if (editor && !opts.css) logger.warn("editor: true needs a `css` config — it reads design-manifest.json.");
|
|
41
347
|
if (opts.legal && !opts.css) logger.warn("legal: needs a `css` config — the generation runs in the Vite plugin that `css` registers. Run `npx vitops legal` instead if you do not use `css`.");
|
|
348
|
+
if (opts.media && !opts.css) logger.warn("media: needs a `css` config — the encoding runs in the Vite plugin that `css` registers. Run `npx vitops media` instead if you do not use `css`.");
|
|
349
|
+
const cssInput = opts.css ? opts.css.input ?? "design-system.json" : void 0;
|
|
350
|
+
const siteInput = opts.site?.input ?? (cssInput != null && readsAsSiteConfig(resolve(root, cssInput)) ? cssInput : void 0);
|
|
351
|
+
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.");
|
|
352
|
+
const legalInput = opts.legal?.input ?? siteInput;
|
|
353
|
+
if (opts.legal && !legalInput) throw new Error("[getvitops] legal: needs `legal.input` (a site config path), the top-level `site` option, or a `css.input` that is itself a site config.");
|
|
42
354
|
if (opts.seo) {
|
|
43
355
|
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.");
|
|
44
356
|
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.");
|
|
45
357
|
}
|
|
358
|
+
const consentEnabled = !!opts.consent;
|
|
359
|
+
const consentOpts = typeof opts.consent === "object" ? opts.consent : {};
|
|
360
|
+
const analytics = resolveAnalytics(opts.analytics, { consent: consentEnabled });
|
|
361
|
+
const detectedCategories = consentCategories(analytics.tags);
|
|
362
|
+
const offeredCategories = consentOpts.categories ?? (detectedCategories.length ? detectedCategories : ["analytics"]);
|
|
363
|
+
for (const warning of analytics.warnings) logger.warn(warning);
|
|
364
|
+
if (opts.analytics && legalInput) {
|
|
365
|
+
const undisclosed = undisclosedProviders(resolve(root, legalInput), opts.analytics);
|
|
366
|
+
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}.`);
|
|
367
|
+
}
|
|
46
368
|
const hasManifest = !!(opts.favicon?.name && opts.favicon?.themeColor);
|
|
47
369
|
let hasSvg = false;
|
|
48
370
|
if (opts.favicon) {
|
|
@@ -51,7 +373,8 @@ function getvitops(opts = {}) {
|
|
|
51
373
|
await generateFavicons({
|
|
52
374
|
source,
|
|
53
375
|
outputDir: publicDir,
|
|
54
|
-
...opts.favicon.lowResSource ? { lowResSource: resolve(root, opts.favicon.lowResSource) } : {}
|
|
376
|
+
...opts.favicon.lowResSource ? { lowResSource: resolve(root, opts.favicon.lowResSource) } : {},
|
|
377
|
+
...opts.favicon.backgroundColor ? { backgroundColor: opts.favicon.backgroundColor } : {}
|
|
55
378
|
});
|
|
56
379
|
if (opts.favicon.name && opts.favicon.themeColor) await writeFaviconManifest(publicDir, {
|
|
57
380
|
name: opts.favicon.name,
|
|
@@ -60,16 +383,45 @@ function getvitops(opts = {}) {
|
|
|
60
383
|
});
|
|
61
384
|
logger.info("generated favicons + manifest → public/");
|
|
62
385
|
}
|
|
63
|
-
|
|
386
|
+
let headFonts = [];
|
|
387
|
+
if (opts.fonts) {
|
|
388
|
+
const given = Array.isArray(opts.fonts) ? { families: opts.fonts } : typeof opts.fonts === "string" ? { input: opts.fonts } : opts.fonts === true ? {} : opts.fonts;
|
|
389
|
+
const o = {
|
|
390
|
+
...given,
|
|
391
|
+
...given.input == null && given.families == null && siteInput ? {
|
|
392
|
+
input: siteInput,
|
|
393
|
+
...opts.site?.siteEnv ? { siteEnv: opts.site.siteEnv } : {}
|
|
394
|
+
} : {}
|
|
395
|
+
};
|
|
396
|
+
const declared = [];
|
|
397
|
+
if (o.input) {
|
|
398
|
+
const site = resolveSiteConfig(JSON.parse(readFileSync(resolve(root, o.input), "utf8")), o.siteEnv ?? "production");
|
|
399
|
+
declared.push(...site.fonts ?? []);
|
|
400
|
+
}
|
|
401
|
+
declared.push(...o.families ?? []);
|
|
402
|
+
if (declared.length === 0) logger.warn("fonts: nothing declared — no webfonts will load. Add families, or drop the option.");
|
|
403
|
+
else {
|
|
404
|
+
const existing = new Set((config.fonts ?? []).map((f) => f.cssVariable));
|
|
405
|
+
const { fonts, head } = resolveFonts(declared);
|
|
406
|
+
for (const f of head) if (existing.has(f.cssVariable)) logger.warn(`fonts: ${f.cssVariable} is already declared in your astro.config \`fonts\` array. Astro resolves one family per variable, so one of the two will be dropped — remove it from one place.`);
|
|
407
|
+
updateConfig({ fonts });
|
|
408
|
+
headFonts = head;
|
|
409
|
+
const preloaded = head.filter((f) => f.preload !== false).length;
|
|
410
|
+
logger.info(`fonts: ${fonts.length} famil${fonts.length === 1 ? "y" : "ies"} registered via Astro's Fonts API (${preloaded} preloaded) — <Head /> emits <Font /> for each`);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
const consentRuntime = consentEnabled || analytics.needsRuntime;
|
|
414
|
+
if (webComponents || consentRuntime) {
|
|
64
415
|
const coreDist = join(dirname(fileURLToPath(import.meta.resolve("@getvitops/core/package.json"))), "dist");
|
|
65
416
|
const dest = join(publicDir, "vitops");
|
|
66
417
|
if (existsSync(coreDist)) {
|
|
67
|
-
const bundles = [
|
|
418
|
+
const bundles = webComponents ? [
|
|
68
419
|
"polyfills.js",
|
|
69
420
|
"deferred.js",
|
|
70
421
|
"elements.js",
|
|
71
422
|
"polyfills"
|
|
72
|
-
];
|
|
423
|
+
] : [];
|
|
424
|
+
if (consentRuntime) bundles.push("consent.js");
|
|
73
425
|
if (editor) bundles.push("editor.js");
|
|
74
426
|
for (const f of bundles) {
|
|
75
427
|
const src = join(coreDist, f);
|
|
@@ -98,7 +450,97 @@ function getvitops(opts = {}) {
|
|
|
98
450
|
logger.info(`sitemap registered → ${sitemapHref.slice(1)}`);
|
|
99
451
|
}
|
|
100
452
|
}
|
|
101
|
-
|
|
453
|
+
if (siteInput) try {
|
|
454
|
+
const key = resolveSiteConfig(JSON.parse(readFileSync(resolve(root, siteInput), "utf8")), opts.site?.siteEnv ?? "production").seo?.indexing?.indexNow?.key;
|
|
455
|
+
if (key) {
|
|
456
|
+
mkdirSync(publicDir, { recursive: true });
|
|
457
|
+
writeFileSync(join(publicDir, `${key}.txt`), `${key}\n`);
|
|
458
|
+
logger.info(`IndexNow key file → public/${key}.txt`);
|
|
459
|
+
}
|
|
460
|
+
} catch (err) {
|
|
461
|
+
logger.warn(`IndexNow key file skipped: ${err.message}`);
|
|
462
|
+
}
|
|
463
|
+
let iconsData = null;
|
|
464
|
+
if (opts.icons) {
|
|
465
|
+
const o = opts.icons === true ? {} : opts.icons;
|
|
466
|
+
const ui = o.ui ?? ICON_DEFAULTS.ui;
|
|
467
|
+
const brand = o.brand ?? ICON_DEFAULTS.brand;
|
|
468
|
+
const spriteHref = o.sprite ?? ICON_DEFAULTS.sprite;
|
|
469
|
+
const weightOpt = o.weight ? { weight: o.weight } : {};
|
|
470
|
+
const include = o.include ? generateIconInclude({
|
|
471
|
+
ui,
|
|
472
|
+
brand,
|
|
473
|
+
...weightOpt,
|
|
474
|
+
...o.include
|
|
475
|
+
}) : {};
|
|
476
|
+
const add = (prefix, name) => {
|
|
477
|
+
const list = include[prefix] ??= [];
|
|
478
|
+
if (!list.includes(name)) list.push(name);
|
|
479
|
+
};
|
|
480
|
+
const scanDirs = o.scan === false ? [] : o.scan ?? [...ICON_DEFAULTS.scan];
|
|
481
|
+
const unresolved = [];
|
|
482
|
+
const dynamic = [];
|
|
483
|
+
for (const dir of scanDirs) {
|
|
484
|
+
const abs = resolve(root, dir);
|
|
485
|
+
if (!existsSync(abs)) continue;
|
|
486
|
+
const found = collectIconRefs(scanFiles(abs));
|
|
487
|
+
dynamic.push(...found.dynamic.map((d) => ({
|
|
488
|
+
file: relative(root, d.file),
|
|
489
|
+
line: d.line,
|
|
490
|
+
expr: d.expr
|
|
491
|
+
})));
|
|
492
|
+
for (const name of found.names) {
|
|
493
|
+
const colon = name.indexOf(":");
|
|
494
|
+
if (colon > 0) {
|
|
495
|
+
add(name.slice(0, colon), name.slice(colon + 1));
|
|
496
|
+
continue;
|
|
497
|
+
}
|
|
498
|
+
try {
|
|
499
|
+
const q = resolveIcon(name, ui, weightOpt);
|
|
500
|
+
add(q.slice(0, q.indexOf(":")), q.slice(q.indexOf(":") + 1));
|
|
501
|
+
} catch {
|
|
502
|
+
unresolved.push(name);
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
const wantsInclude = config.output !== "static";
|
|
507
|
+
if (dynamic.length && wantsInclude) logger.warn(`icons: ${dynamic.length} icon name(s) are computed at runtime and cannot be bundled automatically:\n` + dynamic.map((d) => ` ${d.file}:${d.line} ${d.expr}`).join("\n") + `\n Declare them in \`icons.include\` (or set \`icons.scan: false\` to silence this).`);
|
|
508
|
+
if (unresolved.length) logger.warn(`icons: ${unresolved.length} name(s) are not in the semantic map for '${ui}': ${unresolved.join(", ")}. Left as-is — fine for a local src/icons/*.svg or a sprite id, a typo otherwise.`);
|
|
509
|
+
const engine = o.engine ?? "inline";
|
|
510
|
+
if (o.sprite && !opts.css) logger.warn("icons: the sprite is written by the same pass that generates the CSS, so `sprite` without `css` produces no file. Add `css`, or run `vitops icons --sprite`.");
|
|
511
|
+
const already = config.integrations.map((i) => i.name).find((n) => n === "astro-icon" || n === "astro-iconset");
|
|
512
|
+
if (o.register === false) logger.info("icons: register: false — resolving names only, not registering anything.");
|
|
513
|
+
else if (already) logger.info(`icons: ${already} is already in your integrations — leaving yours in charge.`);
|
|
514
|
+
else {
|
|
515
|
+
const pkg = await probeIconPackage("astro-icon") ? "astro-icon" : await probeIconPackage("astro-iconset") ? "astro-iconset" : null;
|
|
516
|
+
if (pkg) {
|
|
517
|
+
const { default: icon } = await import(
|
|
518
|
+
/* @vite-ignore */
|
|
519
|
+
pkg
|
|
520
|
+
);
|
|
521
|
+
updateConfig({ integrations: [icon(wantsInclude ? { include } : {})] });
|
|
522
|
+
logger.info(wantsInclude ? `icons: ${pkg} registered — bundling ${Object.values(include).flat().length} icon(s)` : `icons: ${pkg} registered (output: 'static' — no include needed)`);
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
iconsData = {
|
|
526
|
+
engine,
|
|
527
|
+
root,
|
|
528
|
+
ui,
|
|
529
|
+
brand,
|
|
530
|
+
weight: o.weight ?? null,
|
|
531
|
+
overrides: o.overrides ?? {},
|
|
532
|
+
sprite: engine === "sprite" || o.sprite ? spriteHref : null
|
|
533
|
+
};
|
|
534
|
+
}
|
|
535
|
+
updateConfig({ vite: { plugins: [virtualIconsPlugin(iconsData ?? {
|
|
536
|
+
engine: "none",
|
|
537
|
+
root,
|
|
538
|
+
ui: ICON_DEFAULTS.ui,
|
|
539
|
+
brand: ICON_DEFAULTS.brand,
|
|
540
|
+
weight: null,
|
|
541
|
+
overrides: {},
|
|
542
|
+
sprite: null
|
|
543
|
+
}), virtualHeadPlugin({
|
|
102
544
|
favicons: !!opts.favicon,
|
|
103
545
|
faviconLinks: opts.favicon ? faviconLinks({
|
|
104
546
|
hasSvg,
|
|
@@ -109,8 +551,14 @@ function getvitops(opts = {}) {
|
|
|
109
551
|
wcBase: "/vitops",
|
|
110
552
|
editor,
|
|
111
553
|
sitemap: sitemapHref,
|
|
554
|
+
fonts: headFonts,
|
|
112
555
|
site: config.site ?? null,
|
|
113
|
-
seo: opts.seo ?? {}
|
|
556
|
+
seo: opts.seo ?? {},
|
|
557
|
+
analytics: opts.analytics ?? {},
|
|
558
|
+
consent: consentEnabled,
|
|
559
|
+
consentCategories: offeredCategories,
|
|
560
|
+
consentPolicyUrl: consentOpts.policyUrl ?? null,
|
|
561
|
+
consentRuntime
|
|
114
562
|
})] } });
|
|
115
563
|
if (opts.css) {
|
|
116
564
|
const format = opts.css.format ?? "tailwind";
|
|
@@ -118,15 +566,26 @@ function getvitops(opts = {}) {
|
|
|
118
566
|
const out = opts.css.out ?? "src/styles";
|
|
119
567
|
const plugins = [vitops({
|
|
120
568
|
input: opts.css.input ?? "design-system.json",
|
|
569
|
+
...opts.css.theme != null ? { theme: opts.css.theme } : {},
|
|
121
570
|
format,
|
|
122
571
|
out,
|
|
123
572
|
...editor ? { editorManifestDir: publicDir } : {},
|
|
124
|
-
...
|
|
573
|
+
...iconsData?.sprite ? { spriteDir: publicDir } : {},
|
|
574
|
+
...opts.legal ? { legal: {
|
|
575
|
+
...opts.legal,
|
|
576
|
+
input: legalInput
|
|
577
|
+
} } : {},
|
|
578
|
+
...opts.media ? { media: opts.media } : {},
|
|
579
|
+
...opts.site ? { site: opts.site } : {},
|
|
580
|
+
...opts.css.systemColorScheme != null ? { systemColorScheme: opts.css.systemColorScheme } : {}
|
|
125
581
|
})];
|
|
126
582
|
if (format === "tailwind") {
|
|
127
583
|
let tailwindcss;
|
|
128
584
|
try {
|
|
129
|
-
({default: tailwindcss} = await import(
|
|
585
|
+
({default: tailwindcss} = await import(
|
|
586
|
+
/* @vite-ignore */
|
|
587
|
+
TAILWIND_VITE
|
|
588
|
+
));
|
|
130
589
|
} catch {
|
|
131
590
|
throw new Error("[getvitops] css.format: 'tailwind' requires `tailwindcss` and `@tailwindcss/vite` in your devDependencies — install them, or switch to css.format: 'css' for the standalone bundle.");
|
|
132
591
|
}
|
|
@@ -144,6 +603,166 @@ function getvitops(opts = {}) {
|
|
|
144
603
|
};
|
|
145
604
|
}
|
|
146
605
|
|
|
606
|
+
//#endregion
|
|
607
|
+
//#region src/lastmod.ts
|
|
608
|
+
/**
|
|
609
|
+
* Real per-page `<lastmod>` for the sitemap, derived from git.
|
|
610
|
+
*
|
|
611
|
+
* This is the one lever that reliably moves Google. It is also load-bearing for
|
|
612
|
+
* `vitops indexing`, which decides what to submit by diffing lastmod against the
|
|
613
|
+
* previous build — with no lastmod, that diff can see pages appear and disappear
|
|
614
|
+
* but never see one *change*, so an edited page is never resubmitted.
|
|
615
|
+
*
|
|
616
|
+
* A plain exported helper rather than magic inside the `sitemap` option, because
|
|
617
|
+
* it carries a caveat the caller needs to see: it shells out to `git`, so it
|
|
618
|
+
* returns nothing useful from a shallow CI clone (`fetch-depth: 1` — the default
|
|
619
|
+
* in actions/checkout) or from a tarball with no history. In that case it warns
|
|
620
|
+
* and emits no dates at all.
|
|
621
|
+
*
|
|
622
|
+
* import vitops, { gitLastmod } from '@getvitops/astro';
|
|
623
|
+
* export default defineConfig({
|
|
624
|
+
* site: 'https://acme.ca',
|
|
625
|
+
* integrations: [vitops({ sitemap: { serialize: await gitLastmod() } })],
|
|
626
|
+
* });
|
|
627
|
+
*
|
|
628
|
+
* **No date beats a wrong date.** An unmatched URL is left alone rather than
|
|
629
|
+
* stamped with the build time: Google weighs lastmod only while it stays
|
|
630
|
+
* consistent with what actually changed, and a site that stamps every page on
|
|
631
|
+
* every deploy teaches it to stop believing the field — site-wide, not just for
|
|
632
|
+
* the pages that were wrong.
|
|
633
|
+
*/
|
|
634
|
+
const exec = promisify(execFile);
|
|
635
|
+
/** Route-bearing sources: file-based pages, and the collections they render. */
|
|
636
|
+
const DEFAULT_DIRS = [
|
|
637
|
+
"src/pages",
|
|
638
|
+
"src/content",
|
|
639
|
+
"src/data"
|
|
640
|
+
];
|
|
641
|
+
const PAGE_EXT = /\.(astro|md|mdx|markdown|html)$/;
|
|
642
|
+
const CONTENT_EXT = /\.(md|mdx|markdown|json|yaml|yml)$/;
|
|
643
|
+
/**
|
|
644
|
+
* `src/pages/blog/index.astro` → `/blog`; `src/pages/index.astro` → `/`.
|
|
645
|
+
*
|
|
646
|
+
* Dynamic routes return `undefined`: one source file backs many URLs, and its
|
|
647
|
+
* commit date describes the template rather than any of them. Stamping every
|
|
648
|
+
* generated page with the template's date is exactly the "wrong date" case above.
|
|
649
|
+
*/
|
|
650
|
+
function routeFromPage(file) {
|
|
651
|
+
const m = /(?:^|\/)src\/pages\/(.+)$/.exec(file);
|
|
652
|
+
if (!m) return void 0;
|
|
653
|
+
let rest = m[1];
|
|
654
|
+
if (!PAGE_EXT.test(rest)) return void 0;
|
|
655
|
+
if (rest.includes("[")) return void 0;
|
|
656
|
+
rest = rest.replace(PAGE_EXT, "");
|
|
657
|
+
if (rest === "index") return "/";
|
|
658
|
+
rest = rest.replace(/\/index$/, "");
|
|
659
|
+
return `/${rest}`;
|
|
660
|
+
}
|
|
661
|
+
/** The slug a content entry is most likely published under. */
|
|
662
|
+
function slugFromContent(file) {
|
|
663
|
+
const m = /(?:^|\/)src\/(?:content|data)\/(.+)$/.exec(file);
|
|
664
|
+
if (!m) return void 0;
|
|
665
|
+
const rest = m[1];
|
|
666
|
+
if (!CONTENT_EXT.test(rest)) return void 0;
|
|
667
|
+
const base = rest.split("/").pop().replace(CONTENT_EXT, "");
|
|
668
|
+
return base === "index" ? void 0 : base;
|
|
669
|
+
}
|
|
670
|
+
const normalise = (pathname) => {
|
|
671
|
+
const p = pathname.replace(/\/+$/, "");
|
|
672
|
+
return p === "" ? "/" : p;
|
|
673
|
+
};
|
|
674
|
+
/**
|
|
675
|
+
* Most recent commit date per file, from one `git log` pass.
|
|
676
|
+
*
|
|
677
|
+
* One process rather than one per file: a `git log -1` per page is fine for
|
|
678
|
+
* twenty pages and unusable for two thousand. `--name-only` interleaves each
|
|
679
|
+
* commit's date with its files, and because `git log` is newest-first the first
|
|
680
|
+
* time a path appears is its latest change.
|
|
681
|
+
*/
|
|
682
|
+
async function commitDates(dirs, cwd) {
|
|
683
|
+
const dates = /* @__PURE__ */ new Map();
|
|
684
|
+
const { stdout } = await exec("git", [
|
|
685
|
+
"log",
|
|
686
|
+
"--date-order",
|
|
687
|
+
"--pretty=format:%x00%cI",
|
|
688
|
+
"--name-only",
|
|
689
|
+
"--",
|
|
690
|
+
...dirs
|
|
691
|
+
], {
|
|
692
|
+
cwd,
|
|
693
|
+
maxBuffer: 64 * 1024 * 1024
|
|
694
|
+
});
|
|
695
|
+
let current;
|
|
696
|
+
for (const line of stdout.split("\n")) {
|
|
697
|
+
if (line.startsWith("\0")) {
|
|
698
|
+
current = line.slice(1).trim();
|
|
699
|
+
continue;
|
|
700
|
+
}
|
|
701
|
+
const file = line.trim();
|
|
702
|
+
if (!file || !current) continue;
|
|
703
|
+
if (!dates.has(file)) dates.set(file, current);
|
|
704
|
+
}
|
|
705
|
+
return dates;
|
|
706
|
+
}
|
|
707
|
+
/**
|
|
708
|
+
* Build a `serialize` function that stamps `lastmod` from git history.
|
|
709
|
+
*
|
|
710
|
+
* Resolution order per URL: an exact file-based route first, then a unique
|
|
711
|
+
* content slug. "Unique" is doing real work — a slug appearing in two collections
|
|
712
|
+
* is dropped rather than guessed at, since picking either one would be a coin
|
|
713
|
+
* flip presented as a fact.
|
|
714
|
+
*/
|
|
715
|
+
async function gitLastmod(options = {}) {
|
|
716
|
+
const cwd = options.cwd ?? process.cwd();
|
|
717
|
+
const warn = options.onWarn ?? ((m) => console.warn(`[getvitops] gitLastmod: ${m}`));
|
|
718
|
+
const dirs = (options.dirs ?? DEFAULT_DIRS).filter((d) => existsSync(`${cwd}/${d}`));
|
|
719
|
+
const identity = (item) => item;
|
|
720
|
+
if (dirs.length === 0) {
|
|
721
|
+
warn(`none of ${(options.dirs ?? DEFAULT_DIRS).join(", ")} exist — no dates emitted`);
|
|
722
|
+
return identity;
|
|
723
|
+
}
|
|
724
|
+
let dates;
|
|
725
|
+
try {
|
|
726
|
+
dates = await commitDates(dirs, cwd);
|
|
727
|
+
} catch (err) {
|
|
728
|
+
warn(`git log failed (${err.message}) — no dates emitted`);
|
|
729
|
+
return identity;
|
|
730
|
+
}
|
|
731
|
+
if (dates.size === 0) {
|
|
732
|
+
warn("git history has no commits touching your source — a shallow clone? Set fetch-depth: 0 in your checkout step. No dates emitted.");
|
|
733
|
+
return identity;
|
|
734
|
+
}
|
|
735
|
+
const byRoute = /* @__PURE__ */ new Map();
|
|
736
|
+
const bySlug = /* @__PURE__ */ new Map();
|
|
737
|
+
const ambiguous = /* @__PURE__ */ new Set();
|
|
738
|
+
for (const [file, date] of dates) {
|
|
739
|
+
const route = routeFromPage(file);
|
|
740
|
+
if (route) {
|
|
741
|
+
byRoute.set(route, date);
|
|
742
|
+
continue;
|
|
743
|
+
}
|
|
744
|
+
const slug = slugFromContent(file);
|
|
745
|
+
if (!slug) continue;
|
|
746
|
+
if (bySlug.has(slug)) ambiguous.add(slug);
|
|
747
|
+
else bySlug.set(slug, date);
|
|
748
|
+
}
|
|
749
|
+
for (const slug of ambiguous) bySlug.delete(slug);
|
|
750
|
+
return (item) => {
|
|
751
|
+
if (item.lastmod) return item;
|
|
752
|
+
let pathname;
|
|
753
|
+
try {
|
|
754
|
+
pathname = normalise(new URL(item.url).pathname);
|
|
755
|
+
} catch {
|
|
756
|
+
return item;
|
|
757
|
+
}
|
|
758
|
+
const date = byRoute.get(pathname) ?? bySlug.get(pathname.split("/").pop() ?? "");
|
|
759
|
+
return date ? {
|
|
760
|
+
...item,
|
|
761
|
+
lastmod: date
|
|
762
|
+
} : item;
|
|
763
|
+
};
|
|
764
|
+
}
|
|
765
|
+
|
|
147
766
|
//#endregion
|
|
148
767
|
//#region src/seo.ts
|
|
149
768
|
/** `en-CA` → `en_CA`; Open Graph wants the underscore form. */
|
|
@@ -186,7 +805,9 @@ function resolveSeo(defaults, props, ctx) {
|
|
|
186
805
|
const { site } = ctx;
|
|
187
806
|
const siteName = props.siteName ?? defaults.siteName ?? null;
|
|
188
807
|
const templated = props.title && defaults.titleTemplate && props.title !== siteName;
|
|
189
|
-
const
|
|
808
|
+
const fallbackTitle = defaults.defaultTitle ?? siteName;
|
|
809
|
+
const title = props.title ? templated ? String(defaults.titleTemplate).replaceAll("%s", props.title) : props.title : fallbackTitle;
|
|
810
|
+
const socialTitle = props.ogTitle ?? props.title ?? fallbackTitle;
|
|
190
811
|
const description = props.description ?? defaults.defaultDescription ?? null;
|
|
191
812
|
const canonical = props.canonical ? absolute(props.canonical, site) : site ? new URL(ctx.pathname + (ctx.search ?? ""), site).href : null;
|
|
192
813
|
const image = absolute(props.image, site) ?? absolute(defaults.openGraph?.image?.url, site);
|
|
@@ -206,7 +827,7 @@ function resolveSeo(defaults, props, ctx) {
|
|
|
206
827
|
});
|
|
207
828
|
};
|
|
208
829
|
og("og:type", type);
|
|
209
|
-
og("og:title",
|
|
830
|
+
og("og:title", socialTitle);
|
|
210
831
|
og("og:description", description);
|
|
211
832
|
og("og:url", canonical);
|
|
212
833
|
og("og:site_name", siteName);
|
|
@@ -238,7 +859,7 @@ function resolveSeo(defaults, props, ctx) {
|
|
|
238
859
|
tw("twitter:card", image ? "summary_large_image" : defaults.twitter?.card ?? "summary");
|
|
239
860
|
tw("twitter:site", defaults.twitter?.site);
|
|
240
861
|
tw("twitter:creator", props.twitterCreator ?? defaults.twitter?.creator);
|
|
241
|
-
tw("twitter:title",
|
|
862
|
+
tw("twitter:title", socialTitle);
|
|
242
863
|
tw("twitter:description", description);
|
|
243
864
|
tw("twitter:image", image);
|
|
244
865
|
tw("twitter:image:alt", image ? imageAlt : null);
|
|
@@ -253,6 +874,7 @@ function resolveSeo(defaults, props, ctx) {
|
|
|
253
874
|
});
|
|
254
875
|
return {
|
|
255
876
|
title,
|
|
877
|
+
socialTitle,
|
|
256
878
|
description,
|
|
257
879
|
canonical,
|
|
258
880
|
robots: resolveRobots(props, defaults),
|
|
@@ -264,5 +886,5 @@ function resolveSeo(defaults, props, ctx) {
|
|
|
264
886
|
}
|
|
265
887
|
|
|
266
888
|
//#endregion
|
|
267
|
-
export { getvitops as default, resolveSeo };
|
|
889
|
+
export { consentCategories, getvitops as default, gitLastmod, resolveAnalytics, resolveSeo, routeFromPage, slugFromContent };
|
|
268
890
|
//# sourceMappingURL=index.mjs.map
|