@getvitops/config 0.0.0-stage → 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.
- package/CHANGELOG.md +52 -0
- package/README.md +12 -2
- package/dist/index.d.mts +339 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +234 -0
- package/dist/index.mjs.map +1 -0
- package/dist/providers.d.mts +28 -0
- package/dist/providers.d.mts.map +1 -0
- package/dist/providers.mjs +49 -0
- package/dist/providers.mjs.map +1 -0
- package/package.json +38 -3
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# @getvitops/config
|
|
2
|
+
|
|
3
|
+
## 8.2.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 89265af: **New package: `@getvitops/config`** — the site-config provider blocks (`site.crm`, `site.ads`,
|
|
8
|
+
`site.reviews`, `site.seo.indexing`, `site.searchConsole`, `site.notifications`, `site.tracking`,
|
|
9
|
+
`site.security`, `site.deployment`) as zod schemas and inferred types, with the closed provider lists
|
|
10
|
+
(`AD_PROVIDERS`, `CRM_PROVIDERS`, `BIGIN_DATA_CENTERS`) on a zod-free `./providers` entry. It joins the
|
|
11
|
+
`fixed` group.
|
|
12
|
+
|
|
13
|
+
Why: `@getvitops/utils` holds the provider code but could not import its configuration types — the schema
|
|
14
|
+
lived in `@getvitops/generator`, a layer above — so it kept a hand-copied mirror beside each module,
|
|
15
|
+
an adapter converting into it, and a test holding the copy to the schema. The mirrors are gone.
|
|
16
|
+
|
|
17
|
+
- **`@getvitops/generator`**: `AD_PROVIDERS`, `CRM_PROVIDERS`, `BIGIN_DATA_CENTERS`, `AdProvider` and
|
|
18
|
+
`CrmProviderConfig` are re-exported unchanged. No import path changes.
|
|
19
|
+
- **`@getvitops/utils`**: the config types in `./crm`, `./ads`, `./indexing`, `./onboarding`, `./reviews`,
|
|
20
|
+
`./tracking` and `./notify` (`CrmConfig`, `HubSpotConfig`, `BiginConfig`, `JobberConfig`,
|
|
21
|
+
`IndexNowConfig`, `SearchConsoleConfig`, `ReviewsConfig`, `ReviewLocationConfig`, `TrackingConfig`,
|
|
22
|
+
`NotificationsConfig`, `EmailChannelConfig`, `DomainSetup`, `AdPropertySetup`) are now inferred from the
|
|
23
|
+
schema. Same names, same fields, but **optional fields now follow zod's inference**, so a hand-written
|
|
24
|
+
object literal typed against them may need `| undefined` handled. `AdPropertySetup` gains the fields the
|
|
25
|
+
schema always had (`conversionActionId`, `reporting`).
|
|
26
|
+
- **`@getvitops/astro`**: `GetvitopsAdProperty` and `GetvitopsTrackingOptions` are aliases of the schema
|
|
27
|
+
types. `toCrmConfig()` and `toGoogleAdsConfig()` are **deprecated** — they returned their argument's
|
|
28
|
+
field unchanged; pass `config.site.crm` / `config.site.ads?.google` directly.
|
|
29
|
+
- **`@getvitops/cli`**: `vitops ads` now passes `conversionActionId` and `reporting` through to the planner;
|
|
30
|
+
it previously copied a hand-picked subset of the block and dropped them.
|
|
31
|
+
|
|
32
|
+
Formats reached: none — no CSS change.
|
|
33
|
+
|
|
34
|
+
- 7fd5f8f: **`site.seo` is now the single source of site-level SEO defaults** (ADR-0002). Until now `site.seo`
|
|
35
|
+
declared `titleTemplate`, verification tokens, `openGraph` and `twitter` that nothing read, while
|
|
36
|
+
`<Seo />` took a second, differently-shaped vocabulary through `vitops({ seo })` that no other stack
|
|
37
|
+
could see.
|
|
38
|
+
|
|
39
|
+
- `SeoSchema` moves to `@getvitops/config` and gains what only the integration had: `siteName`,
|
|
40
|
+
`defaultTitle`, `defaultDescription`, `openGraph.localeAlternates` and `openGraph.image` as
|
|
41
|
+
`{ src, alt, width, height, type }`. Text fields are localisable.
|
|
42
|
+
- `@getvitops/astro` exports `resolveSeoDefaults(config, { locale, env })`. The integration reads
|
|
43
|
+
`site.seo` from the site config it already loads; `siteName` falls back to `organization.name`, and
|
|
44
|
+
the environment's `robots` wins over `site.seo.robots`, the same fact `vitops robots` and
|
|
45
|
+
`search notify` read.
|
|
46
|
+
- `vitops({ seo })` keeps working unchanged and is now an **override** merged over the config, field by
|
|
47
|
+
field (`openGraph` and `twitter` merge key by key).
|
|
48
|
+
- **Removed from the config schema** (nothing read them): `descriptionTemplate` → use
|
|
49
|
+
`defaultDescription`; `openGraph.siteName`/`title`/`description` and
|
|
50
|
+
`twitter.title`/`description`/`image` → set per page on `<Seo />`, or the site-wide values above.
|
|
51
|
+
A config that still carries one now fails `vitops validate` with an unrecognised-key error.
|
|
52
|
+
- `openGraph.type` is now `website | article | profile`.
|
package/README.md
CHANGED
|
@@ -1,3 +1,13 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @getvitops/config
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The site-config blocks the Vitops toolchain's provider integrations read — CRM, ads, reviews,
|
|
4
|
+
indexing, notifications, tracking — as [`@zod/mini`](https://zod.dev) schemas and inferred types.
|
|
5
|
+
|
|
6
|
+
```ts
|
|
7
|
+
import { CrmSchema, type CrmConfig } from '@getvitops/config';
|
|
8
|
+
import { AD_PROVIDERS } from '@getvitops/config/providers'; // bare literals, no zod
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
A leaf package: it imports `@zod/mini` and nothing else in the workspace. The umbrella `Config`
|
|
12
|
+
(`designSystem` / `organization` / `site`) lives in `@getvitops/generator`, which composes these blocks.
|
|
13
|
+
See the repo's `adr/0001-site-config-and-providers-packages.md`.
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
import { AD_PROVIDERS, AdProvider, BIGIN_DATA_CENTERS, CRM_PROVIDERS, CrmProviderConfig } from "./providers.mjs";
|
|
2
|
+
import * as z from "@zod/mini";
|
|
3
|
+
/** Attach a human description to a schema; it becomes the JSON Schema `description` editors show as a hover. */
|
|
4
|
+
export declare const desc: <T extends {
|
|
5
|
+
check(...checks: never[]): T;
|
|
6
|
+
}>(schema: T, text: string) => T;
|
|
7
|
+
/**
|
|
8
|
+
* One domain to onboard into Google Search Console via `vitops search setup`.
|
|
9
|
+
*
|
|
10
|
+
* The verification token is fetched live from the Site Verification API at run
|
|
11
|
+
* time and written to DNS — it is never stored here. What the config records is
|
|
12
|
+
* the *intent*: which domains to onboard, and who should co-own them.
|
|
13
|
+
*/
|
|
14
|
+
export declare const SearchConsoleSetupSchema: z.ZodMiniObject<{
|
|
15
|
+
delegatedOwners: z.ZodMiniOptional<z.ZodMiniArray<z.ZodMiniEmail>>;
|
|
16
|
+
fullUserGroup: z.ZodMiniOptional<z.ZodMiniEmail>;
|
|
17
|
+
}, z.core.$strict>;
|
|
18
|
+
/**
|
|
19
|
+
* What `vitops search notify` needs to tell search engines a deploy happened.
|
|
20
|
+
*
|
|
21
|
+
* Scoped by what search engines actually accept, which is narrower than it looks:
|
|
22
|
+
* Google exposes **no** "request indexing" API (the Search Console button is not in
|
|
23
|
+
* its API, and URL Inspection is read-only), and it removed the sitemap ping
|
|
24
|
+
* endpoint in 2023. So the sanctioned set is: an accurate sitemap, a Search Console
|
|
25
|
+
* sitemap resubmit, an IndexNow ping — which Google does *not* honour but Bing,
|
|
26
|
+
* Yandex, Naver, Seznam and Yep do — and a read-only inspection pass to find out
|
|
27
|
+
* what Google actually did.
|
|
28
|
+
*
|
|
29
|
+
* Facts only, the same rule the legal block follows: this records what the site has
|
|
30
|
+
* (a key, a property, which pages matter), never what to say about it.
|
|
31
|
+
*/
|
|
32
|
+
export declare const IndexNowSchema: z.ZodMiniObject<{
|
|
33
|
+
key: z.ZodMiniString<string>;
|
|
34
|
+
keyLocation: z.ZodMiniOptional<z.ZodMiniURL>;
|
|
35
|
+
endpoint: z.ZodMiniOptional<z.ZodMiniURL>;
|
|
36
|
+
}, z.core.$strict>;
|
|
37
|
+
export declare const SearchConsoleSchema: z.ZodMiniObject<{
|
|
38
|
+
siteUrl: z.ZodMiniString<string>;
|
|
39
|
+
resubmitSitemap: z.ZodMiniOptional<z.ZodMiniBoolean<boolean>>;
|
|
40
|
+
}, z.core.$strict>;
|
|
41
|
+
export declare const IndexingSchema: z.ZodMiniObject<{
|
|
42
|
+
sitemapUrl: z.ZodMiniOptional<z.ZodMiniURL>;
|
|
43
|
+
indexNow: z.ZodMiniOptional<z.ZodMiniObject<{
|
|
44
|
+
key: z.ZodMiniString<string>;
|
|
45
|
+
keyLocation: z.ZodMiniOptional<z.ZodMiniURL>;
|
|
46
|
+
endpoint: z.ZodMiniOptional<z.ZodMiniURL>;
|
|
47
|
+
}, z.core.$strict>>;
|
|
48
|
+
searchConsole: z.ZodMiniOptional<z.ZodMiniObject<{
|
|
49
|
+
siteUrl: z.ZodMiniString<string>;
|
|
50
|
+
resubmitSitemap: z.ZodMiniOptional<z.ZodMiniBoolean<boolean>>;
|
|
51
|
+
}, z.core.$strict>>;
|
|
52
|
+
priorityUrls: z.ZodMiniOptional<z.ZodMiniArray<z.ZodMiniURL>>;
|
|
53
|
+
}, z.core.$strict>;
|
|
54
|
+
export type IndexNowConfig = z.infer<typeof IndexNowSchema>;
|
|
55
|
+
export type SearchConsoleConfig = z.infer<typeof SearchConsoleSchema>;
|
|
56
|
+
export type IndexingConfig = z.infer<typeof IndexingSchema>;
|
|
57
|
+
export type SearchConsoleSetupConfig = z.infer<typeof SearchConsoleSetupSchema>;
|
|
58
|
+
export declare const AdProviderSchema: z.ZodMiniEnum<{
|
|
59
|
+
google: "google";
|
|
60
|
+
meta: "meta";
|
|
61
|
+
linkedin: "linkedin";
|
|
62
|
+
reddit: "reddit";
|
|
63
|
+
tiktok: "tiktok";
|
|
64
|
+
microsoft: "microsoft";
|
|
65
|
+
pinterest: "pinterest";
|
|
66
|
+
snapchat: "snapchat";
|
|
67
|
+
}>;
|
|
68
|
+
/**
|
|
69
|
+
* One ad property this site is linked to.
|
|
70
|
+
*
|
|
71
|
+
* Facts, not prose — the same rule the legal block follows. This records *which*
|
|
72
|
+
* account and *which* tag id, and the platform capability table decides what can be
|
|
73
|
+
* done about them. It is also what finally lets the rest of the toolchain see an ad
|
|
74
|
+
* pixel: the generated cookie notice discloses its cookies, `vitops ads lint`
|
|
75
|
+
* checks its click ID is captured, and the consent gate is handed the cookie names
|
|
76
|
+
* to clear on revoke. A pixel pasted into a template is invisible to all three.
|
|
77
|
+
*/
|
|
78
|
+
export declare const AdPropertySchema: z.ZodMiniObject<{
|
|
79
|
+
accountId: z.ZodMiniOptional<z.ZodMiniString<string>>;
|
|
80
|
+
pixelId: z.ZodMiniOptional<z.ZodMiniString<string>>;
|
|
81
|
+
conversionLabel: z.ZodMiniOptional<z.ZodMiniUnion<readonly [z.ZodMiniString<string>, z.ZodMiniObject<{
|
|
82
|
+
call: z.ZodMiniOptional<z.ZodMiniString<string>>;
|
|
83
|
+
email: z.ZodMiniOptional<z.ZodMiniString<string>>;
|
|
84
|
+
form: z.ZodMiniOptional<z.ZodMiniString<string>>;
|
|
85
|
+
}, z.core.$strict>]>>;
|
|
86
|
+
conversionActionId: z.ZodMiniOptional<z.ZodMiniUnion<readonly [z.ZodMiniString<string>, z.ZodMiniObject<{
|
|
87
|
+
call: z.ZodMiniOptional<z.ZodMiniString<string>>;
|
|
88
|
+
email: z.ZodMiniOptional<z.ZodMiniString<string>>;
|
|
89
|
+
form: z.ZodMiniOptional<z.ZodMiniString<string>>;
|
|
90
|
+
}, z.core.$strict>]>>;
|
|
91
|
+
reporting: z.ZodMiniOptional<z.ZodMiniEnum<{
|
|
92
|
+
client: "client";
|
|
93
|
+
server: "server";
|
|
94
|
+
}>>;
|
|
95
|
+
domain: z.ZodMiniOptional<z.ZodMiniString<string>>;
|
|
96
|
+
domainVerification: z.ZodMiniOptional<z.ZodMiniString<string>>;
|
|
97
|
+
category: z.ZodMiniOptional<z.ZodMiniEnum<{
|
|
98
|
+
marketing: "marketing";
|
|
99
|
+
analytics: "analytics";
|
|
100
|
+
}>>;
|
|
101
|
+
enabled: z.ZodMiniOptional<z.ZodMiniBoolean<boolean>>;
|
|
102
|
+
}, z.core.$strict>;
|
|
103
|
+
export type AdPropertyConfig = z.infer<typeof AdPropertySchema>;
|
|
104
|
+
/**
|
|
105
|
+
* How one location's Google reviews are selected and ordered for display.
|
|
106
|
+
*
|
|
107
|
+
* Keyed by `organization.locations` key, mirroring `site.ads`/`site.searchConsole`
|
|
108
|
+
* — `vitops reviews` needs to know which physical location a fetched review set
|
|
109
|
+
* belongs to, and that record already exists.
|
|
110
|
+
*/
|
|
111
|
+
export declare const ReviewLocationSchema: z.ZodMiniObject<{
|
|
112
|
+
accountId: z.ZodMiniOptional<z.ZodMiniString<string>>;
|
|
113
|
+
locationId: z.ZodMiniString<string>;
|
|
114
|
+
featured: z.ZodMiniOptional<z.ZodMiniArray<z.ZodMiniString<string>>>;
|
|
115
|
+
minRating: z.ZodMiniOptional<z.ZodMiniNumber<number>>;
|
|
116
|
+
limit: z.ZodMiniOptional<z.ZodMiniNumber<number>>;
|
|
117
|
+
sort: z.ZodMiniOptional<z.ZodMiniEnum<{
|
|
118
|
+
recent: "recent";
|
|
119
|
+
rating: "rating";
|
|
120
|
+
}>>;
|
|
121
|
+
}, z.core.$strict>;
|
|
122
|
+
export declare const ReviewsSchema: z.ZodMiniObject<{
|
|
123
|
+
accountId: z.ZodMiniOptional<z.ZodMiniString<string>>;
|
|
124
|
+
locations: z.ZodMiniRecord<z.ZodMiniString<string>, z.ZodMiniObject<{
|
|
125
|
+
accountId: z.ZodMiniOptional<z.ZodMiniString<string>>;
|
|
126
|
+
locationId: z.ZodMiniString<string>;
|
|
127
|
+
featured: z.ZodMiniOptional<z.ZodMiniArray<z.ZodMiniString<string>>>;
|
|
128
|
+
minRating: z.ZodMiniOptional<z.ZodMiniNumber<number>>;
|
|
129
|
+
limit: z.ZodMiniOptional<z.ZodMiniNumber<number>>;
|
|
130
|
+
sort: z.ZodMiniOptional<z.ZodMiniEnum<{
|
|
131
|
+
recent: "recent";
|
|
132
|
+
rating: "rating";
|
|
133
|
+
}>>;
|
|
134
|
+
}, z.core.$strict>>;
|
|
135
|
+
schema: z.ZodMiniOptional<z.ZodMiniBoolean<boolean>>;
|
|
136
|
+
avatars: z.ZodMiniOptional<z.ZodMiniBoolean<boolean>>;
|
|
137
|
+
}, z.core.$strict>;
|
|
138
|
+
export type ReviewsConfig = z.infer<typeof ReviewsSchema>;
|
|
139
|
+
export type ReviewLocationConfig = z.infer<typeof ReviewLocationSchema>;
|
|
140
|
+
export declare const CrmProviderSchema: z.ZodMiniEnum<{
|
|
141
|
+
hubspot: "hubspot";
|
|
142
|
+
bigin: "bigin";
|
|
143
|
+
jobber: "jobber";
|
|
144
|
+
}>;
|
|
145
|
+
export declare const HubSpotConfigSchema: z.ZodMiniObject<{
|
|
146
|
+
portalId: z.ZodMiniString<string>;
|
|
147
|
+
formGuid: z.ZodMiniString<string>;
|
|
148
|
+
dataCenter: z.ZodMiniOptional<z.ZodMiniEnum<{
|
|
149
|
+
eu: "eu";
|
|
150
|
+
na: "na";
|
|
151
|
+
}>>;
|
|
152
|
+
}, z.core.$strict>;
|
|
153
|
+
export declare const BiginPipelineConfigSchema: z.ZodMiniObject<{
|
|
154
|
+
enabled: z.ZodMiniBoolean<boolean>;
|
|
155
|
+
subPipeline: z.ZodMiniOptional<z.ZodMiniString<string>>;
|
|
156
|
+
stage: z.ZodMiniOptional<z.ZodMiniString<string>>;
|
|
157
|
+
}, z.core.$strict>;
|
|
158
|
+
export declare const BiginConfigSchema: z.ZodMiniObject<{
|
|
159
|
+
dataCenter: z.ZodMiniEnum<{
|
|
160
|
+
com: "com";
|
|
161
|
+
ca: "ca";
|
|
162
|
+
eu: "eu";
|
|
163
|
+
in: "in";
|
|
164
|
+
"com.au": "com.au";
|
|
165
|
+
jp: "jp";
|
|
166
|
+
}>;
|
|
167
|
+
duplicateCheckField: z.ZodMiniOptional<z.ZodMiniString<string>>;
|
|
168
|
+
pipeline: z.ZodMiniOptional<z.ZodMiniObject<{
|
|
169
|
+
enabled: z.ZodMiniBoolean<boolean>;
|
|
170
|
+
subPipeline: z.ZodMiniOptional<z.ZodMiniString<string>>;
|
|
171
|
+
stage: z.ZodMiniOptional<z.ZodMiniString<string>>;
|
|
172
|
+
}, z.core.$strict>>;
|
|
173
|
+
}, z.core.$strict>;
|
|
174
|
+
export declare const JobberNoteConfigSchema: z.ZodMiniObject<{
|
|
175
|
+
enabled: z.ZodMiniOptional<z.ZodMiniBoolean<boolean>>;
|
|
176
|
+
}, z.core.$strict>;
|
|
177
|
+
export declare const JobberConfigSchema: z.ZodMiniObject<{
|
|
178
|
+
apiVersion: z.ZodMiniOptional<z.ZodMiniString<string>>;
|
|
179
|
+
dedupe: z.ZodMiniOptional<z.ZodMiniBoolean<boolean>>;
|
|
180
|
+
note: z.ZodMiniOptional<z.ZodMiniObject<{
|
|
181
|
+
enabled: z.ZodMiniOptional<z.ZodMiniBoolean<boolean>>;
|
|
182
|
+
}, z.core.$strict>>;
|
|
183
|
+
}, z.core.$strict>;
|
|
184
|
+
/**
|
|
185
|
+
* A client's CRM, wired to the same conversion route `site.notifications`
|
|
186
|
+
* already reaches — see AGENTS.md's "CRM submissions" section for why this is
|
|
187
|
+
* server-side-only and hung off `createConversionRoute()` rather than a
|
|
188
|
+
* client-side embed. Credentials never live here — the toolchain has no
|
|
189
|
+
* secret store, matching every other integration.
|
|
190
|
+
*/
|
|
191
|
+
export declare const CrmSchema: z.ZodMiniObject<{
|
|
192
|
+
provider: z.ZodMiniEnum<{
|
|
193
|
+
hubspot: "hubspot";
|
|
194
|
+
bigin: "bigin";
|
|
195
|
+
jobber: "jobber";
|
|
196
|
+
}>;
|
|
197
|
+
enabled: z.ZodMiniOptional<z.ZodMiniBoolean<boolean>>;
|
|
198
|
+
hubspot: z.ZodMiniOptional<z.ZodMiniObject<{
|
|
199
|
+
portalId: z.ZodMiniString<string>;
|
|
200
|
+
formGuid: z.ZodMiniString<string>;
|
|
201
|
+
dataCenter: z.ZodMiniOptional<z.ZodMiniEnum<{
|
|
202
|
+
eu: "eu";
|
|
203
|
+
na: "na";
|
|
204
|
+
}>>;
|
|
205
|
+
}, z.core.$strict>>;
|
|
206
|
+
bigin: z.ZodMiniOptional<z.ZodMiniObject<{
|
|
207
|
+
dataCenter: z.ZodMiniEnum<{
|
|
208
|
+
com: "com";
|
|
209
|
+
ca: "ca";
|
|
210
|
+
eu: "eu";
|
|
211
|
+
in: "in";
|
|
212
|
+
"com.au": "com.au";
|
|
213
|
+
jp: "jp";
|
|
214
|
+
}>;
|
|
215
|
+
duplicateCheckField: z.ZodMiniOptional<z.ZodMiniString<string>>;
|
|
216
|
+
pipeline: z.ZodMiniOptional<z.ZodMiniObject<{
|
|
217
|
+
enabled: z.ZodMiniBoolean<boolean>;
|
|
218
|
+
subPipeline: z.ZodMiniOptional<z.ZodMiniString<string>>;
|
|
219
|
+
stage: z.ZodMiniOptional<z.ZodMiniString<string>>;
|
|
220
|
+
}, z.core.$strict>>;
|
|
221
|
+
}, z.core.$strict>>;
|
|
222
|
+
jobber: z.ZodMiniOptional<z.ZodMiniObject<{
|
|
223
|
+
apiVersion: z.ZodMiniOptional<z.ZodMiniString<string>>;
|
|
224
|
+
dedupe: z.ZodMiniOptional<z.ZodMiniBoolean<boolean>>;
|
|
225
|
+
note: z.ZodMiniOptional<z.ZodMiniObject<{
|
|
226
|
+
enabled: z.ZodMiniOptional<z.ZodMiniBoolean<boolean>>;
|
|
227
|
+
}, z.core.$strict>>;
|
|
228
|
+
}, z.core.$strict>>;
|
|
229
|
+
fieldMap: z.ZodMiniOptional<z.ZodMiniRecord<z.ZodMiniString<string>, z.ZodMiniString<string>>>;
|
|
230
|
+
trackingFields: z.ZodMiniOptional<z.ZodMiniRecord<z.ZodMiniString<string>, z.ZodMiniString<string>>>;
|
|
231
|
+
}, z.core.$strict>;
|
|
232
|
+
export type CrmConfig = z.infer<typeof CrmSchema>;
|
|
233
|
+
export type HubSpotConfig = z.infer<typeof HubSpotConfigSchema>;
|
|
234
|
+
export type BiginConfig = z.infer<typeof BiginConfigSchema>;
|
|
235
|
+
export type BiginPipelineConfig = z.infer<typeof BiginPipelineConfigSchema>;
|
|
236
|
+
export type JobberConfig = z.infer<typeof JobberConfigSchema>;
|
|
237
|
+
/** `site.notifications` */
|
|
238
|
+
export declare const NotificationsSchema: z.ZodMiniOptional<z.ZodMiniObject<{
|
|
239
|
+
email: z.ZodMiniOptional<z.ZodMiniUnion<readonly [z.ZodMiniEmail, z.ZodMiniObject<{
|
|
240
|
+
provider: z.ZodMiniLiteral<"cloudflare">;
|
|
241
|
+
to: z.ZodMiniOptional<z.ZodMiniEmail>;
|
|
242
|
+
from: z.ZodMiniOptional<z.ZodMiniEmail>;
|
|
243
|
+
fromName: z.ZodMiniOptional<z.ZodMiniString<string>>;
|
|
244
|
+
replyTo: z.ZodMiniOptional<z.ZodMiniEmail>;
|
|
245
|
+
binding: z.ZodMiniOptional<z.ZodMiniString<string>>;
|
|
246
|
+
verifySender: z.ZodMiniOptional<z.ZodMiniBoolean<boolean>>;
|
|
247
|
+
}, z.core.$strict>]>>;
|
|
248
|
+
actionUrl: z.ZodMiniOptional<z.ZodMiniString<string>>;
|
|
249
|
+
}, z.core.$strict>>;
|
|
250
|
+
/** `site.tracking` */
|
|
251
|
+
export declare const TrackingSchema: z.ZodMiniOptional<z.ZodMiniObject<{
|
|
252
|
+
enabled: z.ZodMiniOptional<z.ZodMiniBoolean<boolean>>;
|
|
253
|
+
category: z.ZodMiniOptional<z.ZodMiniEnum<{
|
|
254
|
+
marketing: "marketing";
|
|
255
|
+
analytics: "analytics";
|
|
256
|
+
}>>;
|
|
257
|
+
platforms: z.ZodMiniOptional<z.ZodMiniArray<z.ZodMiniString<string>>>;
|
|
258
|
+
}, z.core.$strict>>;
|
|
259
|
+
/** `site.security` */
|
|
260
|
+
export declare const SecuritySchema: z.ZodMiniOptional<z.ZodMiniObject<{
|
|
261
|
+
turnstile: z.ZodMiniOptional<z.ZodMiniObject<{
|
|
262
|
+
siteKey: z.ZodMiniOptional<z.ZodMiniString<string>>;
|
|
263
|
+
}, z.core.$strict>>;
|
|
264
|
+
}, z.core.$strict>>;
|
|
265
|
+
/** `site.deployment` */
|
|
266
|
+
export declare const DeploymentSchema: z.ZodMiniOptional<z.ZodMiniObject<{
|
|
267
|
+
platform: z.ZodMiniOptional<z.ZodMiniString<string>>;
|
|
268
|
+
buildCommand: z.ZodMiniOptional<z.ZodMiniString<string>>;
|
|
269
|
+
deployCommand: z.ZodMiniOptional<z.ZodMiniString<string>>;
|
|
270
|
+
outputDirectory: z.ZodMiniOptional<z.ZodMiniString<string>>;
|
|
271
|
+
type: z.ZodMiniOptional<z.ZodMiniString<string>>;
|
|
272
|
+
repo: z.ZodMiniOptional<z.ZodMiniString<string>>;
|
|
273
|
+
branch: z.ZodMiniOptional<z.ZodMiniString<string>>;
|
|
274
|
+
}, z.core.$strict>>;
|
|
275
|
+
export type NotificationsConfig = NonNullable<z.infer<typeof NotificationsSchema>>;
|
|
276
|
+
export type TrackingConfig = NonNullable<z.infer<typeof TrackingSchema>>;
|
|
277
|
+
export type SecurityConfig = NonNullable<z.infer<typeof SecuritySchema>>;
|
|
278
|
+
export type DeploymentConfig = NonNullable<z.infer<typeof DeploymentSchema>>;
|
|
279
|
+
/** A string, or a per-locale map of strings (`{ en: "…", fr: "…" }`). */
|
|
280
|
+
export declare const LocalizableSchema: z.ZodMiniUnion<readonly [z.ZodMiniString<string>, z.ZodMiniRecord<z.ZodMiniString<string>, z.ZodMiniString<string>>]>;
|
|
281
|
+
/**
|
|
282
|
+
* `site.seo` — the single source of site-level SEO defaults.
|
|
283
|
+
*
|
|
284
|
+
* Read by `<Seo />` (through `@getvitops/astro`'s `resolveSeoDefaults`), `vitops robots` and
|
|
285
|
+
* `vitops search notify`. Every display field here is consumed by `resolveSeoDefaults`;
|
|
286
|
+
* `seo-defaults.test.ts` fails when a field is added to this schema and not read, which is how
|
|
287
|
+
* four fields once sat here doing nothing (ADR-0002).
|
|
288
|
+
*/
|
|
289
|
+
export declare const SeoSchema: z.ZodMiniObject<{
|
|
290
|
+
siteName: z.ZodMiniOptional<z.ZodMiniUnion<readonly [z.ZodMiniString<string>, z.ZodMiniRecord<z.ZodMiniString<string>, z.ZodMiniString<string>>]>>;
|
|
291
|
+
titleTemplate: z.ZodMiniOptional<z.ZodMiniUnion<readonly [z.ZodMiniString<string>, z.ZodMiniRecord<z.ZodMiniString<string>, z.ZodMiniString<string>>]>>;
|
|
292
|
+
defaultTitle: z.ZodMiniOptional<z.ZodMiniUnion<readonly [z.ZodMiniString<string>, z.ZodMiniRecord<z.ZodMiniString<string>, z.ZodMiniString<string>>]>>;
|
|
293
|
+
defaultDescription: z.ZodMiniOptional<z.ZodMiniUnion<readonly [z.ZodMiniString<string>, z.ZodMiniRecord<z.ZodMiniString<string>, z.ZodMiniString<string>>]>>;
|
|
294
|
+
robots: z.ZodMiniOptional<z.ZodMiniString<string>>;
|
|
295
|
+
googleSiteVerification: z.ZodMiniOptional<z.ZodMiniString<string>>;
|
|
296
|
+
bingSiteVerification: z.ZodMiniOptional<z.ZodMiniString<string>>;
|
|
297
|
+
openGraph: z.ZodMiniOptional<z.ZodMiniObject<{
|
|
298
|
+
type: z.ZodMiniOptional<z.ZodMiniEnum<{
|
|
299
|
+
website: "website";
|
|
300
|
+
article: "article";
|
|
301
|
+
profile: "profile";
|
|
302
|
+
}>>;
|
|
303
|
+
locale: z.ZodMiniOptional<z.ZodMiniString<string>>;
|
|
304
|
+
localeAlternates: z.ZodMiniOptional<z.ZodMiniArray<z.ZodMiniString<string>>>;
|
|
305
|
+
image: z.ZodMiniOptional<z.ZodMiniUnion<readonly [z.ZodMiniString<string>, z.ZodMiniObject<{
|
|
306
|
+
src: z.ZodMiniString<string>;
|
|
307
|
+
alt: z.ZodMiniOptional<z.ZodMiniString<string>>;
|
|
308
|
+
width: z.ZodMiniOptional<z.ZodMiniNumber<number>>;
|
|
309
|
+
height: z.ZodMiniOptional<z.ZodMiniNumber<number>>;
|
|
310
|
+
type: z.ZodMiniOptional<z.ZodMiniString<string>>;
|
|
311
|
+
}, z.core.$strict>]>>;
|
|
312
|
+
}, z.core.$strict>>;
|
|
313
|
+
twitter: z.ZodMiniOptional<z.ZodMiniObject<{
|
|
314
|
+
card: z.ZodMiniOptional<z.ZodMiniEnum<{
|
|
315
|
+
summary: "summary";
|
|
316
|
+
summary_large_image: "summary_large_image";
|
|
317
|
+
app: "app";
|
|
318
|
+
player: "player";
|
|
319
|
+
}>>;
|
|
320
|
+
site: z.ZodMiniOptional<z.ZodMiniString<string>>;
|
|
321
|
+
creator: z.ZodMiniOptional<z.ZodMiniString<string>>;
|
|
322
|
+
}, z.core.$strict>>;
|
|
323
|
+
indexing: z.ZodMiniOptional<z.ZodMiniObject<{
|
|
324
|
+
sitemapUrl: z.ZodMiniOptional<z.ZodMiniURL>;
|
|
325
|
+
indexNow: z.ZodMiniOptional<z.ZodMiniObject<{
|
|
326
|
+
key: z.ZodMiniString<string>;
|
|
327
|
+
keyLocation: z.ZodMiniOptional<z.ZodMiniURL>;
|
|
328
|
+
endpoint: z.ZodMiniOptional<z.ZodMiniURL>;
|
|
329
|
+
}, z.core.$strict>>;
|
|
330
|
+
searchConsole: z.ZodMiniOptional<z.ZodMiniObject<{
|
|
331
|
+
siteUrl: z.ZodMiniString<string>;
|
|
332
|
+
resubmitSitemap: z.ZodMiniOptional<z.ZodMiniBoolean<boolean>>;
|
|
333
|
+
}, z.core.$strict>>;
|
|
334
|
+
priorityUrls: z.ZodMiniOptional<z.ZodMiniArray<z.ZodMiniURL>>;
|
|
335
|
+
}, z.core.$strict>>;
|
|
336
|
+
}, z.core.$strict>;
|
|
337
|
+
export type SeoConfig = z.infer<typeof SeoSchema>;
|
|
338
|
+
export { AD_PROVIDERS, AdProvider, BIGIN_DATA_CENTERS, CRM_PROVIDERS, CrmProviderConfig };
|
|
339
|
+
//# sourceMappingURL=index.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/desc.ts","../src/indexing.ts","../src/ads.ts","../src/reviews.ts","../src/crm.ts","../src/blocks.ts","../src/seo.ts"],"mappings":";;;qBAGa,OAAQ;EAAY,SAAS,kBAAkB;GAAK,QAAQ,GAAG,iBAAe;;;;;;;;qBCO9E,0BAAwB,EAAA;;;GASnC,EAAA,KAAA;;;;;;;;;;;;;;;qBAgBW,gBAAc,EAAA;;;;GAazB,EAAA,KAAA;qBAEW,qBAAmB,EAAA;;;GAS9B,EAAA,KAAA;qBAEW,gBAAc,EAAA;;;;;;;;;;;;GAiBzB,EAAA,KAAA;YAEU,iBAAiB,EAAE,aAAa;YAChC,sBAAsB,EAAE,aAAa;YACrC,iBAAiB,EAAE,aAAa;YAChC,2BAA2B,EAAE,aAAa;qBC/EzC,kBAAgB,EAAA;;;;;;;;;;;;;;;;;;;;qBAYhB,kBAAgB,EAAA;;;;;;;;;;;;;;;;;;;;;;;;GAqE3B,EAAA,KAAA;YAEU,mBAAmB,EAAE,aAAa;;;;;;;;qBC7EjC,sBAAoB,EAAA;;;;;;;;;;GAmB/B,EAAA,KAAA;qBAEW,eAAa,EAAA;;;;;;;;;;;;;;;GAiBxB,EAAA,KAAA;YAEU,gBAAgB,EAAE,aAAa;YAC/B,uBAAuB,EAAE,aAAa;qBC/CrC,mBAAiB,EAAA;;;;;qBAEjB,qBAAmB,EAAA;;;;;;;GAO9B,EAAA,KAAA;qBAEW,2BAAyB,EAAA;;;;GAIpC,EAAA,KAAA;qBAEW,mBAAiB,EAAA;;;;;;;;;;;;;;;GAa5B,EAAA,KAAA;qBAEW,wBAAsB,EAAA;;GAKjC,EAAA,KAAA;qBAEW,oBAAkB,EAAA;;;;;;GAa7B,EAAA,KAAA;;;;;;;;qBASW,WAAS,EAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiBpB,EAAA,KAAA;YAEU,YAAY,EAAE,aAAa;YAC3B,gBAAgB,EAAE,aAAa;YAC/B,cAAc,EAAE,aAAa;YAC7B,sBAAsB,EAAE,aAAa;YACrC,eAAe,EAAE,aAAa;;qBCpF7B,qBAAmB,EAAA,gBAAA,EAAA;;;;;;;;;;;GA6C/B,EAAA,KAAA;;qBAGY,gBAAc,EAAA,gBAAA,EAAA;;;;;;;GAe1B,EAAA,KAAA;;qBAGY,gBAAc,EAAA,gBAAA,EAAA;;;;GAQ1B,EAAA,KAAA;;qBAGY,kBAAgB,EAAA,gBAAA,EAAA;;;;;;;;GAgB5B,EAAA,KAAA;YAEW,sBAAsB,YAAY,EAAE,aAAa;YACjD,iBAAiB,YAAY,EAAE,aAAa;YAC5C,iBAAiB,YAAY,EAAE,aAAa;YAC5C,mBAAmB,YAAY,EAAE,aAAa;;qBCjG7C,mBAAiB,EAAA,uBAAA,EAAA,uBAAA,EAAA,cAAA,EAAA,uBAAA,EAAA;;;;;;;;;qBA8BjB,WAAS,EAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+DpB,EAAA,KAAA;YAEU,YAAY,EAAE,aAAa"}
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import { AD_PROVIDERS, BIGIN_DATA_CENTERS, CRM_PROVIDERS } from "./providers.mjs";
|
|
2
|
+
import * as z from "@zod/mini";
|
|
3
|
+
|
|
4
|
+
// src/desc.ts
|
|
5
|
+
/** Attach a human description to a schema; it becomes the JSON Schema `description` editors show as a hover. */
|
|
6
|
+
const desc = (schema, text) => schema.check(z.describe(text));
|
|
7
|
+
|
|
8
|
+
// src/indexing.ts
|
|
9
|
+
/**
|
|
10
|
+
* One domain to onboard into Google Search Console via `vitops search setup`.
|
|
11
|
+
*
|
|
12
|
+
* The verification token is fetched live from the Site Verification API at run
|
|
13
|
+
* time and written to DNS — it is never stored here. What the config records is
|
|
14
|
+
* the *intent*: which domains to onboard, and who should co-own them.
|
|
15
|
+
*/
|
|
16
|
+
const SearchConsoleSetupSchema = z.strictObject({
|
|
17
|
+
delegatedOwners: desc(z.optional(z.array(z.email())), "Emails added as verification owners on the Site Verification web resource. Automated via the API — additive and idempotent, never removes an existing owner."),
|
|
18
|
+
fullUserGroup: desc(z.optional(z.email()), "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.")
|
|
19
|
+
});
|
|
20
|
+
/**
|
|
21
|
+
* What `vitops search notify` needs to tell search engines a deploy happened.
|
|
22
|
+
*
|
|
23
|
+
* Scoped by what search engines actually accept, which is narrower than it looks:
|
|
24
|
+
* Google exposes **no** "request indexing" API (the Search Console button is not in
|
|
25
|
+
* its API, and URL Inspection is read-only), and it removed the sitemap ping
|
|
26
|
+
* endpoint in 2023. So the sanctioned set is: an accurate sitemap, a Search Console
|
|
27
|
+
* sitemap resubmit, an IndexNow ping — which Google does *not* honour but Bing,
|
|
28
|
+
* Yandex, Naver, Seznam and Yep do — and a read-only inspection pass to find out
|
|
29
|
+
* what Google actually did.
|
|
30
|
+
*
|
|
31
|
+
* Facts only, the same rule the legal block follows: this records what the site has
|
|
32
|
+
* (a key, a property, which pages matter), never what to say about it.
|
|
33
|
+
*/
|
|
34
|
+
const IndexNowSchema = z.strictObject({
|
|
35
|
+
key: desc(z.string().check(z.regex(/^[a-zA-Z0-9-]{8,128}$/)), "IndexNow key (8–128 chars, hex is conventional). NOT a secret — it is served publicly at `keyLocation` so the engine can verify you own the host. Generate one with `vitops search notify --new-key`."),
|
|
36
|
+
keyLocation: desc(z.optional(z.url()), "Absolute URL of the key file. Defaults to `<canonical>/<key>.txt`; set it only when the file lives elsewhere."),
|
|
37
|
+
endpoint: desc(z.optional(z.url()), "IndexNow endpoint (default `https://api.indexnow.org/indexnow`). Any participating engine shares submissions with the rest, so one is normally enough.")
|
|
38
|
+
});
|
|
39
|
+
const SearchConsoleSchema = z.strictObject({
|
|
40
|
+
siteUrl: desc(z.string(), "The property exactly as Search Console identifies it — `sc-domain:acme.ca` for a domain property, or the URL-prefix form `https://acme.ca/`. A mismatch here is a 403, not a \"not found\"."),
|
|
41
|
+
resubmitSitemap: desc(z.optional(z.boolean()), "Re-submit the sitemap through the Search Console API on each notify (default true when `searchConsole` is set). This is the automated equivalent of the manual resubmit in the UI.")
|
|
42
|
+
});
|
|
43
|
+
const IndexingSchema = z.strictObject({
|
|
44
|
+
sitemapUrl: desc(z.optional(z.url()), "The sitemap to submit and to diff for changed URLs. Defaults to `<canonical>/sitemap-index.xml`."),
|
|
45
|
+
indexNow: desc(z.optional(IndexNowSchema), "IndexNow submission (Bing, Yandex, Naver, Seznam, Yep — not Google). Omit to skip the channel."),
|
|
46
|
+
searchConsole: desc(z.optional(SearchConsoleSchema), "Google Search Console property. Needs a credential at run time — either a service account in `VITOPS_GSC_SERVICE_ACCOUNT` / `GOOGLE_APPLICATION_CREDENTIALS`, or the user OAuth credential `vitops search setup` uses (`VITOPS_GOOGLE_CLIENT_ID` / `_CLIENT_SECRET` / `_REFRESH_TOKEN`). The service account wins when both are set. Never put a key in this file."),
|
|
47
|
+
priorityUrls: desc(z.optional(z.array(z.url())), "The pages whose indexing actually matters. `vitops search notify --check` inspects these and exits non-zero if Google has not indexed one. Kept explicit because URL Inspection is quota-bound (2000/day), so checking every page is neither affordable nor informative.")
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
// src/ads.ts
|
|
51
|
+
const AdProviderSchema = z.enum(AD_PROVIDERS);
|
|
52
|
+
/**
|
|
53
|
+
* One ad property this site is linked to.
|
|
54
|
+
*
|
|
55
|
+
* Facts, not prose — the same rule the legal block follows. This records *which*
|
|
56
|
+
* account and *which* tag id, and the platform capability table decides what can be
|
|
57
|
+
* done about them. It is also what finally lets the rest of the toolchain see an ad
|
|
58
|
+
* pixel: the generated cookie notice discloses its cookies, `vitops ads lint`
|
|
59
|
+
* checks its click ID is captured, and the consent gate is handed the cookie names
|
|
60
|
+
* to clear on revoke. A pixel pasted into a template is invisible to all three.
|
|
61
|
+
*/
|
|
62
|
+
const AdPropertySchema = z.strictObject({
|
|
63
|
+
accountId: desc(z.optional(z.string()), "The advertising account as the platform shows it — Google Ads customer ID, Meta ad account, LinkedIn partner ID, Reddit advertiser ID. For LinkedIn this is also the Insight Tag id; everywhere else the tag id is `pixelId`."),
|
|
64
|
+
pixelId: desc(z.optional(z.string()), "The tag/pixel ID the browser snippet initialises — Meta pixel, TikTok pixel, Reddit pixel, Microsoft UET tag ID, Pinterest tag ID, Snap pixel, or the Google Ads conversion ID (`AW-…`, which is NOT the customer ID)."),
|
|
65
|
+
conversionLabel: desc(z.optional(z.union([z.string(), z.strictObject({
|
|
66
|
+
call: desc(z.optional(z.string()), "Fired on a tel: link click."),
|
|
67
|
+
email: desc(z.optional(z.string()), "Fired on a mailto: link click."),
|
|
68
|
+
form: desc(z.optional(z.string()), "Fired client-side by `vitopsFormConversion()` — the consumer's own submit handler calls it once a form POST succeeds.")
|
|
69
|
+
})])), "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."),
|
|
70
|
+
conversionActionId: desc(z.optional(z.union([z.string(), z.strictObject({
|
|
71
|
+
call: desc(z.optional(z.string()), "Fired on a tel: link click."),
|
|
72
|
+
email: desc(z.optional(z.string()), "Fired on a mailto: link click."),
|
|
73
|
+
form: desc(z.optional(z.string()), "Fired on a form submission.")
|
|
74
|
+
})])), "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\"`."),
|
|
75
|
+
reporting: desc(z.optional(z.enum(["client", "server"])), "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."),
|
|
76
|
+
domain: desc(z.optional(z.string()), "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."),
|
|
77
|
+
domainVerification: desc(z.optional(z.string()), "The verification token from the platform UI, for the platforms that verify by DNS TXT (Meta, TikTok, Pinterest, Snapchat). `vitops ads setup` prompts for it and writes it here on first run. NOT a secret — it is published in DNS, and the platform fetching it back is the ownership proof, exactly like the IndexNow key. A value containing `=` is used as the whole record, which is the escape hatch if a platform changes its prefix."),
|
|
78
|
+
category: desc(z.optional(z.enum(["marketing", "analytics"])), "Consent category the tag waits on (default `marketing`). An ad pixel is advertising by default; `analytics` is for a site using one purely for its own measurement."),
|
|
79
|
+
enabled: desc(z.optional(z.boolean()), "Set false to keep the property on record without emitting its tag.")
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
// src/reviews.ts
|
|
83
|
+
/**
|
|
84
|
+
* How one location's Google reviews are selected and ordered for display.
|
|
85
|
+
*
|
|
86
|
+
* Keyed by `organization.locations` key, mirroring `site.ads`/`site.searchConsole`
|
|
87
|
+
* — `vitops reviews` needs to know which physical location a fetched review set
|
|
88
|
+
* belongs to, and that record already exists.
|
|
89
|
+
*/
|
|
90
|
+
const ReviewLocationSchema = z.strictObject({
|
|
91
|
+
accountId: desc(z.optional(z.string()), "GBP account resource id (`accounts/…`), overriding `site.reviews.accountId` for this location."),
|
|
92
|
+
locationId: desc(z.string(), "GBP location resource id (`locations/…`) to fetch reviews for."),
|
|
93
|
+
featured: desc(z.optional(z.array(z.string())), "Review ids to always include, in this order, ahead of everything else automatically selected."),
|
|
94
|
+
minRating: desc(z.optional(z.number()), "Drop reviews below this star rating (default 4) from the automatic (non-featured) selection."),
|
|
95
|
+
limit: desc(z.optional(z.number()), "Keep at most this many reviews after filtering."),
|
|
96
|
+
sort: desc(z.optional(z.enum(["recent", "rating"])), "Ordering for the non-featured remainder (default `recent`).")
|
|
97
|
+
});
|
|
98
|
+
const ReviewsSchema = z.strictObject({
|
|
99
|
+
accountId: desc(z.optional(z.string()), "Default GBP account resource id (`accounts/…`) for locations that do not override it."),
|
|
100
|
+
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`."),
|
|
101
|
+
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."),
|
|
102
|
+
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.")
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
// src/crm.ts
|
|
106
|
+
const CrmProviderSchema = z.enum(CRM_PROVIDERS);
|
|
107
|
+
const HubSpotConfigSchema = z.strictObject({
|
|
108
|
+
portalId: desc(z.string(), "The HubSpot portal (hub) ID the form belongs to."),
|
|
109
|
+
formGuid: desc(z.string(), "The HubSpot form GUID to submit against."),
|
|
110
|
+
dataCenter: desc(z.optional(z.enum(["na", "eu"])), "Which regional submissions API to post to (default `na`).")
|
|
111
|
+
});
|
|
112
|
+
const BiginPipelineConfigSchema = z.strictObject({
|
|
113
|
+
enabled: desc(z.boolean(), "Also open a Pipelines record for the new/updated contact."),
|
|
114
|
+
subPipeline: desc(z.optional(z.string()), "Sub-pipeline to file the record under."),
|
|
115
|
+
stage: desc(z.optional(z.string()), "Starting stage for the pipeline record.")
|
|
116
|
+
});
|
|
117
|
+
const BiginConfigSchema = z.strictObject({
|
|
118
|
+
dataCenter: desc(z.enum(BIGIN_DATA_CENTERS), "The self-client's data centre — determines both the OAuth accounts host and the API host (`www.zohoapis.<dc>`)."),
|
|
119
|
+
duplicateCheckField: desc(z.optional(z.string()), "Bigin field the upsert dedupes on (default `Email`, Bigin's own unique field)."),
|
|
120
|
+
pipeline: desc(z.optional(BiginPipelineConfigSchema), "Also open a Pipelines record alongside the Contact.")
|
|
121
|
+
});
|
|
122
|
+
const JobberNoteConfigSchema = z.strictObject({ enabled: desc(z.optional(z.boolean()), "Attach a best-effort note carrying the message and unmapped attribution (default true). A failure here never loses the client record itself.") });
|
|
123
|
+
const JobberConfigSchema = z.strictObject({
|
|
124
|
+
apiVersion: desc(z.optional(z.string()), "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."),
|
|
125
|
+
dedupe: desc(z.optional(z.boolean()), "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."),
|
|
126
|
+
note: desc(z.optional(JobberNoteConfigSchema), "Attach a best-effort note carrying the submitted message and any attribution/extra fields with no dedicated Jobber field.")
|
|
127
|
+
});
|
|
128
|
+
/**
|
|
129
|
+
* A client's CRM, wired to the same conversion route `site.notifications`
|
|
130
|
+
* already reaches — see AGENTS.md's "CRM submissions" section for why this is
|
|
131
|
+
* server-side-only and hung off `createConversionRoute()` rather than a
|
|
132
|
+
* client-side embed. Credentials never live here — the toolchain has no
|
|
133
|
+
* secret store, matching every other integration.
|
|
134
|
+
*/
|
|
135
|
+
const CrmSchema = z.strictObject({
|
|
136
|
+
provider: desc(CrmProviderSchema, "Which CRM this site pushes form submissions into."),
|
|
137
|
+
enabled: desc(z.optional(z.boolean()), "Set false to keep the block on record without writing to the CRM."),
|
|
138
|
+
hubspot: desc(z.optional(HubSpotConfigSchema), "Required when `provider` is `hubspot`."),
|
|
139
|
+
bigin: desc(z.optional(BiginConfigSchema), "Required when `provider` is `bigin`."),
|
|
140
|
+
jobber: desc(z.optional(JobberConfigSchema), "Required when `provider` is `jobber`."),
|
|
141
|
+
fieldMap: desc(z.optional(z.record(z.string(), z.string())), "Submitted form field name → CRM property name. A field with no entry here is dropped, not guessed — an unmapped field never reaches the CRM."),
|
|
142
|
+
trackingFields: desc(z.optional(z.record(z.string(), z.string())), "`_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.")
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
// src/blocks.ts
|
|
146
|
+
/** `site.notifications` */
|
|
147
|
+
const NotificationsSchema = z.optional(z.strictObject({
|
|
148
|
+
email: desc(z.optional(z.union([z.email(), z.strictObject({
|
|
149
|
+
provider: desc(z.literal("cloudflare"), "Delivery provider. Only Cloudflare Email Sending is implemented; the field is stated so adding another is additive."),
|
|
150
|
+
to: desc(z.optional(z.email()), "Recipient. Defaults to the primary location’s `email`, then the first location that has one."),
|
|
151
|
+
from: desc(z.optional(z.email()), "Sender. Defaults to `noreply@<domains.canonical>`. Its domain must be onboarded (`wrangler email sending enable <domain>`) or every send fails."),
|
|
152
|
+
fromName: desc(z.optional(z.string()), "Display name on the From header. Defaults to `organization.name`."),
|
|
153
|
+
replyTo: desc(z.optional(z.email()), "Reply-To header."),
|
|
154
|
+
binding: desc(z.optional(z.string()), "Workers `send_email` binding name (default `EMAIL`). Set it when using a restricted binding."),
|
|
155
|
+
verifySender: desc(z.optional(z.boolean()), "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.")
|
|
156
|
+
})])), "Where conversion notifications are e-mailed. A bare address is shorthand for `{ provider: \"cloudflare\", to }`."),
|
|
157
|
+
actionUrl: desc(z.optional(z.string()), "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.")
|
|
158
|
+
}));
|
|
159
|
+
/** `site.tracking` */
|
|
160
|
+
const TrackingSchema = z.optional(z.strictObject({
|
|
161
|
+
enabled: desc(z.optional(z.boolean()), "Capture ad click IDs and UTMs from the landing URL into the `_ac` cookie, for attributing conversions."),
|
|
162
|
+
category: desc(z.optional(z.enum(["marketing", "analytics"])), "Consent category the `_ac` cookie waits on (default `marketing`). It is a 90-day identifier tying a visitor to the ad that brought them, so it is only written once this category is granted — and asking for it is what raises the banner."),
|
|
163
|
+
platforms: desc(z.optional(z.array(z.string())), "Informational list of ad platforms in use. Capture recognises every known click-ID parameter regardless.")
|
|
164
|
+
}));
|
|
165
|
+
/** `site.security` */
|
|
166
|
+
const SecuritySchema = z.optional(z.strictObject({ turnstile: z.optional(z.strictObject({ siteKey: desc(z.optional(z.string()), "Cloudflare Turnstile site key (public).") })) }));
|
|
167
|
+
/** `site.deployment` */
|
|
168
|
+
const DeploymentSchema = z.optional(z.strictObject({
|
|
169
|
+
platform: z.optional(z.string()),
|
|
170
|
+
buildCommand: z.optional(z.string()),
|
|
171
|
+
deployCommand: z.optional(z.string()),
|
|
172
|
+
outputDirectory: z.optional(z.string()),
|
|
173
|
+
type: z.optional(z.string()),
|
|
174
|
+
repo: desc(z.optional(z.string()), "`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."),
|
|
175
|
+
branch: desc(z.optional(z.string()), "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.")
|
|
176
|
+
}));
|
|
177
|
+
|
|
178
|
+
// src/seo.ts
|
|
179
|
+
/** A string, or a per-locale map of strings (`{ en: "…", fr: "…" }`). */
|
|
180
|
+
const LocalizableSchema = desc(z.union([z.string(), z.record(z.string(), z.string())]), "A string, or a per-locale map of strings (`{ en: \"…\", fr: \"…\" }`).");
|
|
181
|
+
/**
|
|
182
|
+
* A share image: a bare URL, or the URL with the metadata crawlers read alongside it.
|
|
183
|
+
*
|
|
184
|
+
* Distinct from a content image (`alt` is required there): a site-wide fallback card often has
|
|
185
|
+
* no meaningful alt text, and `<Seo />` already omits `og:image:alt` when none is given.
|
|
186
|
+
*/
|
|
187
|
+
const SeoImageSchema = z.union([z.string(), z.strictObject({
|
|
188
|
+
src: desc(z.string(), "Absolute, or relative to `site` (resolved at render time)."),
|
|
189
|
+
alt: z.optional(z.string()),
|
|
190
|
+
width: z.optional(z.number()),
|
|
191
|
+
height: z.optional(z.number()),
|
|
192
|
+
type: desc(z.optional(z.string()), "MIME type, e.g. `image/png`.")
|
|
193
|
+
})]);
|
|
194
|
+
/**
|
|
195
|
+
* `site.seo` — the single source of site-level SEO defaults.
|
|
196
|
+
*
|
|
197
|
+
* Read by `<Seo />` (through `@getvitops/astro`'s `resolveSeoDefaults`), `vitops robots` and
|
|
198
|
+
* `vitops search notify`. Every display field here is consumed by `resolveSeoDefaults`;
|
|
199
|
+
* `seo-defaults.test.ts` fails when a field is added to this schema and not read, which is how
|
|
200
|
+
* four fields once sat here doing nothing (ADR-0002).
|
|
201
|
+
*/
|
|
202
|
+
const SeoSchema = z.strictObject({
|
|
203
|
+
siteName: desc(z.optional(LocalizableSchema), "`og:site_name`, and the `<title>` when a page passes none. Defaults to `organization.name`, so most sites never state it."),
|
|
204
|
+
titleTemplate: desc(z.optional(LocalizableSchema), "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."),
|
|
205
|
+
defaultTitle: desc(z.optional(LocalizableSchema), "`<title>` for pages that pass none. Used verbatim — the template is not applied. Defaults to the site name."),
|
|
206
|
+
defaultDescription: desc(z.optional(LocalizableSchema), "`<meta name=\"description\">` for pages that pass none."),
|
|
207
|
+
robots: desc(z.optional(z.string()), "Site-wide robots directive (e.g. \"index,follow\" / \"noindex,nofollow\"). `site.environments.<env>.robots` wins over it."),
|
|
208
|
+
googleSiteVerification: desc(z.optional(z.string()), "Google Search Console verification token (`<meta name=\"google-site-verification\">`)."),
|
|
209
|
+
bingSiteVerification: desc(z.optional(z.string()), "Bing Webmaster Tools verification token (`<meta name=\"msvalidate.01\">`)."),
|
|
210
|
+
openGraph: desc(z.optional(z.strictObject({
|
|
211
|
+
type: desc(z.optional(z.enum([
|
|
212
|
+
"website",
|
|
213
|
+
"article",
|
|
214
|
+
"profile"
|
|
215
|
+
])), "Default `og:type`. Default `website`."),
|
|
216
|
+
locale: desc(z.optional(z.string()), "`og:locale`. `en-CA` and `en_CA` both emit `en_CA`."),
|
|
217
|
+
localeAlternates: desc(z.optional(z.array(z.string())), "Other locales the same content exists in → `og:locale:alternate`."),
|
|
218
|
+
image: desc(z.optional(SeoImageSchema), "Fallback share image for pages that bring none.")
|
|
219
|
+
})), "Open Graph defaults. Per-page `<Seo />` props override them."),
|
|
220
|
+
twitter: desc(z.optional(z.strictObject({
|
|
221
|
+
card: desc(z.optional(z.enum([
|
|
222
|
+
"summary",
|
|
223
|
+
"summary_large_image",
|
|
224
|
+
"app",
|
|
225
|
+
"player"
|
|
226
|
+
])), "Card type when no image resolves (default `summary`)."),
|
|
227
|
+
site: desc(z.optional(z.string()), "The site's own handle, e.g. \"@acme\"."),
|
|
228
|
+
creator: desc(z.optional(z.string()), "Default author handle, e.g. \"@ada\".")
|
|
229
|
+
})), "Twitter/X card defaults."),
|
|
230
|
+
indexing: desc(z.optional(IndexingSchema), "How `vitops search notify` tells search engines about a deploy: which sitemap, IndexNow key, Search Console property, and which pages to verify afterwards.")
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
export { AD_PROVIDERS, AdPropertySchema, AdProviderSchema, BIGIN_DATA_CENTERS, BiginConfigSchema, BiginPipelineConfigSchema, CRM_PROVIDERS, CrmProviderSchema, CrmSchema, DeploymentSchema, HubSpotConfigSchema, IndexNowSchema, IndexingSchema, JobberConfigSchema, JobberNoteConfigSchema, LocalizableSchema, NotificationsSchema, ReviewLocationSchema, ReviewsSchema, SearchConsoleSchema, SearchConsoleSetupSchema, SecuritySchema, SeoSchema, TrackingSchema, desc };
|
|
234
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../src/desc.ts","../src/indexing.ts","../src/ads.ts","../src/reviews.ts","../src/crm.ts","../src/blocks.ts","../src/seo.ts"],"sourcesContent":["import * as z from '@zod/mini';\n\n/** Attach a human description to a schema; it becomes the JSON Schema `description` editors show as a hover. */\nexport const desc = <T extends { check(...checks: never[]): T }>(schema: T, text: string): T =>\n schema.check(z.describe(text) as never);\n","import * as z from '@zod/mini';\nimport { desc } from './desc.ts';\n\n/**\n * One domain to onboard into Google Search Console via `vitops search setup`.\n *\n * The verification token is fetched live from the Site Verification API at run\n * time and written to DNS — it is never stored here. What the config records is\n * the *intent*: which domains to onboard, and who should co-own them.\n */\nexport const SearchConsoleSetupSchema = z.strictObject({\n delegatedOwners: desc(\n z.optional(z.array(z.email())),\n 'Emails added as verification owners on the Site Verification web resource. Automated via the API — additive and idempotent, never removes an existing owner.',\n ),\n fullUserGroup: desc(\n z.optional(z.email()),\n '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.',\n ),\n});\n\n/**\n * What `vitops search notify` needs to tell search engines a deploy happened.\n *\n * Scoped by what search engines actually accept, which is narrower than it looks:\n * Google exposes **no** \"request indexing\" API (the Search Console button is not in\n * its API, and URL Inspection is read-only), and it removed the sitemap ping\n * endpoint in 2023. So the sanctioned set is: an accurate sitemap, a Search Console\n * sitemap resubmit, an IndexNow ping — which Google does *not* honour but Bing,\n * Yandex, Naver, Seznam and Yep do — and a read-only inspection pass to find out\n * what Google actually did.\n *\n * Facts only, the same rule the legal block follows: this records what the site has\n * (a key, a property, which pages matter), never what to say about it.\n */\nexport const IndexNowSchema = z.strictObject({\n key: desc(\n z.string().check(z.regex(/^[a-zA-Z0-9-]{8,128}$/)),\n 'IndexNow key (8–128 chars, hex is conventional). NOT a secret — it is served publicly at `keyLocation` so the engine can verify you own the host. Generate one with `vitops search notify --new-key`.',\n ),\n keyLocation: desc(\n z.optional(z.url()),\n 'Absolute URL of the key file. Defaults to `<canonical>/<key>.txt`; set it only when the file lives elsewhere.',\n ),\n endpoint: desc(\n z.optional(z.url()),\n 'IndexNow endpoint (default `https://api.indexnow.org/indexnow`). Any participating engine shares submissions with the rest, so one is normally enough.',\n ),\n});\n\nexport const SearchConsoleSchema = z.strictObject({\n siteUrl: desc(\n z.string(),\n 'The property exactly as Search Console identifies it — `sc-domain:acme.ca` for a domain property, or the URL-prefix form `https://acme.ca/`. A mismatch here is a 403, not a \"not found\".',\n ),\n resubmitSitemap: desc(\n z.optional(z.boolean()),\n 'Re-submit the sitemap through the Search Console API on each notify (default true when `searchConsole` is set). This is the automated equivalent of the manual resubmit in the UI.',\n ),\n});\n\nexport const IndexingSchema = z.strictObject({\n sitemapUrl: desc(\n z.optional(z.url()),\n 'The sitemap to submit and to diff for changed URLs. Defaults to `<canonical>/sitemap-index.xml`.',\n ),\n indexNow: desc(\n z.optional(IndexNowSchema),\n 'IndexNow submission (Bing, Yandex, Naver, Seznam, Yep — not Google). Omit to skip the channel.',\n ),\n searchConsole: desc(\n z.optional(SearchConsoleSchema),\n 'Google Search Console property. Needs a credential at run time — either a service account in `VITOPS_GSC_SERVICE_ACCOUNT` / `GOOGLE_APPLICATION_CREDENTIALS`, or the user OAuth credential `vitops search setup` uses (`VITOPS_GOOGLE_CLIENT_ID` / `_CLIENT_SECRET` / `_REFRESH_TOKEN`). The service account wins when both are set. Never put a key in this file.',\n ),\n priorityUrls: desc(\n z.optional(z.array(z.url())),\n 'The pages whose indexing actually matters. `vitops search notify --check` inspects these and exits non-zero if Google has not indexed one. Kept explicit because URL Inspection is quota-bound (2000/day), so checking every page is neither affordable nor informative.',\n ),\n});\n\nexport type IndexNowConfig = z.infer<typeof IndexNowSchema>;\nexport type SearchConsoleConfig = z.infer<typeof SearchConsoleSchema>;\nexport type IndexingConfig = z.infer<typeof IndexingSchema>;\nexport type SearchConsoleSetupConfig = z.infer<typeof SearchConsoleSetupSchema>;\n","import * as z from '@zod/mini';\nimport { desc } from './desc.ts';\nimport { AD_PROVIDERS } from './providers.ts';\n\nexport const AdProviderSchema = z.enum(AD_PROVIDERS);\n\n/**\n * One ad property this site is linked to.\n *\n * Facts, not prose — the same rule the legal block follows. This records *which*\n * account and *which* tag id, and the platform capability table decides what can be\n * done about them. It is also what finally lets the rest of the toolchain see an ad\n * pixel: the generated cookie notice discloses its cookies, `vitops ads lint`\n * checks its click ID is captured, and the consent gate is handed the cookie names\n * to clear on revoke. A pixel pasted into a template is invisible to all three.\n */\nexport const AdPropertySchema = z.strictObject({\n accountId: desc(\n z.optional(z.string()),\n 'The advertising account as the platform shows it — Google Ads customer ID, Meta ad account, LinkedIn partner ID, Reddit advertiser ID. For LinkedIn this is also the Insight Tag id; everywhere else the tag id is `pixelId`.',\n ),\n pixelId: desc(\n z.optional(z.string()),\n 'The tag/pixel ID the browser snippet initialises — Meta pixel, TikTok pixel, Reddit pixel, Microsoft UET tag ID, Pinterest tag ID, Snap pixel, or the Google Ads conversion ID (`AW-…`, which is NOT the customer ID).',\n ),\n conversionLabel: desc(\n z.optional(\n z.union([\n z.string(),\n z.strictObject({\n call: desc(z.optional(z.string()), 'Fired on a tel: link click.'),\n email: desc(z.optional(z.string()), 'Fired on a mailto: link click.'),\n form: desc(\n z.optional(z.string()),\n \"Fired client-side by `vitopsFormConversion()` — the consumer's own submit handler calls it once a form POST succeeds.\",\n ),\n }),\n ]),\n ),\n 'Google Ads only: the conversion action label(s) paired with the `AW-…` conversion ID — ' +\n 'either one bare label (back-compat, treated as the `call` action) or a map of ' +\n 'action → label, so a call, an email click and a form submission each fire their own ' +\n 'Google Ads conversion action instead of all three reporting as the same one.',\n ),\n conversionActionId: desc(\n z.optional(\n z.union([\n z.string(),\n z.strictObject({\n call: desc(z.optional(z.string()), 'Fired on a tel: link click.'),\n email: desc(z.optional(z.string()), 'Fired on a mailto: link click.'),\n form: desc(z.optional(z.string()), 'Fired on a form submission.'),\n }),\n ]),\n ),\n 'Google Ads only: the conversion action **resource id(s)** (a numeric id, or a full ' +\n '`customers/…/conversionActions/…` resource name) used for server-side offline ' +\n 'conversion upload — NOT the same value as `conversionLabel`, which is the opaque ' +\n \"label the web tag uses and which the Ads API's upload endpoint does not accept. \" +\n 'Only meaningful when `reporting: \"server\"`.',\n ),\n reporting: desc(\n z.optional(z.enum(['client', 'server'])),\n 'Google Ads only: `\"client\"` (default) fires the conversion via `gtag.js` in the ' +\n 'browser at click/submit time; `\"server\"` uploads it via the Ads API from the ' +\n 'conversion endpoint instead (see `conversionActionId`). An action with no matching ' +\n '`conversionActionId` keeps firing client-side regardless of this setting, so setting ' +\n 'it before `conversionActionId` is filled in never silently drops a conversion.',\n ),\n domain: desc(\n z.optional(z.string()),\n '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.',\n ),\n domainVerification: desc(\n z.optional(z.string()),\n 'The verification token from the platform UI, for the platforms that verify by DNS TXT (Meta, TikTok, Pinterest, Snapchat). `vitops ads setup` prompts for it and writes it here on first run. NOT a secret — it is published in DNS, and the platform fetching it back is the ownership proof, exactly like the IndexNow key. A value containing `=` is used as the whole record, which is the escape hatch if a platform changes its prefix.',\n ),\n category: desc(\n z.optional(z.enum(['marketing', 'analytics'])),\n 'Consent category the tag waits on (default `marketing`). An ad pixel is advertising by default; `analytics` is for a site using one purely for its own measurement.',\n ),\n enabled: desc(\n z.optional(z.boolean()),\n 'Set false to keep the property on record without emitting its tag.',\n ),\n});\n\nexport type AdPropertyConfig = z.infer<typeof AdPropertySchema>;\n","import * as z from '@zod/mini';\nimport { desc } from './desc.ts';\n\n/**\n * How one location's Google reviews are selected and ordered for display.\n *\n * Keyed by `organization.locations` key, mirroring `site.ads`/`site.searchConsole`\n * — `vitops reviews` needs to know which physical location a fetched review set\n * belongs to, and that record already exists.\n */\nexport const ReviewLocationSchema = z.strictObject({\n accountId: desc(\n z.optional(z.string()),\n 'GBP account resource id (`accounts/…`), overriding `site.reviews.accountId` for this location.',\n ),\n locationId: desc(z.string(), 'GBP location resource id (`locations/…`) to fetch reviews for.'),\n featured: desc(\n z.optional(z.array(z.string())),\n 'Review ids to always include, in this order, ahead of everything else automatically selected.',\n ),\n minRating: desc(\n z.optional(z.number()),\n 'Drop reviews below this star rating (default 4) from the automatic (non-featured) selection.',\n ),\n limit: desc(z.optional(z.number()), 'Keep at most this many reviews after filtering.'),\n sort: desc(\n z.optional(z.enum(['recent', 'rating'])),\n 'Ordering for the non-featured remainder (default `recent`).',\n ),\n});\n\nexport const ReviewsSchema = z.strictObject({\n accountId: desc(\n z.optional(z.string()),\n 'Default GBP account resource id (`accounts/…`) for locations that do not override it.',\n ),\n locations: desc(\n z.record(z.string(), ReviewLocationSchema),\n '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`.',\n ),\n schema: desc(\n z.optional(z.boolean()),\n '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.',\n ),\n avatars: desc(\n z.optional(z.boolean()),\n '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.',\n ),\n});\n\nexport type ReviewsConfig = z.infer<typeof ReviewsSchema>;\nexport type ReviewLocationConfig = z.infer<typeof ReviewLocationSchema>;\n","import * as z from '@zod/mini';\nimport { desc } from './desc.ts';\nimport { BIGIN_DATA_CENTERS, CRM_PROVIDERS } from './providers.ts';\n\nexport const CrmProviderSchema = z.enum(CRM_PROVIDERS);\n\nexport const HubSpotConfigSchema = z.strictObject({\n portalId: desc(z.string(), 'The HubSpot portal (hub) ID the form belongs to.'),\n formGuid: desc(z.string(), 'The HubSpot form GUID to submit against.'),\n dataCenter: desc(\n z.optional(z.enum(['na', 'eu'])),\n 'Which regional submissions API to post to (default `na`).',\n ),\n});\n\nexport const BiginPipelineConfigSchema = z.strictObject({\n enabled: desc(z.boolean(), 'Also open a Pipelines record for the new/updated contact.'),\n subPipeline: desc(z.optional(z.string()), 'Sub-pipeline to file the record under.'),\n stage: desc(z.optional(z.string()), 'Starting stage for the pipeline record.'),\n});\n\nexport const BiginConfigSchema = z.strictObject({\n dataCenter: desc(\n z.enum(BIGIN_DATA_CENTERS),\n \"The self-client's data centre — determines both the OAuth accounts host and the API host (`www.zohoapis.<dc>`).\",\n ),\n duplicateCheckField: desc(\n z.optional(z.string()),\n \"Bigin field the upsert dedupes on (default `Email`, Bigin's own unique field).\",\n ),\n pipeline: desc(\n z.optional(BiginPipelineConfigSchema),\n 'Also open a Pipelines record alongside the Contact.',\n ),\n});\n\nexport const JobberNoteConfigSchema = z.strictObject({\n enabled: desc(\n z.optional(z.boolean()),\n 'Attach a best-effort note carrying the message and unmapped attribution (default true). A failure here never loses the client record itself.',\n ),\n});\n\nexport const JobberConfigSchema = z.strictObject({\n apiVersion: desc(\n z.optional(z.string()),\n \"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.\",\n ),\n dedupe: desc(\n z.optional(z.boolean()),\n \"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.\",\n ),\n note: desc(\n z.optional(JobberNoteConfigSchema),\n 'Attach a best-effort note carrying the submitted message and any attribution/extra fields with no dedicated Jobber field.',\n ),\n});\n\n/**\n * A client's CRM, wired to the same conversion route `site.notifications`\n * already reaches — see AGENTS.md's \"CRM submissions\" section for why this is\n * server-side-only and hung off `createConversionRoute()` rather than a\n * client-side embed. Credentials never live here — the toolchain has no\n * secret store, matching every other integration.\n */\nexport const CrmSchema = z.strictObject({\n provider: desc(CrmProviderSchema, 'Which CRM this site pushes form submissions into.'),\n enabled: desc(\n z.optional(z.boolean()),\n 'Set false to keep the block on record without writing to the CRM.',\n ),\n hubspot: desc(z.optional(HubSpotConfigSchema), 'Required when `provider` is `hubspot`.'),\n bigin: desc(z.optional(BiginConfigSchema), 'Required when `provider` is `bigin`.'),\n jobber: desc(z.optional(JobberConfigSchema), 'Required when `provider` is `jobber`.'),\n fieldMap: desc(\n z.optional(z.record(z.string(), z.string())),\n 'Submitted form field name → CRM property name. A field with no entry here is dropped, not guessed — an unmapped field never reaches the CRM.',\n ),\n trackingFields: desc(\n z.optional(z.record(z.string(), z.string())),\n '`_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.',\n ),\n});\n\nexport type CrmConfig = z.infer<typeof CrmSchema>;\nexport type HubSpotConfig = z.infer<typeof HubSpotConfigSchema>;\nexport type BiginConfig = z.infer<typeof BiginConfigSchema>;\nexport type BiginPipelineConfig = z.infer<typeof BiginPipelineConfigSchema>;\nexport type JobberConfig = z.infer<typeof JobberConfigSchema>;\n","import * as z from '@zod/mini';\nimport { desc } from './desc.ts';\n\n/** `site.notifications` */\nexport const NotificationsSchema = z.optional(\n z.strictObject({\n email: desc(\n z.optional(\n z.union([\n z.email(),\n z.strictObject({\n provider: desc(\n z.literal('cloudflare'),\n 'Delivery provider. Only Cloudflare Email Sending is implemented; the field is stated so adding another is additive.',\n ),\n to: desc(\n z.optional(z.email()),\n 'Recipient. Defaults to the primary location’s `email`, then the first location that has one.',\n ),\n from: desc(\n z.optional(z.email()),\n 'Sender. Defaults to `noreply@<domains.canonical>`. Its domain must be onboarded (`wrangler email sending enable <domain>`) or every send fails.',\n ),\n fromName: desc(\n z.optional(z.string()),\n 'Display name on the From header. Defaults to `organization.name`.',\n ),\n replyTo: desc(z.optional(z.email()), 'Reply-To header.'),\n binding: desc(\n z.optional(z.string()),\n 'Workers `send_email` binding name (default `EMAIL`). Set it when using a restricted binding.',\n ),\n verifySender: desc(\n z.optional(z.boolean()),\n \"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.\",\n ),\n }),\n ]),\n ),\n 'Where conversion notifications are e-mailed. A bare address is shorthand for `{ provider: \"cloudflare\", to }`.',\n ),\n actionUrl: desc(\n z.optional(z.string()),\n 'Optional action-link template added to the conversion email (e.g. a link into your ' +\n 'ad platform or your own CRM/portal). `{clickId}` is replaced with the ad click ID ' +\n 'captured on `_ac` (gclid, fbclid, …). Omitted from the email when the event carries ' +\n 'no click ID to interpolate, rather than rendering a broken link.',\n ),\n }),\n);\n\n/** `site.tracking` */\nexport const TrackingSchema = z.optional(\n z.strictObject({\n enabled: desc(\n z.optional(z.boolean()),\n 'Capture ad click IDs and UTMs from the landing URL into the `_ac` cookie, for attributing conversions.',\n ),\n category: desc(\n z.optional(z.enum(['marketing', 'analytics'])),\n 'Consent category the `_ac` cookie waits on (default `marketing`). It is a 90-day identifier tying a visitor to the ad that brought them, so it is only written once this category is granted — and asking for it is what raises the banner.',\n ),\n platforms: desc(\n z.optional(z.array(z.string())),\n 'Informational list of ad platforms in use. Capture recognises every known click-ID parameter regardless.',\n ),\n }),\n);\n\n/** `site.security` */\nexport const SecuritySchema = z.optional(\n z.strictObject({\n turnstile: z.optional(\n z.strictObject({\n siteKey: desc(z.optional(z.string()), 'Cloudflare Turnstile site key (public).'),\n }),\n ),\n }),\n);\n\n/** `site.deployment` */\nexport const DeploymentSchema = z.optional(\n z.strictObject({\n platform: z.optional(z.string()),\n buildCommand: z.optional(z.string()),\n deployCommand: z.optional(z.string()),\n outputDirectory: z.optional(z.string()),\n type: z.optional(z.string()),\n repo: desc(\n z.optional(z.string()),\n \"`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.\",\n ),\n branch: desc(\n z.optional(z.string()),\n '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.',\n ),\n }),\n);\n\nexport type NotificationsConfig = NonNullable<z.infer<typeof NotificationsSchema>>;\nexport type TrackingConfig = NonNullable<z.infer<typeof TrackingSchema>>;\nexport type SecurityConfig = NonNullable<z.infer<typeof SecuritySchema>>;\nexport type DeploymentConfig = NonNullable<z.infer<typeof DeploymentSchema>>;\n","import * as z from '@zod/mini';\nimport { desc } from './desc.ts';\nimport { IndexingSchema } from './indexing.ts';\n\n/** A string, or a per-locale map of strings (`{ en: \"…\", fr: \"…\" }`). */\nexport const LocalizableSchema = desc(\n z.union([z.string(), z.record(z.string(), z.string())]),\n 'A string, or a per-locale map of strings (`{ en: \"…\", fr: \"…\" }`).',\n);\n\n/**\n * A share image: a bare URL, or the URL with the metadata crawlers read alongside it.\n *\n * Distinct from a content image (`alt` is required there): a site-wide fallback card often has\n * no meaningful alt text, and `<Seo />` already omits `og:image:alt` when none is given.\n */\nconst SeoImageSchema = z.union([\n z.string(),\n z.strictObject({\n src: desc(z.string(), 'Absolute, or relative to `site` (resolved at render time).'),\n alt: z.optional(z.string()),\n width: z.optional(z.number()),\n height: z.optional(z.number()),\n type: desc(z.optional(z.string()), 'MIME type, e.g. `image/png`.'),\n }),\n]);\n\n/**\n * `site.seo` — the single source of site-level SEO defaults.\n *\n * Read by `<Seo />` (through `@getvitops/astro`'s `resolveSeoDefaults`), `vitops robots` and\n * `vitops search notify`. Every display field here is consumed by `resolveSeoDefaults`;\n * `seo-defaults.test.ts` fails when a field is added to this schema and not read, which is how\n * four fields once sat here doing nothing (ADR-0002).\n */\nexport const SeoSchema = z.strictObject({\n siteName: desc(\n z.optional(LocalizableSchema),\n '`og:site_name`, and the `<title>` when a page passes none. Defaults to `organization.name`, so most sites never state it.',\n ),\n titleTemplate: desc(\n z.optional(LocalizableSchema),\n '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.',\n ),\n defaultTitle: desc(\n z.optional(LocalizableSchema),\n '`<title>` for pages that pass none. Used verbatim — the template is not applied. Defaults to the site name.',\n ),\n defaultDescription: desc(\n z.optional(LocalizableSchema),\n '`<meta name=\"description\">` for pages that pass none.',\n ),\n robots: desc(\n z.optional(z.string()),\n 'Site-wide robots directive (e.g. \"index,follow\" / \"noindex,nofollow\"). `site.environments.<env>.robots` wins over it.',\n ),\n googleSiteVerification: desc(\n z.optional(z.string()),\n 'Google Search Console verification token (`<meta name=\"google-site-verification\">`).',\n ),\n bingSiteVerification: desc(\n z.optional(z.string()),\n 'Bing Webmaster Tools verification token (`<meta name=\"msvalidate.01\">`).',\n ),\n openGraph: desc(\n z.optional(\n z.strictObject({\n type: desc(\n z.optional(z.enum(['website', 'article', 'profile'])),\n 'Default `og:type`. Default `website`.',\n ),\n locale: desc(z.optional(z.string()), '`og:locale`. `en-CA` and `en_CA` both emit `en_CA`.'),\n localeAlternates: desc(\n z.optional(z.array(z.string())),\n 'Other locales the same content exists in → `og:locale:alternate`.',\n ),\n image: desc(z.optional(SeoImageSchema), 'Fallback share image for pages that bring none.'),\n }),\n ),\n 'Open Graph defaults. Per-page `<Seo />` props override them.',\n ),\n twitter: desc(\n z.optional(\n z.strictObject({\n card: desc(\n z.optional(z.enum(['summary', 'summary_large_image', 'app', 'player'])),\n 'Card type when no image resolves (default `summary`).',\n ),\n site: desc(z.optional(z.string()), 'The site\\'s own handle, e.g. \"@acme\".'),\n creator: desc(z.optional(z.string()), 'Default author handle, e.g. \"@ada\".'),\n }),\n ),\n 'Twitter/X card defaults.',\n ),\n indexing: desc(\n z.optional(IndexingSchema),\n 'How `vitops search notify` tells search engines about a deploy: which sitemap, IndexNow key, Search Console property, and which pages to verify afterwards.',\n ),\n});\n\nexport type SeoConfig = z.infer<typeof SeoSchema>;\n"],"mappings":"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"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Closed provider lists, as bare literals. No zod import — see vite.config.ts for why this is its own entry.
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* The ad platforms `vitops ads` can link a site to.
|
|
6
|
+
*
|
|
7
|
+
* A closed enum, mirroring `AD_PROVIDER_KEYS` in `@getvitops/utils/ads` — that
|
|
8
|
+
* module owns what each platform *can do* (whether it verifies a domain, what its
|
|
9
|
+
* tag is, which click ID it stamps), this owns what a config may *say*. Mirrored
|
|
10
|
+
* rather than derived because a `z.enum` needs literals to reach the published JSON
|
|
11
|
+
* Schema; `config.test.ts` asserts the two lists are the same set, so a provider
|
|
12
|
+
* added to one and not the other fails the build rather than silently getting no
|
|
13
|
+
* verification step and no tag.
|
|
14
|
+
*/
|
|
15
|
+
export declare const AD_PROVIDERS: readonly ["google", "meta", "linkedin", "reddit", "tiktok", "microsoft", "pinterest", "snapchat"];
|
|
16
|
+
export type AdProvider = (typeof AD_PROVIDERS)[number];
|
|
17
|
+
/**
|
|
18
|
+
* The CRMs this toolchain can push a form submission into.
|
|
19
|
+
*
|
|
20
|
+
* A closed enum, mirroring `CrmProvider` in `@getvitops/utils/crm` — that module
|
|
21
|
+
* owns what each provider's API needs, this owns what a config may say.
|
|
22
|
+
* Mirrored rather than derived for the same reason `AD_PROVIDERS` is: a
|
|
23
|
+
* `z.enum` needs literals to reach the published JSON Schema.
|
|
24
|
+
*/
|
|
25
|
+
export declare const CRM_PROVIDERS: readonly ["hubspot", "bigin", "jobber"];
|
|
26
|
+
export type CrmProviderConfig = (typeof CRM_PROVIDERS)[number];
|
|
27
|
+
export declare const BIGIN_DATA_CENTERS: readonly ["com", "ca", "eu", "in", "com.au", "jp"];
|
|
28
|
+
//# sourceMappingURL=providers.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"providers.d.mts","names":[],"sources":["../src/providers.ts"],"mappings":";;;;;;;;;;;;;;qBAea;YAWD,qBAAqB;;;;;;;;;qBAUpB;YAED,4BAA4B;qBAE3B"}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// src/providers.ts
|
|
2
|
+
/**
|
|
3
|
+
* Closed provider lists, as bare literals. No zod import — see vite.config.ts for why this is its own entry.
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* The ad platforms `vitops ads` can link a site to.
|
|
7
|
+
*
|
|
8
|
+
* A closed enum, mirroring `AD_PROVIDER_KEYS` in `@getvitops/utils/ads` — that
|
|
9
|
+
* module owns what each platform *can do* (whether it verifies a domain, what its
|
|
10
|
+
* tag is, which click ID it stamps), this owns what a config may *say*. Mirrored
|
|
11
|
+
* rather than derived because a `z.enum` needs literals to reach the published JSON
|
|
12
|
+
* Schema; `config.test.ts` asserts the two lists are the same set, so a provider
|
|
13
|
+
* added to one and not the other fails the build rather than silently getting no
|
|
14
|
+
* verification step and no tag.
|
|
15
|
+
*/
|
|
16
|
+
const AD_PROVIDERS = [
|
|
17
|
+
"google",
|
|
18
|
+
"meta",
|
|
19
|
+
"linkedin",
|
|
20
|
+
"reddit",
|
|
21
|
+
"tiktok",
|
|
22
|
+
"microsoft",
|
|
23
|
+
"pinterest",
|
|
24
|
+
"snapchat"
|
|
25
|
+
];
|
|
26
|
+
/**
|
|
27
|
+
* The CRMs this toolchain can push a form submission into.
|
|
28
|
+
*
|
|
29
|
+
* A closed enum, mirroring `CrmProvider` in `@getvitops/utils/crm` — that module
|
|
30
|
+
* owns what each provider's API needs, this owns what a config may say.
|
|
31
|
+
* Mirrored rather than derived for the same reason `AD_PROVIDERS` is: a
|
|
32
|
+
* `z.enum` needs literals to reach the published JSON Schema.
|
|
33
|
+
*/
|
|
34
|
+
const CRM_PROVIDERS = [
|
|
35
|
+
"hubspot",
|
|
36
|
+
"bigin",
|
|
37
|
+
"jobber"
|
|
38
|
+
];
|
|
39
|
+
const BIGIN_DATA_CENTERS = [
|
|
40
|
+
"com",
|
|
41
|
+
"ca",
|
|
42
|
+
"eu",
|
|
43
|
+
"in",
|
|
44
|
+
"com.au",
|
|
45
|
+
"jp"
|
|
46
|
+
];
|
|
47
|
+
|
|
48
|
+
export { AD_PROVIDERS, BIGIN_DATA_CENTERS, CRM_PROVIDERS };
|
|
49
|
+
//# sourceMappingURL=providers.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"providers.mjs","names":[],"sources":["../src/providers.ts"],"sourcesContent":["/**\n * Closed provider lists, as bare literals. No zod import — see vite.config.ts for why this is its own entry.\n */\n\n/**\n * The ad platforms `vitops ads` can link a site to.\n *\n * A closed enum, mirroring `AD_PROVIDER_KEYS` in `@getvitops/utils/ads` — that\n * module owns what each platform *can do* (whether it verifies a domain, what its\n * tag is, which click ID it stamps), this owns what a config may *say*. Mirrored\n * rather than derived because a `z.enum` needs literals to reach the published JSON\n * Schema; `config.test.ts` asserts the two lists are the same set, so a provider\n * added to one and not the other fails the build rather than silently getting no\n * verification step and no tag.\n */\nexport const AD_PROVIDERS = [\n 'google',\n 'meta',\n 'linkedin',\n 'reddit',\n 'tiktok',\n 'microsoft',\n 'pinterest',\n 'snapchat',\n] as const;\n\nexport type AdProvider = (typeof AD_PROVIDERS)[number];\n\n/**\n * The CRMs this toolchain can push a form submission into.\n *\n * A closed enum, mirroring `CrmProvider` in `@getvitops/utils/crm` — that module\n * owns what each provider's API needs, this owns what a config may say.\n * Mirrored rather than derived for the same reason `AD_PROVIDERS` is: a\n * `z.enum` needs literals to reach the published JSON Schema.\n */\nexport const CRM_PROVIDERS = ['hubspot', 'bigin', 'jobber'] as const;\n\nexport type CrmProviderConfig = (typeof CRM_PROVIDERS)[number];\n\nexport const BIGIN_DATA_CENTERS = ['com', 'ca', 'eu', 'in', 'com.au', 'jp'] as const;\n"],"mappings":";;;;;;;;;;;;;;;AAeA,MAAa,eAAe;CAC1B;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACF;;;;;;;;;AAYA,MAAa,gBAAgB;CAAC;CAAW;CAAS;AAAQ;AAI1D,MAAa,qBAAqB;CAAC;CAAO;CAAM;CAAM;CAAM;CAAU;AAAI"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,41 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@getvitops/config",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "8.2.0",
|
|
4
|
+
"description": "The site-config schema blocks the Vitops toolchain's provider integrations read — CRM, ads, reviews, indexing, notifications, tracking — as zod schemas and inferred types. A leaf package: it imports nothing else in the toolchain.",
|
|
5
|
+
"homepage": "https://docs.vitops.ca",
|
|
6
|
+
"bugs": "https://github.com/getvitops/design-system/issues",
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/getvitops/design-system.git",
|
|
11
|
+
"directory": "packages/config"
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"dist",
|
|
15
|
+
"CHANGELOG.md"
|
|
16
|
+
],
|
|
17
|
+
"type": "module",
|
|
18
|
+
"sideEffects": false,
|
|
19
|
+
"main": "./dist/index.mjs",
|
|
20
|
+
"types": "./dist/index.d.mts",
|
|
21
|
+
"exports": {
|
|
22
|
+
".": {
|
|
23
|
+
"types": "./dist/index.d.mts",
|
|
24
|
+
"import": "./dist/index.mjs"
|
|
25
|
+
},
|
|
26
|
+
"./providers": {
|
|
27
|
+
"types": "./dist/providers.d.mts",
|
|
28
|
+
"import": "./dist/providers.mjs"
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
"publishConfig": {
|
|
32
|
+
"access": "public"
|
|
33
|
+
},
|
|
34
|
+
"dependencies": {
|
|
35
|
+
"@zod/mini": "^4.6.5",
|
|
36
|
+
"zod": "^4.6.7"
|
|
37
|
+
},
|
|
38
|
+
"engines": {
|
|
39
|
+
"node": ">=24"
|
|
40
|
+
}
|
|
6
41
|
}
|