@getvitops/generator 8.1.0 → 8.2.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.
Files changed (44) hide show
  1. package/CHANGELOG.md +797 -20
  2. package/assets/css/patterns/banner.css +20 -0
  3. package/assets/css/patterns/copy.css +5 -0
  4. package/assets/css/patterns/forms.css +5 -0
  5. package/assets/css/patterns/marquee.css +16 -0
  6. package/assets/css/patterns/splitter.css +11 -0
  7. package/assets/css/patterns/table.css +10 -0
  8. package/assets/js/consent.js +1 -1
  9. package/assets/js/deferred.js +1 -1
  10. package/assets/js/editor.js +2 -2
  11. package/assets/js/elements.js +5 -5
  12. package/assets/js/polyfills/adoptedStyleSheets.js +1 -1
  13. package/assets/js/polyfills/css-anchor-positioning-fn.js +4 -4
  14. package/assets/js/polyfills/global.esm.js +1 -1
  15. package/assets/js/polyfills/invokers-polyfill.js +1 -1
  16. package/assets/js/polyfills/popover.js +2 -2
  17. package/assets/js/polyfills/webcomponents-bundle.js +9 -9
  18. package/config.schema.json +464 -228
  19. package/dist/config-BfcWKH-5.mjs +1743 -0
  20. package/dist/config-BfcWKH-5.mjs.map +1 -0
  21. package/dist/config-Bhg3Xv3D.mjs +1774 -0
  22. package/dist/config-Bhg3Xv3D.mjs.map +1 -0
  23. package/dist/config.d.mts +2431 -0
  24. package/dist/config.d.mts.map +1 -0
  25. package/dist/config.mjs +3 -0
  26. package/dist/icons-sprite-CQ5Zj4go.d.mts +265 -0
  27. package/dist/icons-sprite-CQ5Zj4go.d.mts.map +1 -0
  28. package/dist/icons-sprite.d.mts +2 -0
  29. package/dist/icons-sprite.mjs +128 -0
  30. package/dist/icons-sprite.mjs.map +1 -0
  31. package/dist/index.d.mts +73 -2885
  32. package/dist/index.d.mts.map +1 -1
  33. package/dist/index.mjs +144 -2422
  34. package/dist/index.mjs.map +1 -1
  35. package/dist/schema-BV2n2phj.d.mts +291 -0
  36. package/dist/schema-BV2n2phj.d.mts.map +1 -0
  37. package/dist/tokens-BzBbTeTi.d.mts +225 -0
  38. package/dist/tokens-BzBbTeTi.d.mts.map +1 -0
  39. package/dist/tokens-C1RJwFeG.mjs +695 -0
  40. package/dist/tokens-C1RJwFeG.mjs.map +1 -0
  41. package/dist/tokens.d.mts +2 -0
  42. package/dist/tokens.mjs +3 -0
  43. package/package.json +19 -6
  44. package/schema.json +12 -28
@@ -7,6 +7,11 @@
7
7
  "description": "URL of the published config JSON Schema (stamped by `vitops init --site`) so editors provide autocomplete + validation.",
8
8
  "type": "string"
9
9
  },
10
+ "configVersion": {
11
+ "description": "The config shape version (see `CONFIG_VERSION`) — absent reads as version 1, which is every config validated by this schema so far.",
12
+ "type": "number",
13
+ "const": 1
14
+ },
10
15
  "designSystem": {
11
16
  "type": "object",
12
17
  "properties": {
@@ -456,13 +461,9 @@
456
461
  "type": "string"
457
462
  },
458
463
  "additionalProperties": {
459
- "anyOf": [
460
- {
461
- "type": "string"
462
- },
463
- {
464
- "type": "number"
465
- }
464
+ "type": [
465
+ "string",
466
+ "number"
466
467
  ]
467
468
  },
468
469
  "description": "A bag of CSS-ish keys, each mapped to a declaration plus a `--<role>-<sfx>` override hook. The recognised set is **closed**: `family`, `size`, `weight`, `style`, `line-height`, `tracking` (→ `letter-spacing`), `text-transform`, `text-decoration`, `text-wrap`, `color`. Note the last four are spelled with their full CSS property names — `transform` and `decoration` are NOT accepted. Anything unrecognised is **ignored**, not passed through, so the generator warns rather than emitting it: a silently-dropped `transform: uppercase` is how title-case navigation reaches production. Note also that `style`, `text-transform`, `text-decoration` and `text-wrap` are emitted on **every** role at their identity value (`normal`/`none`/`none`/`wrap`) whether declared or not, so applying one role class over another fully resets it — which means **omitting `text-wrap` is not \"inherit\"**: it emits `text-wrap: wrap` and cancels the `pretty` the role would otherwise inherit from a `pretty` ancestor such as a `body`-mapped role. Declare it on every role — `balance` for heading-like roles, `pretty` for copy, `wrap` for short single-line labels."
@@ -504,13 +505,9 @@
504
505
  "type": "string"
505
506
  },
506
507
  "additionalProperties": {
507
- "anyOf": [
508
- {
509
- "type": "string"
510
- },
511
- {
512
- "type": "number"
513
- }
508
+ "type": [
509
+ "string",
510
+ "number"
514
511
  ]
515
512
  },
516
513
  "description": "Extra literal declarations merged into the effect class as-is."
@@ -521,13 +518,9 @@
521
518
  "type": "string"
522
519
  },
523
520
  "additionalProperties": {
524
- "anyOf": [
525
- {
526
- "type": "string"
527
- },
528
- {
529
- "type": "number"
530
- }
521
+ "type": [
522
+ "string",
523
+ "number"
531
524
  ]
532
525
  },
533
526
  "description": "Effect endpoint variables (`--<key>: <value>`, e.g. opacity-from, translate-y-to) that override the keyframe defaults."
@@ -556,13 +549,9 @@
556
549
  "type": "string"
557
550
  },
558
551
  "additionalProperties": {
559
- "anyOf": [
560
- {
561
- "type": "string"
562
- },
563
- {
564
- "type": "number"
565
- }
552
+ "type": [
553
+ "string",
554
+ "number"
566
555
  ]
567
556
  }
568
557
  }
@@ -694,7 +683,7 @@
694
683
  "description": "General contact address. Also the last fallback for privacy requests — see `contact`.",
695
684
  "type": "string",
696
685
  "format": "email",
697
- "pattern": "^(?!\\.)(?!.*\\.\\.)([A-Za-z0-9_'+\\-\\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"
686
+ "pattern": "^(?:[A-Za-z0-9_'+\\-]+\\.)*[A-Za-z0-9_'+\\-]*[A-Za-z0-9_+-]@(?:[A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"
698
687
  },
699
688
  "phone": {
700
689
  "description": "General contact telephone number.",
@@ -761,7 +750,7 @@
761
750
  "email": {
762
751
  "type": "string",
763
752
  "format": "email",
764
- "pattern": "^(?!\\.)(?!.*\\.\\.)([A-Za-z0-9_'+\\-\\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"
753
+ "pattern": "^(?:[A-Za-z0-9_'+\\-]+\\.)*[A-Za-z0-9_'+\\-]*[A-Za-z0-9_+-]@(?:[A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"
765
754
  },
766
755
  "phone": {
767
756
  "type": "string"
@@ -851,7 +840,7 @@
851
840
  "email": {
852
841
  "type": "string",
853
842
  "format": "email",
854
- "pattern": "^(?!\\.)(?!.*\\.\\.)([A-Za-z0-9_'+\\-\\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"
843
+ "pattern": "^(?:[A-Za-z0-9_'+\\-]+\\.)*[A-Za-z0-9_'+\\-]*[A-Za-z0-9_+-]@(?:[A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"
855
844
  },
856
845
  "phone": {
857
846
  "type": "string"
@@ -1178,13 +1167,9 @@
1178
1167
  "description": "A string, or a per-locale map of strings (`{ en: \"…\", fr: \"…\" }`)."
1179
1168
  },
1180
1169
  "price": {
1181
- "anyOf": [
1182
- {
1183
- "type": "string"
1184
- },
1185
- {
1186
- "type": "number"
1187
- }
1170
+ "type": [
1171
+ "string",
1172
+ "number"
1188
1173
  ]
1189
1174
  },
1190
1175
  "priceCurrency": {
@@ -1450,14 +1435,14 @@
1450
1435
  "items": {
1451
1436
  "type": "string",
1452
1437
  "format": "email",
1453
- "pattern": "^(?!\\.)(?!.*\\.\\.)([A-Za-z0-9_'+\\-\\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"
1438
+ "pattern": "^(?:[A-Za-z0-9_'+\\-]+\\.)*[A-Za-z0-9_'+\\-]*[A-Za-z0-9_+-]@(?:[A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"
1454
1439
  }
1455
1440
  },
1456
1441
  "fullUserGroup": {
1457
1442
  "description": "A Google Group to grant Full-User access in Search Console. SURFACED AS A REMINDER only: Search Console exposes no user/permission API, so this step stays manual by design.",
1458
1443
  "type": "string",
1459
1444
  "format": "email",
1460
- "pattern": "^(?!\\.)(?!.*\\.\\.)([A-Za-z0-9_'+\\-\\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"
1445
+ "pattern": "^(?:[A-Za-z0-9_'+\\-]+\\.)*[A-Za-z0-9_'+\\-]*[A-Za-z0-9_+-]@(?:[A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"
1461
1446
  }
1462
1447
  },
1463
1448
  "additionalProperties": false
@@ -1505,6 +1490,27 @@
1505
1490
  },
1506
1491
  "description": "Deploy environments (production, dev, …): URL, API origin, analytics toggle, robots policy, active A/B variant."
1507
1492
  },
1493
+ "plan": {
1494
+ "description": "Commercial plan/add-on entitlement, gating which capability fields below may be turned on. Absence means unrestricted.",
1495
+ "type": "object",
1496
+ "properties": {
1497
+ "key": {
1498
+ "type": "string",
1499
+ "description": "Commercial plan identifier (e.g. \"essentials\", \"growth\") — open string, not a closed enum."
1500
+ },
1501
+ "addons": {
1502
+ "description": "Add-on keys this site is entitled to (e.g. \"ab_testing\", \"server_side_tracking\") — checked against PLAN_CAPABILITIES.",
1503
+ "type": "array",
1504
+ "items": {
1505
+ "type": "string"
1506
+ }
1507
+ }
1508
+ },
1509
+ "required": [
1510
+ "key"
1511
+ ],
1512
+ "additionalProperties": false
1513
+ },
1508
1514
  "abTesting": {
1509
1515
  "description": "A/B testing: cookie-based split plus named variants whose `overrides` patch the config per environment.",
1510
1516
  "type": "object",
@@ -1562,6 +1568,65 @@
1562
1568
  ],
1563
1569
  "additionalProperties": false
1564
1570
  },
1571
+ "experiments": {
1572
+ "description": "Runtime, per-visitor content experiments — one config-schema-free page branches on a cookie the vitopsAbTesting() middleware assigns. Distinct from `abTesting`, which merges a whole build/environment variant at resolve time; this instead lets one page (e.g. a real, nav-linked /contact) run a copy test without a second URL or a second deploy. Keyed by an open experiment name so more than one can run at once.",
1573
+ "type": "object",
1574
+ "propertyNames": {
1575
+ "type": "string"
1576
+ },
1577
+ "additionalProperties": {
1578
+ "type": "object",
1579
+ "properties": {
1580
+ "enabled": {
1581
+ "type": "boolean"
1582
+ },
1583
+ "category": {
1584
+ "description": "Consent category this experiment's assignment cookie waits on (default `analytics`) — independent of `site.tracking.category`, so an experiment never piggybacks on ad-attribution consent.",
1585
+ "type": "string",
1586
+ "enum": [
1587
+ "analytics",
1588
+ "marketing"
1589
+ ]
1590
+ },
1591
+ "cookieName": {
1592
+ "description": "Default `_ab_<experiment key>`.",
1593
+ "type": "string"
1594
+ },
1595
+ "cookieMaxAge": {
1596
+ "description": "Seconds.",
1597
+ "type": "integer",
1598
+ "exclusiveMinimum": 0,
1599
+ "maximum": 9007199254740991
1600
+ },
1601
+ "splitRatio": {
1602
+ "description": "Fraction of new visitors auto-assigned to `variants[1]` (default 0.5). 0 means nobody is auto-assigned — a manually or query-param-set cookie still pins a visitor to `variants[1]`, which is what makes 0 a real blue/green-preview mode rather than a no-op.",
1603
+ "type": "number",
1604
+ "minimum": 0,
1605
+ "maximum": 1
1606
+ },
1607
+ "variants": {
1608
+ "type": "array",
1609
+ "prefixItems": [
1610
+ {
1611
+ "type": "string"
1612
+ },
1613
+ {
1614
+ "type": "string"
1615
+ }
1616
+ ],
1617
+ "items": false,
1618
+ "minItems": 2,
1619
+ "maxItems": 2,
1620
+ "description": "Exactly two variant names, e.g. [\"control\", \"new-form\"]. `variants[0]` is the default a visitor with no assignment yet effectively sees."
1621
+ }
1622
+ },
1623
+ "required": [
1624
+ "enabled",
1625
+ "variants"
1626
+ ],
1627
+ "additionalProperties": false
1628
+ }
1629
+ },
1565
1630
  "fonts": {
1566
1631
  "description": "Font LOADING (a serialisable projection of the Astro Fonts API: provider, weights, subsets, preload). Font TOKENS live in `designSystem.themes.<theme>.fonts` and reference the same `cssVariable`.",
1567
1632
  "type": "array",
@@ -1591,13 +1656,9 @@
1591
1656
  "weights": {
1592
1657
  "type": "array",
1593
1658
  "items": {
1594
- "anyOf": [
1595
- {
1596
- "type": "number"
1597
- },
1598
- {
1599
- "type": "string"
1600
- }
1659
+ "type": [
1660
+ "number",
1661
+ "string"
1601
1662
  ]
1602
1663
  }
1603
1664
  },
@@ -1660,13 +1721,9 @@
1660
1721
  }
1661
1722
  },
1662
1723
  "weight": {
1663
- "anyOf": [
1664
- {
1665
- "type": "number"
1666
- },
1667
- {
1668
- "type": "string"
1669
- }
1724
+ "type": [
1725
+ "number",
1726
+ "string"
1670
1727
  ]
1671
1728
  },
1672
1729
  "style": {
@@ -1711,13 +1768,9 @@
1711
1768
  "type": "object",
1712
1769
  "properties": {
1713
1770
  "weight": {
1714
- "anyOf": [
1715
- {
1716
- "type": "number"
1717
- },
1718
- {
1719
- "type": "string"
1720
- }
1771
+ "type": [
1772
+ "number",
1773
+ "string"
1721
1774
  ]
1722
1775
  },
1723
1776
  "style": {
@@ -2079,16 +2132,10 @@
2079
2132
  "type": "string"
2080
2133
  },
2081
2134
  "additionalProperties": {
2082
- "anyOf": [
2083
- {
2084
- "type": "string"
2085
- },
2086
- {
2087
- "type": "number"
2088
- },
2089
- {
2090
- "type": "boolean"
2091
- }
2135
+ "type": [
2136
+ "string",
2137
+ "number",
2138
+ "boolean"
2092
2139
  ]
2093
2140
  }
2094
2141
  }
@@ -2106,16 +2153,10 @@
2106
2153
  "type": "string"
2107
2154
  },
2108
2155
  "additionalProperties": {
2109
- "anyOf": [
2110
- {
2111
- "type": "string"
2112
- },
2113
- {
2114
- "type": "number"
2115
- },
2116
- {
2117
- "type": "boolean"
2118
- }
2156
+ "type": [
2157
+ "string",
2158
+ "number",
2159
+ "boolean"
2119
2160
  ]
2120
2161
  }
2121
2162
  },
@@ -2185,70 +2226,115 @@
2185
2226
  "additionalProperties": false
2186
2227
  },
2187
2228
  "seo": {
2188
- "description": "SEO defaults: title/description templates, robots policy, verification tokens, Open Graph + Twitter cards, and post-deploy indexing notification.",
2229
+ "description": "SEO defaults: site name, title/description defaults, robots policy, verification tokens, Open Graph + Twitter cards, and post-deploy indexing notification. Read by `<Seo />`, `vitops robots` and `vitops search notify`.",
2189
2230
  "type": "object",
2190
2231
  "properties": {
2232
+ "siteName": {
2233
+ "description": "`og:site_name`, and the `<title>` when a page passes none. Defaults to `organization.name`, so most sites never state it.",
2234
+ "anyOf": [
2235
+ {
2236
+ "type": "string"
2237
+ },
2238
+ {
2239
+ "type": "object",
2240
+ "propertyNames": {
2241
+ "type": "string"
2242
+ },
2243
+ "additionalProperties": {
2244
+ "type": "string"
2245
+ }
2246
+ }
2247
+ ]
2248
+ },
2191
2249
  "titleTemplate": {
2192
- "description": "Page-title pattern (e.g. \"%s · Acme\").",
2193
- "type": "string"
2250
+ "description": "Page-title pattern; `%s` is the page title (e.g. \"%s · Acme\"). Applies to `<title>` only, never to `og:title`, and is skipped when the page title already equals the site name.",
2251
+ "anyOf": [
2252
+ {
2253
+ "type": "string"
2254
+ },
2255
+ {
2256
+ "type": "object",
2257
+ "propertyNames": {
2258
+ "type": "string"
2259
+ },
2260
+ "additionalProperties": {
2261
+ "type": "string"
2262
+ }
2263
+ }
2264
+ ]
2194
2265
  },
2195
- "descriptionTemplate": {
2196
- "type": "string"
2266
+ "defaultTitle": {
2267
+ "description": "`<title>` for pages that pass none. Used verbatim — the template is not applied. Defaults to the site name.",
2268
+ "anyOf": [
2269
+ {
2270
+ "type": "string"
2271
+ },
2272
+ {
2273
+ "type": "object",
2274
+ "propertyNames": {
2275
+ "type": "string"
2276
+ },
2277
+ "additionalProperties": {
2278
+ "type": "string"
2279
+ }
2280
+ }
2281
+ ]
2282
+ },
2283
+ "defaultDescription": {
2284
+ "description": "`<meta name=\"description\">` for pages that pass none.",
2285
+ "anyOf": [
2286
+ {
2287
+ "type": "string"
2288
+ },
2289
+ {
2290
+ "type": "object",
2291
+ "propertyNames": {
2292
+ "type": "string"
2293
+ },
2294
+ "additionalProperties": {
2295
+ "type": "string"
2296
+ }
2297
+ }
2298
+ ]
2197
2299
  },
2198
2300
  "robots": {
2199
- "description": "e.g. \"index,follow\" / \"noindex,nofollow\".",
2301
+ "description": "Site-wide robots directive (e.g. \"index,follow\" / \"noindex,nofollow\"). `site.environments.<env>.robots` wins over it.",
2200
2302
  "type": "string"
2201
2303
  },
2202
2304
  "googleSiteVerification": {
2305
+ "description": "Google Search Console verification token (`<meta name=\"google-site-verification\">`).",
2203
2306
  "type": "string"
2204
2307
  },
2205
2308
  "bingSiteVerification": {
2309
+ "description": "Bing Webmaster Tools verification token (`<meta name=\"msvalidate.01\">`).",
2206
2310
  "type": "string"
2207
2311
  },
2208
2312
  "openGraph": {
2313
+ "description": "Open Graph defaults. Per-page `<Seo />` props override them.",
2209
2314
  "type": "object",
2210
2315
  "properties": {
2211
2316
  "type": {
2212
- "type": "string"
2317
+ "description": "Default `og:type`. Default `website`.",
2318
+ "type": "string",
2319
+ "enum": [
2320
+ "website",
2321
+ "article",
2322
+ "profile"
2323
+ ]
2213
2324
  },
2214
- "siteName": {
2325
+ "locale": {
2326
+ "description": "`og:locale`. `en-CA` and `en_CA` both emit `en_CA`.",
2215
2327
  "type": "string"
2216
2328
  },
2217
- "title": {
2218
- "anyOf": [
2219
- {
2220
- "type": "string"
2221
- },
2222
- {
2223
- "type": "object",
2224
- "propertyNames": {
2225
- "type": "string"
2226
- },
2227
- "additionalProperties": {
2228
- "type": "string"
2229
- }
2230
- }
2231
- ],
2232
- "description": "A string, or a per-locale map of strings (`{ en: \"…\", fr: \"…\" }`)."
2233
- },
2234
- "description": {
2235
- "anyOf": [
2236
- {
2237
- "type": "string"
2238
- },
2239
- {
2240
- "type": "object",
2241
- "propertyNames": {
2242
- "type": "string"
2243
- },
2244
- "additionalProperties": {
2245
- "type": "string"
2246
- }
2247
- }
2248
- ],
2249
- "description": "A string, or a per-locale map of strings (`{ en: \"…\", fr: \"…\" }`)."
2329
+ "localeAlternates": {
2330
+ "description": "Other locales the same content exists in → `og:locale:alternate`.",
2331
+ "type": "array",
2332
+ "items": {
2333
+ "type": "string"
2334
+ }
2250
2335
  },
2251
2336
  "image": {
2337
+ "description": "Fallback share image for pages that bring none.",
2252
2338
  "anyOf": [
2253
2339
  {
2254
2340
  "type": "string"
@@ -2257,7 +2343,8 @@
2257
2343
  "type": "object",
2258
2344
  "properties": {
2259
2345
  "src": {
2260
- "type": "string"
2346
+ "type": "string",
2347
+ "description": "Absolute, or relative to `site` (resolved at render time)."
2261
2348
  },
2262
2349
  "alt": {
2263
2350
  "type": "string"
@@ -2267,26 +2354,28 @@
2267
2354
  },
2268
2355
  "height": {
2269
2356
  "type": "number"
2357
+ },
2358
+ "type": {
2359
+ "description": "MIME type, e.g. `image/png`.",
2360
+ "type": "string"
2270
2361
  }
2271
2362
  },
2272
2363
  "required": [
2273
- "src",
2274
- "alt"
2364
+ "src"
2275
2365
  ],
2276
2366
  "additionalProperties": false
2277
2367
  }
2278
2368
  ]
2279
- },
2280
- "locale": {
2281
- "type": "string"
2282
2369
  }
2283
2370
  },
2284
2371
  "additionalProperties": false
2285
2372
  },
2286
2373
  "twitter": {
2374
+ "description": "Twitter/X card defaults.",
2287
2375
  "type": "object",
2288
2376
  "properties": {
2289
2377
  "card": {
2378
+ "description": "Card type when no image resolves (default `summary`).",
2290
2379
  "type": "string",
2291
2380
  "enum": [
2292
2381
  "summary",
@@ -2296,73 +2385,12 @@
2296
2385
  ]
2297
2386
  },
2298
2387
  "site": {
2388
+ "description": "The site's own handle, e.g. \"@acme\".",
2299
2389
  "type": "string"
2300
2390
  },
2301
2391
  "creator": {
2392
+ "description": "Default author handle, e.g. \"@ada\".",
2302
2393
  "type": "string"
2303
- },
2304
- "title": {
2305
- "anyOf": [
2306
- {
2307
- "type": "string"
2308
- },
2309
- {
2310
- "type": "object",
2311
- "propertyNames": {
2312
- "type": "string"
2313
- },
2314
- "additionalProperties": {
2315
- "type": "string"
2316
- }
2317
- }
2318
- ],
2319
- "description": "A string, or a per-locale map of strings (`{ en: \"…\", fr: \"…\" }`)."
2320
- },
2321
- "description": {
2322
- "anyOf": [
2323
- {
2324
- "type": "string"
2325
- },
2326
- {
2327
- "type": "object",
2328
- "propertyNames": {
2329
- "type": "string"
2330
- },
2331
- "additionalProperties": {
2332
- "type": "string"
2333
- }
2334
- }
2335
- ],
2336
- "description": "A string, or a per-locale map of strings (`{ en: \"…\", fr: \"…\" }`)."
2337
- },
2338
- "image": {
2339
- "anyOf": [
2340
- {
2341
- "type": "string"
2342
- },
2343
- {
2344
- "type": "object",
2345
- "properties": {
2346
- "src": {
2347
- "type": "string"
2348
- },
2349
- "alt": {
2350
- "type": "string"
2351
- },
2352
- "width": {
2353
- "type": "number"
2354
- },
2355
- "height": {
2356
- "type": "number"
2357
- }
2358
- },
2359
- "required": [
2360
- "src",
2361
- "alt"
2362
- ],
2363
- "additionalProperties": false
2364
- }
2365
- ]
2366
2394
  }
2367
2395
  },
2368
2396
  "additionalProperties": false
@@ -2465,6 +2493,16 @@
2465
2493
  "cookies": {
2466
2494
  "description": "Default false — Matomo runs cookieless (`disableCookies`), which is what lets the cookie notice state positively that it sets none.",
2467
2495
  "type": "boolean"
2496
+ },
2497
+ "abVariantDimensions": {
2498
+ "description": "Pre-created Matomo custom-dimension index per `site.experiments` key — Matomo requires the dimension to exist in the instance before a value can be pushed for it.",
2499
+ "type": "object",
2500
+ "propertyNames": {
2501
+ "type": "string"
2502
+ },
2503
+ "additionalProperties": {
2504
+ "type": "number"
2505
+ }
2468
2506
  }
2469
2507
  },
2470
2508
  "required": [
@@ -2486,7 +2524,7 @@
2486
2524
  {
2487
2525
  "type": "string",
2488
2526
  "format": "email",
2489
- "pattern": "^(?!\\.)(?!.*\\.\\.)([A-Za-z0-9_'+\\-\\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"
2527
+ "pattern": "^(?:[A-Za-z0-9_'+\\-]+\\.)*[A-Za-z0-9_'+\\-]*[A-Za-z0-9_+-]@(?:[A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"
2490
2528
  },
2491
2529
  {
2492
2530
  "type": "object",
@@ -2500,13 +2538,13 @@
2500
2538
  "description": "Recipient. Defaults to the primary location’s `email`, then the first location that has one.",
2501
2539
  "type": "string",
2502
2540
  "format": "email",
2503
- "pattern": "^(?!\\.)(?!.*\\.\\.)([A-Za-z0-9_'+\\-\\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"
2541
+ "pattern": "^(?:[A-Za-z0-9_'+\\-]+\\.)*[A-Za-z0-9_'+\\-]*[A-Za-z0-9_+-]@(?:[A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"
2504
2542
  },
2505
2543
  "from": {
2506
2544
  "description": "Sender. Defaults to `noreply@<domains.canonical>`. Its domain must be onboarded (`wrangler email sending enable <domain>`) or every send fails.",
2507
2545
  "type": "string",
2508
2546
  "format": "email",
2509
- "pattern": "^(?!\\.)(?!.*\\.\\.)([A-Za-z0-9_'+\\-\\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"
2547
+ "pattern": "^(?:[A-Za-z0-9_'+\\-]+\\.)*[A-Za-z0-9_'+\\-]*[A-Za-z0-9_+-]@(?:[A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"
2510
2548
  },
2511
2549
  "fromName": {
2512
2550
  "description": "Display name on the From header. Defaults to `organization.name`.",
@@ -2516,11 +2554,15 @@
2516
2554
  "description": "Reply-To header.",
2517
2555
  "type": "string",
2518
2556
  "format": "email",
2519
- "pattern": "^(?!\\.)(?!.*\\.\\.)([A-Za-z0-9_'+\\-\\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"
2557
+ "pattern": "^(?:[A-Za-z0-9_'+\\-]+\\.)*[A-Za-z0-9_'+\\-]*[A-Za-z0-9_+-]@(?:[A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"
2520
2558
  },
2521
2559
  "binding": {
2522
2560
  "description": "Workers `send_email` binding name (default `EMAIL`). Set it when using a restricted binding.",
2523
2561
  "type": "string"
2562
+ },
2563
+ "verifySender": {
2564
+ "description": "Set false to skip `vitops email check`'s deploy-time onboarding warning for this site — for a `from` domain the scan cannot see is correct (e.g. built dynamically), which would otherwise be a false positive.",
2565
+ "type": "boolean"
2524
2566
  }
2525
2567
  },
2526
2568
  "required": [
@@ -2529,6 +2571,10 @@
2529
2571
  "additionalProperties": false
2530
2572
  }
2531
2573
  ]
2574
+ },
2575
+ "actionUrl": {
2576
+ "description": "Optional action-link template added to the conversion email (e.g. a link into your ad platform or your own CRM/portal). `{clickId}` is replaced with the ad click ID captured on `_ac` (gclid, fbclid, …). Omitted from the email when the event carries no click ID to interpolate, rather than rendering a broken link.",
2577
+ "type": "string"
2532
2578
  }
2533
2579
  },
2534
2580
  "additionalProperties": false
@@ -2587,8 +2633,64 @@
2587
2633
  "type": "string"
2588
2634
  },
2589
2635
  "conversionLabel": {
2590
- "description": "Google Ads only: the conversion action label paired with the `AW-…` conversion ID.",
2591
- "type": "string"
2636
+ "description": "Google Ads only: the conversion action label(s) paired with the `AW-…` conversion ID — either one bare label (back-compat, treated as the `call` action) or a map of action → label, so a call, an email click and a form submission each fire their own Google Ads conversion action instead of all three reporting as the same one.",
2637
+ "anyOf": [
2638
+ {
2639
+ "type": "string"
2640
+ },
2641
+ {
2642
+ "type": "object",
2643
+ "properties": {
2644
+ "call": {
2645
+ "description": "Fired on a tel: link click.",
2646
+ "type": "string"
2647
+ },
2648
+ "email": {
2649
+ "description": "Fired on a mailto: link click.",
2650
+ "type": "string"
2651
+ },
2652
+ "form": {
2653
+ "description": "Fired client-side by `vitopsFormConversion()` — the consumer's own submit handler calls it once a form POST succeeds.",
2654
+ "type": "string"
2655
+ }
2656
+ },
2657
+ "additionalProperties": false
2658
+ }
2659
+ ]
2660
+ },
2661
+ "conversionActionId": {
2662
+ "description": "Google Ads only: the conversion action **resource id(s)** (a numeric id, or a full `customers/…/conversionActions/…` resource name) used for server-side offline conversion upload — NOT the same value as `conversionLabel`, which is the opaque label the web tag uses and which the Ads API's upload endpoint does not accept. Only meaningful when `reporting: \"server\"`.",
2663
+ "anyOf": [
2664
+ {
2665
+ "type": "string"
2666
+ },
2667
+ {
2668
+ "type": "object",
2669
+ "properties": {
2670
+ "call": {
2671
+ "description": "Fired on a tel: link click.",
2672
+ "type": "string"
2673
+ },
2674
+ "email": {
2675
+ "description": "Fired on a mailto: link click.",
2676
+ "type": "string"
2677
+ },
2678
+ "form": {
2679
+ "description": "Fired on a form submission.",
2680
+ "type": "string"
2681
+ }
2682
+ },
2683
+ "additionalProperties": false
2684
+ }
2685
+ ]
2686
+ },
2687
+ "reporting": {
2688
+ "description": "Google Ads only: `\"client\"` (default) fires the conversion via `gtag.js` in the browser at click/submit time; `\"server\"` uploads it via the Ads API from the conversion endpoint instead (see `conversionActionId`). An action with no matching `conversionActionId` keeps firing client-side regardless of this setting, so setting it before `conversionActionId` is filled in never silently drops a conversion.",
2689
+ "type": "string",
2690
+ "enum": [
2691
+ "client",
2692
+ "server"
2693
+ ]
2592
2694
  },
2593
2695
  "domain": {
2594
2696
  "description": "Bare hostname to verify with this platform. Defaults to the host of `domains.canonical` — set it only when the ad account is verified against a different domain.",
@@ -2683,6 +2785,150 @@
2683
2785
  ],
2684
2786
  "additionalProperties": false
2685
2787
  },
2788
+ "crm": {
2789
+ "description": "Push form submissions into a client CRM (HubSpot, Zoho Bigin, or Jobber) alongside the notification email. Read by `createConversionRoute()`'s `crm` option and `vitops crm check`/`vitops crm setup`. Credentials never live here.",
2790
+ "type": "object",
2791
+ "properties": {
2792
+ "provider": {
2793
+ "type": "string",
2794
+ "enum": [
2795
+ "hubspot",
2796
+ "bigin",
2797
+ "jobber"
2798
+ ],
2799
+ "description": "Which CRM this site pushes form submissions into."
2800
+ },
2801
+ "enabled": {
2802
+ "description": "Set false to keep the block on record without writing to the CRM.",
2803
+ "type": "boolean"
2804
+ },
2805
+ "hubspot": {
2806
+ "description": "Required when `provider` is `hubspot`.",
2807
+ "type": "object",
2808
+ "properties": {
2809
+ "portalId": {
2810
+ "type": "string",
2811
+ "description": "The HubSpot portal (hub) ID the form belongs to."
2812
+ },
2813
+ "formGuid": {
2814
+ "type": "string",
2815
+ "description": "The HubSpot form GUID to submit against."
2816
+ },
2817
+ "dataCenter": {
2818
+ "description": "Which regional submissions API to post to (default `na`).",
2819
+ "type": "string",
2820
+ "enum": [
2821
+ "na",
2822
+ "eu"
2823
+ ]
2824
+ }
2825
+ },
2826
+ "required": [
2827
+ "portalId",
2828
+ "formGuid"
2829
+ ],
2830
+ "additionalProperties": false
2831
+ },
2832
+ "bigin": {
2833
+ "description": "Required when `provider` is `bigin`.",
2834
+ "type": "object",
2835
+ "properties": {
2836
+ "dataCenter": {
2837
+ "type": "string",
2838
+ "enum": [
2839
+ "com",
2840
+ "ca",
2841
+ "eu",
2842
+ "in",
2843
+ "com.au",
2844
+ "jp"
2845
+ ],
2846
+ "description": "The self-client's data centre — determines both the OAuth accounts host and the API host (`www.zohoapis.<dc>`)."
2847
+ },
2848
+ "duplicateCheckField": {
2849
+ "description": "Bigin field the upsert dedupes on (default `Email`, Bigin's own unique field).",
2850
+ "type": "string"
2851
+ },
2852
+ "pipeline": {
2853
+ "description": "Also open a Pipelines record alongside the Contact.",
2854
+ "type": "object",
2855
+ "properties": {
2856
+ "enabled": {
2857
+ "type": "boolean",
2858
+ "description": "Also open a Pipelines record for the new/updated contact."
2859
+ },
2860
+ "subPipeline": {
2861
+ "description": "Sub-pipeline to file the record under.",
2862
+ "type": "string"
2863
+ },
2864
+ "stage": {
2865
+ "description": "Starting stage for the pipeline record.",
2866
+ "type": "string"
2867
+ }
2868
+ },
2869
+ "required": [
2870
+ "enabled"
2871
+ ],
2872
+ "additionalProperties": false
2873
+ }
2874
+ },
2875
+ "required": [
2876
+ "dataCenter"
2877
+ ],
2878
+ "additionalProperties": false
2879
+ },
2880
+ "jobber": {
2881
+ "description": "Required when `provider` is `jobber`.",
2882
+ "type": "object",
2883
+ "properties": {
2884
+ "apiVersion": {
2885
+ "description": "Override the pinned `X-JOBBER-GRAPHQL-VERSION` date this integration is tested against. An escape hatch for an app pinned to a different version — Jobber's schema can change field names between versions.",
2886
+ "type": "string"
2887
+ },
2888
+ "dedupe": {
2889
+ "description": "Look up an existing client by email before creating one (default false pending verification of Jobber's search API against a live schema — see AGENTS.md). Jobber's `clientCreate` has no upsert, so without this a resubmitting visitor creates a second client.",
2890
+ "type": "boolean"
2891
+ },
2892
+ "note": {
2893
+ "description": "Attach a best-effort note carrying the submitted message and any attribution/extra fields with no dedicated Jobber field.",
2894
+ "type": "object",
2895
+ "properties": {
2896
+ "enabled": {
2897
+ "description": "Attach a best-effort note carrying the message and unmapped attribution (default true). A failure here never loses the client record itself.",
2898
+ "type": "boolean"
2899
+ }
2900
+ },
2901
+ "additionalProperties": false
2902
+ }
2903
+ },
2904
+ "additionalProperties": false
2905
+ },
2906
+ "fieldMap": {
2907
+ "description": "Submitted form field name → CRM property name. A field with no entry here is dropped, not guessed — an unmapped field never reaches the CRM.",
2908
+ "type": "object",
2909
+ "propertyNames": {
2910
+ "type": "string"
2911
+ },
2912
+ "additionalProperties": {
2913
+ "type": "string"
2914
+ }
2915
+ },
2916
+ "trackingFields": {
2917
+ "description": "`_ac` attribution key (a click-ID param, a UTM param, `landingPage`, `referrer`, or `ab_variant`) → CRM property name. Same drop-if-unmapped rule as `fieldMap`; a visitor with no `_ac` cookie maps to no attribution fields at all, never an error.",
2918
+ "type": "object",
2919
+ "propertyNames": {
2920
+ "type": "string"
2921
+ },
2922
+ "additionalProperties": {
2923
+ "type": "string"
2924
+ }
2925
+ }
2926
+ },
2927
+ "required": [
2928
+ "provider"
2929
+ ],
2930
+ "additionalProperties": false
2931
+ },
2686
2932
  "security": {
2687
2933
  "description": "Security integrations (bot protection).",
2688
2934
  "type": "object",
@@ -2740,7 +2986,7 @@
2740
2986
  "email": {
2741
2987
  "type": "string",
2742
2988
  "format": "email",
2743
- "pattern": "^(?!\\.)(?!.*\\.\\.)([A-Za-z0-9_'+\\-\\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"
2989
+ "pattern": "^(?:[A-Za-z0-9_'+\\-]+\\.)*[A-Za-z0-9_'+\\-]*[A-Za-z0-9_+-]@(?:[A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"
2744
2990
  },
2745
2991
  "phone": {
2746
2992
  "type": "string"
@@ -3045,6 +3291,14 @@
3045
3291
  },
3046
3292
  "type": {
3047
3293
  "type": "string"
3294
+ },
3295
+ "repo": {
3296
+ "description": "`owner/name` of this site's GitHub repository. Read by the EmDash design-system-editor plugin route to auto-commit a client's admin edit back to `branch` — see AGENTS.md's \"EmDash plugin as the design-system editing surface\" section. Absent means that sync is skipped (the live override still applies); nothing else in this toolchain reads it today.",
3297
+ "type": "string"
3298
+ },
3299
+ "branch": {
3300
+ "description": "Branch the design-system-editor sync commits to. Default `\"dev\"` — never `main`/production directly; a commit here still needs this repo's own `promote` PR + admin approval to reach production, the same gate every other change goes through.",
3301
+ "type": "string"
3048
3302
  }
3049
3303
  },
3050
3304
  "additionalProperties": false
@@ -3126,16 +3380,10 @@
3126
3380
  "type": "string"
3127
3381
  },
3128
3382
  "additionalProperties": {
3129
- "anyOf": [
3130
- {
3131
- "type": "string"
3132
- },
3133
- {
3134
- "type": "number"
3135
- },
3136
- {
3137
- "type": "boolean"
3138
- }
3383
+ "type": [
3384
+ "string",
3385
+ "number",
3386
+ "boolean"
3139
3387
  ]
3140
3388
  }
3141
3389
  },
@@ -3145,16 +3393,10 @@
3145
3393
  "type": "string"
3146
3394
  },
3147
3395
  "additionalProperties": {
3148
- "anyOf": [
3149
- {
3150
- "type": "string"
3151
- },
3152
- {
3153
- "type": "number"
3154
- },
3155
- {
3156
- "type": "boolean"
3157
- }
3396
+ "type": [
3397
+ "string",
3398
+ "number",
3399
+ "boolean"
3158
3400
  ]
3159
3401
  }
3160
3402
  },
@@ -3192,16 +3434,10 @@
3192
3434
  "type": "string"
3193
3435
  },
3194
3436
  "additionalProperties": {
3195
- "anyOf": [
3196
- {
3197
- "type": "string"
3198
- },
3199
- {
3200
- "type": "number"
3201
- },
3202
- {
3203
- "type": "boolean"
3204
- }
3437
+ "type": [
3438
+ "string",
3439
+ "number",
3440
+ "boolean"
3205
3441
  ]
3206
3442
  }
3207
3443
  },