@getvitops/generator 6.0.0 → 8.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 +432 -22
- package/assets/css/animation.css +9 -0
- package/assets/css/global.css +28 -0
- package/assets/css/index.css +4 -0
- package/assets/css/patterns/counter.css +50 -0
- package/assets/css/patterns/forms.css +1 -1
- package/assets/css/patterns/gallery.css +64 -0
- package/assets/css/patterns/lightbox.css +97 -98
- package/assets/css/patterns/list.css +1 -1
- package/assets/css/patterns/review.css +91 -0
- package/assets/css/patterns/tag.css +1 -2
- package/assets/css/patterns/tree.css +111 -8
- package/assets/css/utilities.css +17 -0
- package/assets/js/elements.js +7 -844
- package/assets/js/polyfills/invokers-polyfill.js +1 -0
- package/assets/js/polyfills/template-shadowroot.js +1 -14
- package/assets/js/polyfills.js +1 -1
- package/config.schema.json +169 -1
- package/dist/index.d.mts +138 -2
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +218 -20
- package/dist/index.mjs.map +1 -1
- package/package.json +15 -4
package/dist/index.mjs
CHANGED
|
@@ -9,6 +9,7 @@ import { LIGHTNESS_LADDER, contrastLc, generateOklchPalette, hexToOklch, oklchSt
|
|
|
9
9
|
import { AD_PLATFORMS, AD_PROVIDER_KEYS } from "@getvitops/utils/ads";
|
|
10
10
|
import { generateIconInclude, nodesToHtml, resolveIcon, t } from "@getvitops/utils";
|
|
11
11
|
import { getIconData, iconToSVG, replaceIDs } from "@iconify/utils";
|
|
12
|
+
import { resolveSitemapUrl } from "@getvitops/utils/indexing";
|
|
12
13
|
|
|
13
14
|
//#region src/shared.ts
|
|
14
15
|
/**
|
|
@@ -187,7 +188,9 @@ const TW_CLASH = new Set([
|
|
|
187
188
|
"text-wrap",
|
|
188
189
|
"text-nowrap",
|
|
189
190
|
"text-balance",
|
|
190
|
-
"text-pretty"
|
|
191
|
+
"text-pretty",
|
|
192
|
+
"rounded-none",
|
|
193
|
+
"rounded-full"
|
|
191
194
|
]);
|
|
192
195
|
|
|
193
196
|
//#endregion
|
|
@@ -326,7 +329,7 @@ const LADDER_TOLERANCE = .03;
|
|
|
326
329
|
function ladderWarnings(palette) {
|
|
327
330
|
const warnings = [];
|
|
328
331
|
for (const [hue, expanded] of Object.entries(palette)) for (const [step, deviation] of Object.entries(expanded.deviations ?? {})) {
|
|
329
|
-
if (Math.abs(deviation) <=
|
|
332
|
+
if (Math.abs(deviation) <= LADDER_TOLERANCE) continue;
|
|
330
333
|
const dir = deviation > 0 ? "lighter" : "darker";
|
|
331
334
|
warnings.push(`colors.palette.${hue}: the colour pinned at step ${step} is ${Math.abs(deviation).toFixed(3)} ${dir} than the shared lightness ladder — this hue will read ${dir} than others at ${step}. Legal, but move it to a neighbouring step if that wasn't intended.`);
|
|
332
335
|
}
|
|
@@ -1257,6 +1260,18 @@ const OpeningHoursSchema = z.object({
|
|
|
1257
1260
|
opens: z.string().check(z.regex(/^\d{2}:\d{2}$/)),
|
|
1258
1261
|
closes: z.string().check(z.regex(/^\d{2}:\d{2}$/))
|
|
1259
1262
|
});
|
|
1263
|
+
/**
|
|
1264
|
+
* A dated deviation from the recurring `hours` — holiday hours, a one-off
|
|
1265
|
+
* closure, a temporary schedule. Same schema.org shape
|
|
1266
|
+
* (`OpeningHoursSpecification`), distinguished by carrying `validFrom`. A
|
|
1267
|
+
* full closure omits `opens`/`closes` rather than encoding an open interval.
|
|
1268
|
+
*/
|
|
1269
|
+
const SpecialHoursSchema = z.object({
|
|
1270
|
+
validFrom: IsoDate,
|
|
1271
|
+
validThrough: z.optional(IsoDate),
|
|
1272
|
+
opens: z.optional(z.string().check(z.regex(/^\d{2}:\d{2}$/))),
|
|
1273
|
+
closes: z.optional(z.string().check(z.regex(/^\d{2}:\d{2}$/)))
|
|
1274
|
+
});
|
|
1260
1275
|
const LocalBusinessType = z.enum([
|
|
1261
1276
|
"LocalBusiness",
|
|
1262
1277
|
"Store",
|
|
@@ -1272,6 +1287,11 @@ const LocalBusinessType = z.enum([
|
|
|
1272
1287
|
"RealEstateAgent",
|
|
1273
1288
|
"Organization"
|
|
1274
1289
|
]);
|
|
1290
|
+
const ListingPlatform = z.enum([
|
|
1291
|
+
"google",
|
|
1292
|
+
"bing",
|
|
1293
|
+
"apple"
|
|
1294
|
+
]);
|
|
1275
1295
|
const LocationSchema = z.object({
|
|
1276
1296
|
slug: z.optional(LocalizableSchema),
|
|
1277
1297
|
name: LocalizableSchema,
|
|
@@ -1280,13 +1300,17 @@ const LocationSchema = z.object({
|
|
|
1280
1300
|
address: z.optional(PostalAddressSchema),
|
|
1281
1301
|
geo: z.optional(GeoSchema),
|
|
1282
1302
|
hours: z.optional(z.array(OpeningHoursSchema)),
|
|
1303
|
+
hoursSpecial: desc(z.optional(z.array(SpecialHoursSchema)), "Holiday/temporary hours, as dated deviations from the recurring `hours` — distinct from it because these carry a date range rather than a weekday."),
|
|
1283
1304
|
type: z.optional(LocalBusinessType),
|
|
1284
1305
|
description: z.optional(LocalizableSchema),
|
|
1285
1306
|
areaServed: z.optional(z.array(z.string())),
|
|
1286
1307
|
priceRange: z.optional(z.string()),
|
|
1287
1308
|
paymentAccepted: z.optional(z.array(z.string())),
|
|
1288
1309
|
currenciesAccepted: z.optional(Iso4217),
|
|
1289
|
-
knowsLanguage: z.optional(z.array(z.string()))
|
|
1310
|
+
knowsLanguage: z.optional(z.array(z.string())),
|
|
1311
|
+
photos: desc(z.optional(z.array(ImageRefSchema)), "Location photos (logo, storefront, interior, …) → JSON-LD `image`. Every listing platform (GBP, Bing Places, Apple Business Connect) wants these; none can derive them."),
|
|
1312
|
+
sameAs: desc(z.optional(z.array(z.url())), "This location's own profile URLs (its Google Business Profile, Bing Places listing, Apple Business Connect location, Facebook page, …) → JSON-LD `sameAs`. Distinct from `organization.sameAs`, which is the company overall — a multi-location org's locations do not share one GBP listing."),
|
|
1313
|
+
listings: desc(z.optional(z.partialRecord(ListingPlatform, z.string())), "External listing IDs for this location, keyed by platform — the Google Business Profile location id, the Bing Places business id, the Apple Business Connect location id. Not consumed by the generator today; recorded here so a future sync has a stable id to match against instead of risking a duplicate listing.")
|
|
1290
1314
|
});
|
|
1291
1315
|
const OrganizationSchema = z.object({
|
|
1292
1316
|
name: desc(z.optional(LocalizableSchema), "Trading name, as a reader would recognise it (e.g. \"Acme\"). Localisable."),
|
|
@@ -1595,6 +1619,27 @@ const AdPropertySchema = z.object({
|
|
|
1595
1619
|
enabled: desc(z.optional(z.boolean()), "Set false to keep the property on record without emitting its tag.")
|
|
1596
1620
|
});
|
|
1597
1621
|
/**
|
|
1622
|
+
* How one location's Google reviews are selected and ordered for display.
|
|
1623
|
+
*
|
|
1624
|
+
* Keyed by `organization.locations` key, mirroring `site.ads`/`site.searchConsole`
|
|
1625
|
+
* — `vitops reviews` needs to know which physical location a fetched review set
|
|
1626
|
+
* belongs to, and that record already exists.
|
|
1627
|
+
*/
|
|
1628
|
+
const ReviewLocationSchema = z.object({
|
|
1629
|
+
accountId: desc(z.optional(z.string()), "GBP account resource id (`accounts/…`), overriding `site.reviews.accountId` for this location."),
|
|
1630
|
+
locationId: desc(z.string(), "GBP location resource id (`locations/…`) to fetch reviews for."),
|
|
1631
|
+
featured: desc(z.optional(z.array(z.string())), "Review ids to always include, in this order, ahead of everything else automatically selected."),
|
|
1632
|
+
minRating: desc(z.optional(z.number()), "Drop reviews below this star rating (default 4) from the automatic (non-featured) selection."),
|
|
1633
|
+
limit: desc(z.optional(z.number()), "Keep at most this many reviews after filtering."),
|
|
1634
|
+
sort: desc(z.optional(z.enum(["recent", "rating"])), "Ordering for the non-featured remainder (default `recent`).")
|
|
1635
|
+
});
|
|
1636
|
+
const ReviewsSchema = z.object({
|
|
1637
|
+
accountId: desc(z.optional(z.string()), "Default GBP account resource id (`accounts/…`) for locations that do not override it."),
|
|
1638
|
+
locations: desc(z.record(z.string(), ReviewLocationSchema), "Which `organization.locations` to fetch Google reviews for, and how each one’s reviews are filtered/ordered. `vitops reviews` writes the result to `.vitops/reviews.json`."),
|
|
1639
|
+
schema: desc(z.optional(z.boolean()), "Emit `Review` JSON-LD for these reviews (default false). Off by default: Google’s structured-data policy treats a business marking up reviews of itself as ineligible for review rich results, and risks a manual action."),
|
|
1640
|
+
avatars: desc(z.optional(z.boolean()), "Render the reviewer’s Google-hosted avatar photo (default false). Off by default: every visitor would otherwise issue a third-party request to googleusercontent.com, and the URLs are not guaranteed to stay live.")
|
|
1641
|
+
});
|
|
1642
|
+
/**
|
|
1598
1643
|
* Which template set the generated legal documents are written against. This is
|
|
1599
1644
|
* a closed enum rather than a free string because an unrecognised value would
|
|
1600
1645
|
* otherwise silently fall back to the wrong body of law; `validateConfig` rejects
|
|
@@ -1646,7 +1691,8 @@ const SiteSectionSchema = z.object({
|
|
|
1646
1691
|
})), "Canonical domain + redirecting aliases."),
|
|
1647
1692
|
dns: desc(z.optional(z.record(z.string(), DnsDomainSchema)), "Desired DNS state per domain (provider, nameservers, records) — declarative reference, not applied automatically."),
|
|
1648
1693
|
cloudflare: desc(z.optional(z.record(z.string(), z.unknown())), "Deliberate escape hatch for provider-specific Cloudflare settings — out of scope to model here."),
|
|
1649
|
-
|
|
1694
|
+
google: desc(z.optional(z.object({ project: desc(z.string(), "Google Cloud project id that API usage is attributed to (quota and billing). One project per site keeps usage separate — Search Console today, Maps and anything else later. Sent as `x-goog-user-project`, which is required when the credential is a *user* one (an ADC login from `gcloud auth application-default login`, or `VITOPS_GOOGLE_*`): those use a shared OAuth client that carries no project of its own. A service-account credential belongs to a project already, so this is not needed for it. The identity must hold `serviceusage.services.use` on the project, and the APIs being called must be enabled there.") })), "This site's Google Cloud project. Not a secret — the credentials stay in the environment."),
|
|
1695
|
+
searchConsole: desc(z.optional(z.record(z.string(), SearchConsoleSetupSchema)), "Domains to onboard as Google Search Console *domain properties* via `vitops search setup`, keyed by bare hostname (mirrors `dns`). Credentials come from the environment (CLOUDFLARE_API_TOKEN + the Google OAuth vars) — never in this file. Set `google.project` when using a user credential."),
|
|
1650
1696
|
environments: desc(z.record(z.string(), EnvironmentSchema), "Deploy environments (production, dev, …): URL, API origin, analytics toggle, robots policy, active A/B variant."),
|
|
1651
1697
|
abTesting: desc(z.optional(z.object({
|
|
1652
1698
|
enabled: z.boolean(),
|
|
@@ -1700,6 +1746,7 @@ const SiteSectionSchema = z.object({
|
|
|
1700
1746
|
platforms: desc(z.optional(z.array(z.string())), "Informational list of ad platforms in use. Capture recognises every known click-ID parameter regardless.")
|
|
1701
1747
|
})), "Ad-click attribution for conversion tracking."),
|
|
1702
1748
|
ads: desc(z.optional(z.partialRecord(AdProviderSchema, AdPropertySchema)), "Ad properties this site is linked to, keyed by platform. Read by `vitops ads setup` (ensure the DNS verification record), `vitops ads tags` (emit the consent-gated pixel), and the generated cookie notice (disclose what each pixel stores). Credentials never live here — the DNS write uses CLOUDFLARE_API_TOKEN from the environment."),
|
|
1749
|
+
reviews: desc(z.optional(ReviewsSchema), "Google Business Profile reviews to cache and display, keyed by `organization.locations`. Read by `vitops reviews`. Credentials never live here — a Google OAuth credential comes from the environment, same as `site.searchConsole`."),
|
|
1703
1750
|
security: desc(z.optional(z.object({ turnstile: z.optional(z.object({ siteKey: desc(z.optional(z.string()), "Cloudflare Turnstile site key (public).") })) })), "Security integrations (bot protection)."),
|
|
1704
1751
|
legal: desc(z.optional(z.object({
|
|
1705
1752
|
jurisdiction: desc(z.optional(JurisdictionSchema), "Which legal template set the generated documents use. Default `ca`."),
|
|
@@ -1986,6 +2033,17 @@ function validateConfig(input) {
|
|
|
1986
2033
|
if (entry.domainVerification != null && entry.domain == null && site.domains?.canonical == null) errors.push(issue([...at, "domain"], `site.ads.${provider}.domainVerification needs a domain to put the record on — set site.domains.canonical, or state site.ads.${provider}.domain explicitly`));
|
|
1987
2034
|
if (entry.accountId == null && entry.pixelId == null) errors.push(issue(at, `site.ads.${provider} identifies nothing — set accountId (the advertising account) and/or pixelId (the tag: ${platform.tag.needs} is what its snippet needs)`));
|
|
1988
2035
|
}
|
|
2036
|
+
for (const [key, entry] of Object.entries(site.reviews?.locations ?? {})) {
|
|
2037
|
+
const at = [
|
|
2038
|
+
"site",
|
|
2039
|
+
"reviews",
|
|
2040
|
+
"locations",
|
|
2041
|
+
key
|
|
2042
|
+
];
|
|
2043
|
+
if (!has(org?.locations, key)) errors.push(issue(at, `"${key}" is not an organization.locations key`));
|
|
2044
|
+
else if (org?.locations?.[key]?.listings?.google == null) errors.push(issue(at, `organization.locations.${key} has no listings.google id — record the GBP location id there first`));
|
|
2045
|
+
if (entry.accountId == null && site.reviews?.accountId == null) errors.push(issue([...at, "accountId"], `no accountId — set site.reviews.accountId or site.reviews.locations.${key}.accountId`));
|
|
2046
|
+
}
|
|
1989
2047
|
if (errors.length) return {
|
|
1990
2048
|
ok: false,
|
|
1991
2049
|
data: void 0,
|
|
@@ -2212,13 +2270,14 @@ const TIERS = {
|
|
|
2212
2270
|
"tree",
|
|
2213
2271
|
"tree__item",
|
|
2214
2272
|
"tree__content",
|
|
2273
|
+
"tree__summary",
|
|
2215
2274
|
"tree__toggle",
|
|
2216
2275
|
"tree__label"
|
|
2217
2276
|
], "patterns/tree.css", true),
|
|
2218
2277
|
wc: {
|
|
2219
2278
|
tag: "wc-tree",
|
|
2220
2279
|
registered: true,
|
|
2221
|
-
adds: "filter, expand/collapse all, hash deep-linking"
|
|
2280
|
+
adds: "WAI-ARIA tree semantics, roving-tabindex keyboard navigation, filter, expand/collapse all, hash deep-linking"
|
|
2222
2281
|
},
|
|
2223
2282
|
astro: a("Tree", "wc"),
|
|
2224
2283
|
use: "`<Tree items={…} />` — it emits `<wc-tree>` itself, so do NOT add your own wrapper."
|
|
@@ -2240,6 +2299,21 @@ const TIERS = {
|
|
|
2240
2299
|
bricks: "vitops-carousel",
|
|
2241
2300
|
use: "`<Carousel slides={…} label=\"…\" />` — it emits `<wc-carousel>` itself, so do NOT add your own wrapper. `.carousel` is the shell and `.carousel__track` the scroller; add `loop` for the cloned infinite strip (`autoplay` implies it)."
|
|
2242
2301
|
},
|
|
2302
|
+
gallery: {
|
|
2303
|
+
css: c([
|
|
2304
|
+
"gallery",
|
|
2305
|
+
"gallery__item",
|
|
2306
|
+
"gallery__trigger",
|
|
2307
|
+
"gallery__thumb"
|
|
2308
|
+
], "patterns/gallery.css", true),
|
|
2309
|
+
wc: {
|
|
2310
|
+
tag: "wc-gallery",
|
|
2311
|
+
registered: true,
|
|
2312
|
+
adds: "a real `showModal()` open, neighbour preload and the thumbnail→full-image view-transition morph"
|
|
2313
|
+
},
|
|
2314
|
+
astro: a("Gallery", "wc"),
|
|
2315
|
+
use: "`<Gallery images={…} label=\"…\" />` — it emits `<wc-gallery>` wrapping a `<Carousel />` (which emits its own `<wc-carousel>`) itself, so do NOT add your own wrapper around either. Each thumbnail is a plain `<a href=\"#slide-id\">` into the carousel's own `<li id=\"slide-id\">` inside a single `<dialog class=\"lightbox-dialog\">` (see `lightbox`), so it is a working modal lightbox — WITH prev/next — before any script runs. Stepping between images is the carousel's job, not the gallery's."
|
|
2316
|
+
},
|
|
2243
2317
|
entries: {
|
|
2244
2318
|
css: c([
|
|
2245
2319
|
"entries__table",
|
|
@@ -2318,7 +2392,22 @@ const TIERS = {
|
|
|
2318
2392
|
registered: true,
|
|
2319
2393
|
adds: "fills the track with enough copies to scroll seamlessly"
|
|
2320
2394
|
},
|
|
2321
|
-
|
|
2395
|
+
astro: a("Marquee", "wc"),
|
|
2396
|
+
use: "`<Marquee>` around one `.marquee__content`, or `<wc-marquee>` by hand. Scrolls via CSS alone; JS only removes the seam."
|
|
2397
|
+
},
|
|
2398
|
+
counter: {
|
|
2399
|
+
css: c([
|
|
2400
|
+
"counter",
|
|
2401
|
+
"counter__value",
|
|
2402
|
+
"counter__presentation"
|
|
2403
|
+
], "patterns/counter.css"),
|
|
2404
|
+
wc: {
|
|
2405
|
+
tag: "wc-counter",
|
|
2406
|
+
registered: true,
|
|
2407
|
+
adds: "animates from a start value to the value already in the fallback text, on intersection"
|
|
2408
|
+
},
|
|
2409
|
+
astro: a("Counter", "wc"),
|
|
2410
|
+
use: "`<Counter value=\"94%\" />` — it emits `<wc-counter>` itself, so do NOT add your own wrapper. The final value is parsed from the fallback text, never passed separately."
|
|
2322
2411
|
},
|
|
2323
2412
|
"multi-field": {
|
|
2324
2413
|
css: c([
|
|
@@ -2579,8 +2668,12 @@ const TIERS = {
|
|
|
2579
2668
|
use: "`.notification` with a role variant; wrap in `<wc-dismissable>` to make it dismissable."
|
|
2580
2669
|
},
|
|
2581
2670
|
lightbox: {
|
|
2582
|
-
css: c([
|
|
2583
|
-
|
|
2671
|
+
css: c([
|
|
2672
|
+
"lightbox-dialog",
|
|
2673
|
+
"lightbox-dialog__track",
|
|
2674
|
+
"lightbox-dialog__close"
|
|
2675
|
+
], "patterns/lightbox.css", true),
|
|
2676
|
+
use: "The single `<dialog class=\"lightbox-dialog\">` a gallery opens — it wraps ONE `.carousel` (`lightbox-dialog__track`) of every image, not one dialog per image. Use `gallery` for the thumbnail grid that drives it — `<Gallery />` emits both, and the carousel inside already owns prev/next/dots/keys/swipe."
|
|
2584
2677
|
},
|
|
2585
2678
|
comment: {
|
|
2586
2679
|
css: c([
|
|
@@ -2590,6 +2683,17 @@ const TIERS = {
|
|
|
2590
2683
|
], "patterns/comment.css", true),
|
|
2591
2684
|
use: "`.comment` inside `.comment-thread`."
|
|
2592
2685
|
},
|
|
2686
|
+
review: {
|
|
2687
|
+
css: c([
|
|
2688
|
+
"review",
|
|
2689
|
+
"review__stars",
|
|
2690
|
+
"review__body",
|
|
2691
|
+
"review__reply",
|
|
2692
|
+
"review__source"
|
|
2693
|
+
], "patterns/review.css", true),
|
|
2694
|
+
astro: a("ReviewCard", "css"),
|
|
2695
|
+
use: "`<ReviewCard review={…} />` — one card, from `vitops reviews`' `.vitops/reviews.json` cache. Put several inside `<Carousel>` slides or `<Marquee>` items; there is no group component and no `wc-review`."
|
|
2696
|
+
},
|
|
2593
2697
|
tabs: {
|
|
2594
2698
|
css: c(["tabs"], void 0, true),
|
|
2595
2699
|
use: "Generated pattern only; no structural partial and no component yet."
|
|
@@ -3451,6 +3555,9 @@ and a heading and a footer, \`subgrid\` is the one that makes them line up. Neve
|
|
|
3451
3555
|
- **Display** — \`block\`, \`inline\`, \`inline-block\`, \`flex\`, \`grid\`, \`hidden\`
|
|
3452
3556
|
(breakpoint-prefixable, e.g. \`md-hidden\`).
|
|
3453
3557
|
- **Accessibility** — \`sr-only\` / \`not-sr-only\` (breakpoint-prefixable).
|
|
3558
|
+
- **Shape** — \`rounded-none\`, \`rounded-full\` (the two radii with no design-token meaning of
|
|
3559
|
+
their own; every other radius comes from the pattern token cascade, not a utility). Tailwind
|
|
3560
|
+
ships its own \`rounded-*\` scale, so these are css/bricks only.
|
|
3454
3561
|
- **State hooks** — \`is-active\`, \`is-open\` (styling flags toggled by JS / native state).
|
|
3455
3562
|
|
|
3456
3563
|
## Spacing
|
|
@@ -4007,11 +4114,12 @@ split in two rather than shelved whole. \`layout.css\` (patterns) and \`layout-u
|
|
|
4007
4114
|
format reaches the same arrangement by its own route — patterns in \`@layer components\`,
|
|
4008
4115
|
utilities as \`@utility\`.
|
|
4009
4116
|
|
|
4010
|
-
|
|
4011
|
-
|
|
4012
|
-
|
|
4013
|
-
|
|
4014
|
-
|
|
4117
|
+
The \`typography.headings\` bare-element bindings (\`h1\`, \`h2\`, \`body\`) are a DEFAULT for an
|
|
4118
|
+
unstyled element, not a utility, so they emit into their own chunk in \`vitops.base\` — below
|
|
4119
|
+
every pattern — rather than alongside \`.font-<role>\` in \`vitops.utilities\`. That is what lets
|
|
4120
|
+
\`<h2 class="pull-quote">\` keep the pattern's font-size in every format: the bare \`h2\` binding
|
|
4121
|
+
never outranks \`.pull-quote\`, regardless of specificity, because a layer decides before
|
|
4122
|
+
specificity is ever asked.
|
|
4015
4123
|
|
|
4016
4124
|
## \`bricks\` — WordPress / Bricks Builder payload
|
|
4017
4125
|
|
|
@@ -5009,6 +5117,7 @@ grants**, tracking where each command runs:
|
|
|
5009
5117
|
| --- | --- | --- |
|
|
5010
5118
|
| **Service account** (JWT bearer) | \`VITOPS_GSC_SERVICE_ACCOUNT\` (inline JSON) or \`GOOGLE_APPLICATION_CREDENTIALS\` (path) | \`search notify\` — never expires, right for CI |
|
|
5011
5119
|
| **User OAuth** (refresh token) | \`VITOPS_GOOGLE_CLIENT_ID\` / \`_CLIENT_SECRET\` / \`_REFRESH_TOKEN\` | \`search setup\` — **required** |
|
|
5120
|
+
| **User OAuth via ADC** (fallback) | none — a \`gcloud auth application-default login\` credential | either, when no \`VITOPS_GOOGLE_*\` is set |
|
|
5012
5121
|
|
|
5013
5122
|
Cloudflare uses \`CLOUDFLARE_API_TOKEN\` (a \`Zone:DNS:Edit\` token; the standard "Edit zone DNS"
|
|
5014
5123
|
template also carries the \`Zone:Read\` the zone-by-name lookup needs).
|
|
@@ -5022,6 +5131,33 @@ revoked, and for an OAuth client still in *Testing* publishing status Google exp
|
|
|
5022
5131
|
Console does not care which identity calls it, and someone who has run \`search setup\` already
|
|
5023
5132
|
holds a Google credential.
|
|
5024
5133
|
|
|
5134
|
+
**Being logged in to gcloud is enough**, which also sidesteps the 7-day expiry above — the Cloud
|
|
5135
|
+
SDK client is published, so its refresh token doesn't rot. The Search Console scopes are not in
|
|
5136
|
+
the default ADC set, so ask for them:
|
|
5137
|
+
|
|
5138
|
+
\`\`\`
|
|
5139
|
+
gcloud auth application-default login \\
|
|
5140
|
+
--scopes=openid,https://www.googleapis.com/auth/siteverification,\\
|
|
5141
|
+
https://www.googleapis.com/auth/webmasters,https://www.googleapis.com/auth/cloud-platform
|
|
5142
|
+
\`\`\`
|
|
5143
|
+
|
|
5144
|
+
Explicit \`VITOPS_GOOGLE_*\` vars win over it: a stale local login silently overriding a
|
|
5145
|
+
deliberate CI secret is the worst available precedence.
|
|
5146
|
+
|
|
5147
|
+
Two things follow from ADC using a **shared** OAuth client, and both fail as a bare \`403\`:
|
|
5148
|
+
|
|
5149
|
+
- **It owns no project, so \`site.google.project\` is required** — sent as \`x-goog-user-project\`.
|
|
5150
|
+
Without it: *"requires a quota project, which is not set by default"*. A service account
|
|
5151
|
+
belongs to a project already, so the header is **not** sent for one; attaching a different
|
|
5152
|
+
project would newly require \`serviceusage.services.use\` there and break a working credential.
|
|
5153
|
+
- **That project needs the APIs enabled** (\`searchconsole.googleapis.com\`,
|
|
5154
|
+
\`siteverification.googleapis.com\`) and \`serviceusage.services.use\` for the identity. One
|
|
5155
|
+
project per site keeps usage and billing separate — one grant per site, one admin identity.
|
|
5156
|
+
|
|
5157
|
+
\`GOOGLE_APPLICATION_CREDENTIALS\` carries **either kind**, since it is both Google's convention
|
|
5158
|
+
and where gcloud writes ADC. It is discriminated on the JSON's \`type\` field, never on which
|
|
5159
|
+
fields are present.
|
|
5160
|
+
|
|
5025
5161
|
⚠️ Do not add \`googleapis\` — an enormous dependency for a handful of REST endpoints in a CLI
|
|
5026
5162
|
that installs into every consumer project. The JWT is minted with ~30 lines of \`node:crypto\`.
|
|
5027
5163
|
`;
|
|
@@ -5322,7 +5458,7 @@ function emitDesignMd(ctx) {
|
|
|
5322
5458
|
if (pDefaults.br) pushRadius("DEFAULT", pDefaults.br);
|
|
5323
5459
|
for (const [n, v] of Object.entries(pRadii)) pushRadius(n, v);
|
|
5324
5460
|
const roundedNames = new Set(rounded.map(([k]) => k));
|
|
5325
|
-
/** `var(--br-default)` → `{rounded.DEFAULT}`, `var(--br-
|
|
5461
|
+
/** `var(--br-default)` → `{rounded.DEFAULT}`, `var(--br-circle, 50%)` → `{rounded.circle}`. */
|
|
5326
5462
|
const roundedRef = (value) => {
|
|
5327
5463
|
const v = varName(value);
|
|
5328
5464
|
const bare = v?.startsWith("br-") ? v.slice(3) : void 0;
|
|
@@ -7281,8 +7417,10 @@ function build(ds, format, assetsDir, buildOpts = {}) {
|
|
|
7281
7417
|
const headings = typography?.headings ?? {};
|
|
7282
7418
|
let typo = `/* GENERATED typography roles — do not edit by hand. */\n`;
|
|
7283
7419
|
for (const [role, spec] of Object.entries(roles)) typo += `.font-${role} {\n${decls(roleDecls(role, spec))}\n}\n`;
|
|
7284
|
-
for (const [tag, role] of Object.entries(headings)) if (roles[role]) typo += `${tag} {\n${decls(roleDecls(role, roles[role]))}\n}\n`;
|
|
7285
7420
|
generated["typography.css"] = typo;
|
|
7421
|
+
let typoHeadings = `/* GENERATED typography.headings bare-element bindings — do not edit by hand. */\n`;
|
|
7422
|
+
for (const [tag, role] of Object.entries(headings)) if (roles[role]) typoHeadings += `${tag} {\n${decls(roleDecls(role, roles[role]))}\n}\n`;
|
|
7423
|
+
generated["typography-headings.css"] = typoHeadings;
|
|
7286
7424
|
}
|
|
7287
7425
|
const stateRules = (sel, role, states, colorProp = "background-color") => {
|
|
7288
7426
|
let out = "";
|
|
@@ -8038,6 +8176,7 @@ const CHUNK_LAYER = {
|
|
|
8038
8176
|
"global.css": "vitops.base",
|
|
8039
8177
|
"generated/type-tokens.css": "vitops.base",
|
|
8040
8178
|
"generated/tokens.css": "vitops.base",
|
|
8179
|
+
"generated/typography-headings.css": "vitops.base",
|
|
8041
8180
|
"generated/color.css": "vitops.utilities",
|
|
8042
8181
|
"generated/shadows.css": "vitops.utilities",
|
|
8043
8182
|
"generated/typography.css": "vitops.utilities",
|
|
@@ -8207,6 +8346,68 @@ function loadConfigFile(input, siteEnv) {
|
|
|
8207
8346
|
return resolveConfig(typeof input === "string" ? JSON.parse(readFileSync(input, "utf8")) : input, siteEnv);
|
|
8208
8347
|
}
|
|
8209
8348
|
|
|
8349
|
+
//#endregion
|
|
8350
|
+
//#region src/robots.ts
|
|
8351
|
+
/**
|
|
8352
|
+
* `robots.txt`, generated from the same fact the indexing gate and the
|
|
8353
|
+
* `<meta name="robots">` tag already read.
|
|
8354
|
+
*
|
|
8355
|
+
* Before this, nothing in the toolchain emitted one at all — a staging
|
|
8356
|
+
* environment with `robots: "noindex,nofollow"` got a `<meta>` tag and a
|
|
8357
|
+
* blocked `search notify` run, but a crawler reading `/robots.txt` directly
|
|
8358
|
+
* saw nothing and was free to index it anyway. `plan()`
|
|
8359
|
+
* (`@getvitops/utils/indexing`) already treats `noindex` in the resolved
|
|
8360
|
+
* environment's directive as a hard stop for notifying search engines; this is
|
|
8361
|
+
* the same fact, read the same way, applied to the file crawlers actually ask
|
|
8362
|
+
* for first.
|
|
8363
|
+
*
|
|
8364
|
+
* A sibling of `generateLegal`, not a `generate()` format: this renders from a
|
|
8365
|
+
* **`Config`** (a `site.environments` entry), not a `DesignSystem`, so bolting
|
|
8366
|
+
* it onto `generate()` would be a format that ignores its own input.
|
|
8367
|
+
*
|
|
8368
|
+
* Pure: no filesystem, no clock, no shared state. The CLI writes it to stdout
|
|
8369
|
+
* or `--out`; the Astro integration writes `public/robots.txt` directly,
|
|
8370
|
+
* mirroring how it already writes the IndexNow key file.
|
|
8371
|
+
*/
|
|
8372
|
+
/**
|
|
8373
|
+
* A directive string (`site.environments[env].robots` or `site.seo.robots`)
|
|
8374
|
+
* blocks indexing if it contains `noindex` — matching `plan()`'s own check in
|
|
8375
|
+
* `@getvitops/utils/indexing`, so the two can't read the same fact two ways.
|
|
8376
|
+
*/
|
|
8377
|
+
function isBlocked(directive) {
|
|
8378
|
+
return /noindex/i.test(directive ?? "");
|
|
8379
|
+
}
|
|
8380
|
+
/**
|
|
8381
|
+
* Render `robots.txt` for one environment.
|
|
8382
|
+
*
|
|
8383
|
+
* A blocked environment gets `Disallow: /` for every user agent and no sitemap
|
|
8384
|
+
* line — publishing the sitemap of a site asking not to be indexed would be
|
|
8385
|
+
* the more damaging way to get this wrong, handing every listed URL to a
|
|
8386
|
+
* crawler that then finds itself blocked from all of them, but has already
|
|
8387
|
+
* seen where they are. An allowed environment gets a permissive robots.txt
|
|
8388
|
+
* plus a `Sitemap:` line, using the exact URL `vitops search notify` would
|
|
8389
|
+
* submit — `site.seo.indexing.sitemapUrl`, or `<canonical>/sitemap-index.xml`,
|
|
8390
|
+
* the same default `@astrojs/sitemap` produces.
|
|
8391
|
+
*/
|
|
8392
|
+
function renderRobotsTxt(cfg, options = {}) {
|
|
8393
|
+
const envName = options.env ?? "production";
|
|
8394
|
+
const site = cfg.site;
|
|
8395
|
+
const env = site.environments?.[envName];
|
|
8396
|
+
const directive = env?.robots ?? site.seo?.robots;
|
|
8397
|
+
const lines = ["User-agent: *"];
|
|
8398
|
+
if (isBlocked(directive)) {
|
|
8399
|
+
lines.push("Disallow: /");
|
|
8400
|
+
return lines.join("\n") + "\n";
|
|
8401
|
+
}
|
|
8402
|
+
lines.push("Allow: /");
|
|
8403
|
+
const sitemapUrl = resolveSitemapUrl({
|
|
8404
|
+
canonical: env?.url ?? site.domains?.canonical,
|
|
8405
|
+
sitemapUrl: site.seo?.indexing?.sitemapUrl
|
|
8406
|
+
});
|
|
8407
|
+
if (sitemapUrl) lines.push("", `Sitemap: ${sitemapUrl}`);
|
|
8408
|
+
return lines.join("\n") + "\n";
|
|
8409
|
+
}
|
|
8410
|
+
|
|
8210
8411
|
//#endregion
|
|
8211
8412
|
//#region src/index.ts
|
|
8212
8413
|
/**
|
|
@@ -8308,10 +8509,7 @@ function defaultConfig() {
|
|
|
8308
8509
|
p: "1rem",
|
|
8309
8510
|
fs: "1rem"
|
|
8310
8511
|
},
|
|
8311
|
-
radii: {
|
|
8312
|
-
pill: "999px",
|
|
8313
|
-
circle: "50%"
|
|
8314
|
-
},
|
|
8512
|
+
radii: { circle: "50%" },
|
|
8315
8513
|
groups: {
|
|
8316
8514
|
control: {
|
|
8317
8515
|
ds: "var(--ds-default)",
|
|
@@ -8486,5 +8684,5 @@ function defaultConfig() {
|
|
|
8486
8684
|
}
|
|
8487
8685
|
|
|
8488
8686
|
//#endregion
|
|
8489
|
-
export { AD_PROVIDERS, BASE_HOOK, CONFIG_SCHEMA_URL, CONTRAST, ConfigSchema, DARK_SEL, DOC_ORDER, DOC_SLUGS, DesignSystemSchema, JURISDICTIONS, JURISDICTION_COUNTRIES, KNOWN_PROCESSORS, NUMERIC_STEPS, REQUIRED_ROLES, ROLE_TOKEN_KEYS, SCHEMA_LOCAL_PATH, SCHEMA_URL, SiteFontSchema, TEMPLATES, TIERS, TIER_NAMES, TW_CLASH, buildIconSprite, configJsonSchema, deepMerge, defaultConfig, derivePolicyVars, detectProcessorKeys, enabledDocs, expandPalette, functionalRole, generate, generateDocs, generateLegal, isConfig, jsonSchema, loadIconSvg, movedTokens, parseMarkdown, processorsMissingLocation, renderInlineMarkdown, renderMarkdown, renderNodes, resolveConfig, resolveInput, resolvePrivacyContact, resolveProcessors, resolveTheme, roleColorUtilities, roleHue, roleKind, schemaTreeNodes, spriteId, stripNulls, tierPatterns, tierTags, toContentNodes, toHtmlFragment, toPortableText, tokenClass, tokenVar, validate, validateConfig };
|
|
8687
|
+
export { AD_PROVIDERS, BASE_HOOK, CONFIG_SCHEMA_URL, CONTRAST, ConfigSchema, DARK_SEL, DOC_ORDER, DOC_SLUGS, DesignSystemSchema, JURISDICTIONS, JURISDICTION_COUNTRIES, KNOWN_PROCESSORS, NUMERIC_STEPS, REQUIRED_ROLES, ROLE_TOKEN_KEYS, SCHEMA_LOCAL_PATH, SCHEMA_URL, SiteFontSchema, TEMPLATES, TIERS, TIER_NAMES, TW_CLASH, buildIconSprite, configJsonSchema, deepMerge, defaultConfig, derivePolicyVars, detectProcessorKeys, enabledDocs, expandPalette, functionalRole, generate, generateDocs, generateLegal, isConfig, jsonSchema, loadIconSvg, movedTokens, parseMarkdown, processorsMissingLocation, renderInlineMarkdown, renderMarkdown, renderNodes, renderRobotsTxt, resolveConfig, resolveInput, resolvePrivacyContact, resolveProcessors, resolveTheme, roleColorUtilities, roleHue, roleKind, schemaTreeNodes, spriteId, stripNulls, tierPatterns, tierTags, toContentNodes, toHtmlFragment, toPortableText, tokenClass, tokenVar, validate, validateConfig };
|
|
8490
8688
|
//# sourceMappingURL=index.mjs.map
|