@rathnasgala2/schemas 3.21.0 → 3.22.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 (48) hide show
  1. package/THIRD_PARTY_LICENSES.json +1 -1
  2. package/diagnostics/diagnostic-map.json +236 -24
  3. package/docs/catalogs/schema-inventory.json +5 -5
  4. package/examples/valid/build-input/rules.json +229 -28
  5. package/examples/valid/interactions-config/rules.json +22 -0
  6. package/examples/valid/publication/rules.json +192 -13
  7. package/fixtures/boundary/build-input/rules.json +265 -33
  8. package/fixtures/boundary/interactions-config/rules.json +22 -0
  9. package/fixtures/boundary/publication/rules.json +243 -33
  10. package/fixtures/invalid/build-input/GRAPHEME_LENGTH_INVALID/cases.json +19 -8
  11. package/fixtures/invalid/build-input/REQUEST_FIELD_UNKNOWN/cases.json +80 -16
  12. package/fixtures/invalid/build-input/SCHEMA_ARRAY_DUPLICATE/cases.json +11 -0
  13. package/fixtures/invalid/build-input/SCHEMA_ARRAY_TOO_LONG/cases.json +15 -0
  14. package/fixtures/invalid/build-input/SCHEMA_CONSTANT_INVALID/cases.json +22 -0
  15. package/fixtures/invalid/build-input/SCHEMA_ENUM_INVALID/cases.json +22 -0
  16. package/fixtures/invalid/build-input/SCHEMA_REQUIRED_FIELD_MISSING/cases.json +171 -39
  17. package/fixtures/invalid/build-input/SCHEMA_TYPE_INVALID/cases.json +122 -11
  18. package/fixtures/invalid/build-input/SCHEMA_UNION_INVALID/cases.json +9 -0
  19. package/fixtures/invalid/interactions-config/REQUEST_FIELD_UNKNOWN/cases.json +11 -0
  20. package/fixtures/invalid/interactions-config/SCHEMA_REQUIRED_FIELD_MISSING/cases.json +11 -0
  21. package/fixtures/invalid/interactions-config/SCHEMA_TYPE_INVALID/cases.json +20 -0
  22. package/fixtures/invalid/publication/GRAPHEME_LENGTH_INVALID/cases.json +22 -11
  23. package/fixtures/invalid/publication/REQUEST_FIELD_UNKNOWN/cases.json +62 -9
  24. package/fixtures/invalid/publication/SCHEMA_ARRAY_DUPLICATE/cases.json +13 -0
  25. package/fixtures/invalid/publication/SCHEMA_ARRAY_TOO_LONG/cases.json +15 -0
  26. package/fixtures/invalid/publication/SCHEMA_CONSTANT_INVALID/cases.json +22 -0
  27. package/fixtures/invalid/publication/SCHEMA_ENUM_INVALID/cases.json +22 -0
  28. package/fixtures/invalid/publication/SCHEMA_REQUIRED_FIELD_MISSING/cases.json +143 -20
  29. package/fixtures/invalid/publication/SCHEMA_TYPE_INVALID/cases.json +95 -4
  30. package/fixtures/invalid/publication/SCHEMA_UNION_INVALID/cases.json +11 -0
  31. package/fixtures/manifest.json +1827 -179
  32. package/fixtures/s2/empty-modules-placements.json +943 -0
  33. package/fixtures/s2/manifest.json +13 -1
  34. package/fixtures/s2/normalized-inputs.json +1704 -0
  35. package/fixtures/unknown-field/build-input/rules.json +118 -21
  36. package/fixtures/unknown-field/interactions-config/rules.json +14 -0
  37. package/fixtures/unknown-field/publication/rules.json +92 -9
  38. package/generated/browser/validator-core.mjs +3 -3
  39. package/generated/typescript/contracts/build-input.d.ts +46 -5
  40. package/generated/typescript/contracts/interactions-config.d.ts +3 -0
  41. package/generated/typescript/contracts/publication.d.ts +40 -5
  42. package/openapi/http-catalog.json +2 -2
  43. package/openapi/openapi.yaml +9 -0
  44. package/package.json +1 -1
  45. package/sbom.cdx.json +23 -23
  46. package/schemas/build-input.schema.json +91 -4
  47. package/schemas/interactions-config.schema.json +7 -0
  48. package/schemas/publication.schema.json +85 -7
@@ -102,6 +102,15 @@ export type BuildInputColorMode = Readonly<{
102
102
  }> &
103
103
  unknown;
104
104
 
105
+ export type BuildInputContactFormNormalized = Readonly<{
106
+ readonly enabled: boolean;
107
+ readonly fields: Readonly<{
108
+ readonly phone: boolean;
109
+ readonly website: boolean;
110
+ }>;
111
+ readonly intro?: BuildInputRenderableBody;
112
+ }>;
113
+
105
114
  export type BuildInputContentBuildMediaFile = Readonly<{
106
115
  readonly byteLength: number;
107
116
  readonly mediaType:
@@ -200,6 +209,9 @@ export type BuildInputInteractionsConfig = Readonly<{
200
209
  readonly enabled: boolean;
201
210
  readonly maxDepth: number;
202
211
  }>;
212
+ readonly follows?: Readonly<{
213
+ readonly enabled: boolean;
214
+ }>;
203
215
  readonly publicCounts: Readonly<{
204
216
  readonly comments: boolean;
205
217
  readonly reactions: boolean;
@@ -263,6 +275,21 @@ export type BuildInputNavigationNormalized = Readonly<{
263
275
  readonly source: BuildInputNormalizedSource;
264
276
  }>;
265
277
 
278
+ export type BuildInputNewsletter =
279
+ | Readonly<{
280
+ readonly emailContent: 'card' | 'full';
281
+ readonly mode: 'galascribe';
282
+ readonly replyToOwner: boolean;
283
+ readonly text: BuildInputPlainText;
284
+ readonly title: BuildInputPlainLabel;
285
+ }>
286
+ | Readonly<{
287
+ readonly mode?: 'link';
288
+ readonly text: BuildInputPlainText;
289
+ readonly title: BuildInputPlainLabel;
290
+ readonly url: BuildInputUrlHttps;
291
+ }>;
292
+
266
293
  export type BuildInputNormalizedSource =
267
294
  | Readonly<{
268
295
  readonly defaultDigest: BuildInputDigest;
@@ -308,6 +335,7 @@ export type BuildInputPublicationNormalized = Readonly<{
308
335
  readonly authorIds: ReadonlyArray<BuildInputStableId>;
309
336
  readonly canonicalBase: BuildInputUrlHttps;
310
337
  readonly contactAuthorId?: BuildInputStableId;
338
+ readonly contactForm?: BuildInputContactFormNormalized;
311
339
  readonly crawlers?: Readonly<{
312
340
  readonly ai: 'allow' | 'block';
313
341
  }>;
@@ -321,15 +349,12 @@ export type BuildInputPublicationNormalized = Readonly<{
321
349
  readonly heading: BuildInputPlainText;
322
350
  }>;
323
351
  readonly id: BuildInputStableId;
324
- readonly newsletter?: Readonly<{
325
- readonly text: BuildInputPlainText;
326
- readonly title: BuildInputPlainLabel;
327
- readonly url: BuildInputUrlHttps;
328
- }>;
352
+ readonly newsletter?: BuildInputNewsletter;
329
353
  readonly profile?: Readonly<{
330
354
  readonly body: BuildInputRenderableBody;
331
355
  readonly route: BuildInputCanonicalRoute;
332
356
  }>;
357
+ readonly sharing?: BuildInputSharing;
333
358
  readonly slug: BuildInputSlug;
334
359
  readonly socialLinks: ReadonlyArray<BuildInputSocialLink>;
335
360
  readonly sourceDigest: BuildInputDigest;
@@ -419,6 +444,22 @@ export type BuildInputSemver = string;
419
444
 
420
445
  export type BuildInputSemverRange = string;
421
446
 
447
+ export type BuildInputSharing = Readonly<{
448
+ readonly targets: ReadonlyArray<
449
+ | 'copy-link'
450
+ | 'native'
451
+ | 'x'
452
+ | 'bluesky'
453
+ | 'linkedin'
454
+ | 'facebook'
455
+ | 'hacker-news'
456
+ | 'whatsapp'
457
+ | 'telegram'
458
+ | 'reddit'
459
+ | 'email'
460
+ >;
461
+ }>;
462
+
422
463
  export type BuildInputSlug = string;
423
464
 
424
465
  export type BuildInputSocialLink = Readonly<{
@@ -78,6 +78,9 @@ export type InteractionsConfigDocument = Readonly<{
78
78
  readonly enabled: boolean;
79
79
  readonly maxDepth: number;
80
80
  }>;
81
+ readonly follows?: Readonly<{
82
+ readonly enabled: boolean;
83
+ }>;
81
84
  readonly publicCounts: Readonly<{
82
85
  readonly comments: boolean;
83
86
  readonly reactions: boolean;
@@ -5,6 +5,14 @@ export type PublicationBcp47 = string;
5
5
 
6
6
  export type PublicationCanonicalRoute = string;
7
7
 
8
+ export type PublicationContactForm = Readonly<{
9
+ readonly enabled: boolean;
10
+ readonly fields: Readonly<{
11
+ readonly phone: boolean;
12
+ readonly website: boolean;
13
+ }>;
14
+ }>;
15
+
8
16
  export type PublicationCrawlers = Readonly<{
9
17
  readonly ai: 'allow' | 'block';
10
18
  }>;
@@ -28,11 +36,20 @@ export type PublicationGlob = string;
28
36
 
29
37
  export type PublicationIsoCountry = string;
30
38
 
31
- export type PublicationNewsletter = Readonly<{
32
- readonly text: PublicationPlainText;
33
- readonly title: PublicationPlainLabel;
34
- readonly url: PublicationUrlHttps;
35
- }>;
39
+ export type PublicationNewsletter =
40
+ | Readonly<{
41
+ readonly emailContent: 'card' | 'full';
42
+ readonly mode: 'galascribe';
43
+ readonly replyToOwner: boolean;
44
+ readonly text: PublicationPlainText;
45
+ readonly title: PublicationPlainLabel;
46
+ }>
47
+ | Readonly<{
48
+ readonly mode?: 'link';
49
+ readonly text: PublicationPlainText;
50
+ readonly title: PublicationPlainLabel;
51
+ readonly url: PublicationUrlHttps;
52
+ }>;
36
53
 
37
54
  export type PublicationPackageExact = string;
38
55
 
@@ -67,6 +84,22 @@ export type PublicationSemver = string;
67
84
 
68
85
  export type PublicationSemverRange = string;
69
86
 
87
+ export type PublicationSharing = Readonly<{
88
+ readonly targets: ReadonlyArray<
89
+ | 'copy-link'
90
+ | 'native'
91
+ | 'x'
92
+ | 'bluesky'
93
+ | 'linkedin'
94
+ | 'facebook'
95
+ | 'hacker-news'
96
+ | 'whatsapp'
97
+ | 'telegram'
98
+ | 'reddit'
99
+ | 'email'
100
+ >;
101
+ }>;
102
+
70
103
  export type PublicationSlug = string;
71
104
 
72
105
  export type PublicationSocialLink = Readonly<{
@@ -94,6 +127,7 @@ export type PublicationUrn = string;
94
127
  export type PublicationDocument = Readonly<{
95
128
  readonly authors: ReadonlyArray<PublicationRepoRelativePath>;
96
129
  readonly canonicalBase: PublicationUrlHttps;
130
+ readonly contactForm?: PublicationContactForm;
97
131
  readonly contactRef?: PublicationRepoRelativePath;
98
132
  readonly crawlers?: PublicationCrawlers;
99
133
  readonly defaultImageRef?: PublicationRepoRelativePath;
@@ -107,6 +141,7 @@ export type PublicationDocument = Readonly<{
107
141
  readonly profile?: PublicationPublicationProfile;
108
142
  readonly schemaId: 'urn:gala:schema:publication:2.0.0';
109
143
  readonly schemaVersion: '2.0.0';
144
+ readonly sharing?: PublicationSharing;
110
145
  readonly slug: PublicationSlug;
111
146
  readonly socialLinks: ReadonlyArray<PublicationSocialLink>;
112
147
  readonly title: PublicationPlainText;
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "schemaVersion": "2.0.0",
3
3
  "sourceDesignRevision": "a53052955ceb6c1aa10c886289dc832eedfe6de0a00004df07d353ac19c4403a",
4
- "openApiDigest": "sha256:f015c933c536a3a0030eeb06986d014debd8c929b968c82df9817976492fd6d3",
4
+ "openApiDigest": "sha256:42639c6b587144df5a524606fca2ac95528f48039f6785332855fb0d7487fa63",
5
5
  "operations": [
6
6
  {
7
7
  "operationId": "getInternalHealth",
@@ -2699,5 +2699,5 @@
2699
2699
  "sourceFragment": "workloads"
2700
2700
  }
2701
2701
  ],
2702
- "digest": "sha256:02efa2a51a27ec6907da974e1d7b4b6baaf18efe8783dcdf8f432c8e91f2efea"
2702
+ "digest": "sha256:cc839a8b4a5da1e604fcff157bffb588c7bda0e557bcd78b319e80bf86dfa22f"
2703
2703
  }
@@ -13680,6 +13680,15 @@ components:
13680
13680
  - allowReplies
13681
13681
  - maxDepth
13682
13682
  type: object
13683
+ follows:
13684
+ additionalProperties: false
13685
+ description: "SCHEMA-3.22.0: reader follows. `enabled: true` shows the Follow control, with which a signed-in reader follows an author of the site; an absent `follows` member means off. The user template seeds it on for new sites."
13686
+ properties:
13687
+ enabled:
13688
+ type: boolean
13689
+ required:
13690
+ - enabled
13691
+ type: object
13683
13692
  publicCounts:
13684
13693
  additionalProperties: false
13685
13694
  properties:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rathnasgala2/schemas",
3
- "version": "3.21.0",
3
+ "version": "3.22.0",
4
4
  "description": "Executable JSON Schema and OpenAPI contracts for Galascribe",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
package/sbom.cdx.json CHANGED
@@ -97,8 +97,8 @@
97
97
  "type": "library",
98
98
  "name": "schemas",
99
99
  "group": "@rathnasgala2",
100
- "version": "3.21.0",
101
- "bom-ref": "@rathnasgala2/schemas@3.21.0",
100
+ "version": "3.22.0",
101
+ "bom-ref": "@rathnasgala2/schemas@3.22.0",
102
102
  "description": "Executable JSON Schema and OpenAPI contracts for Galascribe",
103
103
  "licenses": [
104
104
  {
@@ -108,7 +108,7 @@
108
108
  }
109
109
  }
110
110
  ],
111
- "purl": "pkg:npm/%40rathnasgala2/schemas@3.21.0?vcs_url=git%2Bhttps%3A%2F%2Fgithub.com%2Frathnasgala2%2Fschema.git",
111
+ "purl": "pkg:npm/%40rathnasgala2/schemas@3.22.0?vcs_url=git%2Bhttps%3A%2F%2Fgithub.com%2Frathnasgala2%2Fschema.git",
112
112
  "externalReferences": [
113
113
  {
114
114
  "url": "https://github.com/rathnasgala2/schema/issues",
@@ -153,7 +153,7 @@
153
153
  "type": "library",
154
154
  "name": "ajv-formats",
155
155
  "version": "3.0.1",
156
- "bom-ref": "@rathnasgala2/schemas@3.21.0|ajv-formats@3.0.1",
156
+ "bom-ref": "@rathnasgala2/schemas@3.22.0|ajv-formats@3.0.1",
157
157
  "licenses": [
158
158
  {
159
159
  "license": {
@@ -187,7 +187,7 @@
187
187
  "type": "library",
188
188
  "name": "ajv",
189
189
  "version": "8.20.0",
190
- "bom-ref": "@rathnasgala2/schemas@3.21.0|ajv@8.20.0",
190
+ "bom-ref": "@rathnasgala2/schemas@3.22.0|ajv@8.20.0",
191
191
  "licenses": [
192
192
  {
193
193
  "license": {
@@ -221,7 +221,7 @@
221
221
  "type": "library",
222
222
  "name": "fast-deep-equal",
223
223
  "version": "3.1.3",
224
- "bom-ref": "@rathnasgala2/schemas@3.21.0|fast-deep-equal@3.1.3",
224
+ "bom-ref": "@rathnasgala2/schemas@3.22.0|fast-deep-equal@3.1.3",
225
225
  "licenses": [
226
226
  {
227
227
  "license": {
@@ -255,7 +255,7 @@
255
255
  "type": "library",
256
256
  "name": "fast-uri",
257
257
  "version": "3.1.8",
258
- "bom-ref": "@rathnasgala2/schemas@3.21.0|fast-uri@3.1.8",
258
+ "bom-ref": "@rathnasgala2/schemas@3.22.0|fast-uri@3.1.8",
259
259
  "licenses": [
260
260
  {
261
261
  "license": {
@@ -289,7 +289,7 @@
289
289
  "type": "library",
290
290
  "name": "json-schema-traverse",
291
291
  "version": "1.0.0",
292
- "bom-ref": "@rathnasgala2/schemas@3.21.0|json-schema-traverse@1.0.0",
292
+ "bom-ref": "@rathnasgala2/schemas@3.22.0|json-schema-traverse@1.0.0",
293
293
  "licenses": [
294
294
  {
295
295
  "license": {
@@ -323,7 +323,7 @@
323
323
  "type": "library",
324
324
  "name": "require-from-string",
325
325
  "version": "2.0.2",
326
- "bom-ref": "@rathnasgala2/schemas@3.21.0|require-from-string@2.0.2",
326
+ "bom-ref": "@rathnasgala2/schemas@3.22.0|require-from-string@2.0.2",
327
327
  "licenses": [
328
328
  {
329
329
  "license": {
@@ -1064,10 +1064,10 @@
1064
1064
  ],
1065
1065
  "dependencies": [
1066
1066
  {
1067
- "ref": "@rathnasgala2/schemas@3.21.0",
1067
+ "ref": "@rathnasgala2/schemas@3.22.0",
1068
1068
  "dependsOn": [
1069
- "@rathnasgala2/schemas@3.21.0|ajv-formats@3.0.1",
1070
- "@rathnasgala2/schemas@3.21.0|ajv@8.20.0",
1069
+ "@rathnasgala2/schemas@3.22.0|ajv-formats@3.0.1",
1070
+ "@rathnasgala2/schemas@3.22.0|ajv@8.20.0",
1071
1071
  "pkg:npm/semver@7.8.5",
1072
1072
  "urn:gala:source-data:iana-ipv4-special-registry",
1073
1073
  "urn:gala:source-data:iana-ipv6-special-registry",
@@ -1089,31 +1089,31 @@
1089
1089
  ]
1090
1090
  },
1091
1091
  {
1092
- "ref": "@rathnasgala2/schemas@3.21.0|ajv-formats@3.0.1",
1092
+ "ref": "@rathnasgala2/schemas@3.22.0|ajv-formats@3.0.1",
1093
1093
  "dependsOn": [
1094
- "@rathnasgala2/schemas@3.21.0|ajv@8.20.0"
1094
+ "@rathnasgala2/schemas@3.22.0|ajv@8.20.0"
1095
1095
  ]
1096
1096
  },
1097
1097
  {
1098
- "ref": "@rathnasgala2/schemas@3.21.0|ajv@8.20.0",
1098
+ "ref": "@rathnasgala2/schemas@3.22.0|ajv@8.20.0",
1099
1099
  "dependsOn": [
1100
- "@rathnasgala2/schemas@3.21.0|fast-deep-equal@3.1.3",
1101
- "@rathnasgala2/schemas@3.21.0|fast-uri@3.1.8",
1102
- "@rathnasgala2/schemas@3.21.0|json-schema-traverse@1.0.0",
1103
- "@rathnasgala2/schemas@3.21.0|require-from-string@2.0.2"
1100
+ "@rathnasgala2/schemas@3.22.0|fast-deep-equal@3.1.3",
1101
+ "@rathnasgala2/schemas@3.22.0|fast-uri@3.1.8",
1102
+ "@rathnasgala2/schemas@3.22.0|json-schema-traverse@1.0.0",
1103
+ "@rathnasgala2/schemas@3.22.0|require-from-string@2.0.2"
1104
1104
  ]
1105
1105
  },
1106
1106
  {
1107
- "ref": "@rathnasgala2/schemas@3.21.0|fast-deep-equal@3.1.3"
1107
+ "ref": "@rathnasgala2/schemas@3.22.0|fast-deep-equal@3.1.3"
1108
1108
  },
1109
1109
  {
1110
- "ref": "@rathnasgala2/schemas@3.21.0|fast-uri@3.1.8"
1110
+ "ref": "@rathnasgala2/schemas@3.22.0|fast-uri@3.1.8"
1111
1111
  },
1112
1112
  {
1113
- "ref": "@rathnasgala2/schemas@3.21.0|json-schema-traverse@1.0.0"
1113
+ "ref": "@rathnasgala2/schemas@3.22.0|json-schema-traverse@1.0.0"
1114
1114
  },
1115
1115
  {
1116
- "ref": "@rathnasgala2/schemas@3.21.0|require-from-string@2.0.2"
1116
+ "ref": "@rathnasgala2/schemas@3.22.0|require-from-string@2.0.2"
1117
1117
  },
1118
1118
  {
1119
1119
  "ref": "pkg:npm/semver@7.8.5"
@@ -514,10 +514,22 @@
514
514
  "properties": { "ai": { "enum": ["allow", "block"] } },
515
515
  "additionalProperties": false
516
516
  },
517
- "newsletter": {
517
+ "newsletter": { "$ref": "#/$defs/newsletter" },
518
+ "sharing": { "$ref": "#/$defs/sharing" },
519
+ "contactForm": { "$ref": "#/$defs/contactFormNormalized" },
520
+ "sourcePath": { "$ref": "#/$defs/repoRelativePath" },
521
+ "sourceDigest": { "$ref": "#/$defs/digest" }
522
+ },
523
+ "additionalProperties": false
524
+ },
525
+ "newsletter": {
526
+ "description": "SCHEMA-3.22.0: the newsletter panel. Link mode (`mode` absent or `link`) sends readers to the author's own newsletter at `url`; Galascribe mode (`mode: \"galascribe\"`) collects and mails subscribers itself. An absent `newsletter` member means the panel is off.",
527
+ "oneOf": [
528
+ {
518
529
  "type": "object",
519
530
  "required": ["url", "title", "text"],
520
531
  "properties": {
532
+ "mode": { "const": "link" },
521
533
  "url": { "$ref": "#/$defs/urlHttps" },
522
534
  "title": { "$ref": "#/$defs/plainLabel" },
523
535
  "text": {
@@ -525,12 +537,80 @@
525
537
  "x-gala-graphemeLength": { "minimum": 1, "maximum": 300 }
526
538
  }
527
539
  },
540
+ "additionalProperties": false,
541
+ "description": "Link mode: the panel links to an external newsletter. `mode` is optional and `link` when present, so a 3.21 publication still validates."
542
+ },
543
+ {
544
+ "type": "object",
545
+ "required": ["mode", "title", "text", "emailContent", "replyToOwner"],
546
+ "properties": {
547
+ "mode": { "const": "galascribe" },
548
+ "title": { "$ref": "#/$defs/plainLabel" },
549
+ "text": {
550
+ "$ref": "#/$defs/plainText",
551
+ "x-gala-graphemeLength": { "minimum": 1, "maximum": 300 }
552
+ },
553
+ "emailContent": {
554
+ "enum": ["card", "full"],
555
+ "description": "`card` mails the post title, description, image and a Read the post button; the template renders no email fragment. `full` mails the whole post: for each published post the template renders an email-safe HTML fragment at `/_gala/email/<content id>.html` (`<content id>` is the post's front-matter `id`; absolute URLs and inline-safe markup only; `noindex`; absent from the sitemap, the feeds, the search index and the llms files), which the API fetches after the generation is certified live."
556
+ },
557
+ "replyToOwner": {
558
+ "type": "boolean",
559
+ "description": "When true, subscribers' replies go to the site owner's contact email; when false the emails carry no Reply-To."
560
+ }
561
+ },
562
+ "additionalProperties": false,
563
+ "description": "Galascribe mode: the panel is an email field and a Subscribe button. `title` and `text` are the panel heading and sentence; there is no `url`. The API refuses to turn this mode on until the site owner has a postal address in their Account."
564
+ }
565
+ ]
566
+ },
567
+ "sharing": {
568
+ "type": "object",
569
+ "required": ["targets"],
570
+ "properties": {
571
+ "targets": {
572
+ "type": "array",
573
+ "items": {
574
+ "enum": [
575
+ "copy-link",
576
+ "native",
577
+ "x",
578
+ "bluesky",
579
+ "linkedin",
580
+ "facebook",
581
+ "hacker-news",
582
+ "whatsapp",
583
+ "telegram",
584
+ "reddit",
585
+ "email"
586
+ ]
587
+ },
588
+ "minItems": 0,
589
+ "maxItems": 11,
590
+ "uniqueItems": true
591
+ }
592
+ },
593
+ "additionalProperties": false,
594
+ "description": "SCHEMA-3.22.0: the share buttons shown on each article. `targets` is the display order: 0 to 11 unique members of the closed target set. An absent `sharing` member means `[\"copy-link\"]` (the Copy link button only); an empty `targets` array means no share buttons. `native` is shown only where the browser offers the system share sheet; every other target is a plain link to that service's documented share URL. Bookmark is not a share target and is unaffected."
595
+ },
596
+ "contactFormNormalized": {
597
+ "type": "object",
598
+ "required": ["enabled", "fields"],
599
+ "properties": {
600
+ "enabled": { "type": "boolean" },
601
+ "fields": {
602
+ "type": "object",
603
+ "required": ["website", "phone"],
604
+ "properties": {
605
+ "website": { "type": "boolean" },
606
+ "phone": { "type": "boolean" }
607
+ },
528
608
  "additionalProperties": false
529
609
  },
530
- "sourcePath": { "$ref": "#/$defs/repoRelativePath" },
531
- "sourceDigest": { "$ref": "#/$defs/digest" }
610
+ "intro": { "$ref": "#/$defs/renderableBody" }
532
611
  },
533
- "additionalProperties": false
612
+ "additionalProperties": false,
613
+ "description": "SCHEMA-3.22.0: the normalized contact form. `intro` is the rendered body of the optional `gala/theme-pages/contact.md`, absent when the file is absent (the template then shows its default intro). The template renders the `/contact/` page only when `enabled` is true."
534
614
  },
535
615
  "policyPageNormalized": {
536
616
  "type": "object",
@@ -1127,6 +1207,13 @@
1127
1207
  "comments": { "type": "boolean" }
1128
1208
  },
1129
1209
  "additionalProperties": false
1210
+ },
1211
+ "follows": {
1212
+ "type": "object",
1213
+ "required": ["enabled"],
1214
+ "properties": { "enabled": { "type": "boolean" } },
1215
+ "additionalProperties": false,
1216
+ "description": "SCHEMA-3.22.0: reader follows. `enabled: true` shows the Follow control, with which a signed-in reader follows an author of the site; an absent `follows` member means off. The user template seeds it on for new sites."
1130
1217
  }
1131
1218
  },
1132
1219
  "additionalProperties": false,
@@ -45,6 +45,13 @@
45
45
  "comments": { "type": "boolean" }
46
46
  },
47
47
  "additionalProperties": false
48
+ },
49
+ "follows": {
50
+ "type": "object",
51
+ "required": ["enabled"],
52
+ "properties": { "enabled": { "type": "boolean" } },
53
+ "additionalProperties": false,
54
+ "description": "SCHEMA-3.22.0: reader follows. `enabled: true` shows the Follow control, with which a signed-in reader follows an author of the site; an absent `follows` member means off. The user template seeds it on for new sites."
48
55
  }
49
56
  },
50
57
  "additionalProperties": false,
@@ -49,6 +49,8 @@
49
49
  "policies": { "$ref": "#/$defs/publicationPolicies" },
50
50
  "footerCard": { "$ref": "#/$defs/footerCard" },
51
51
  "newsletter": { "$ref": "#/$defs/newsletter" },
52
+ "sharing": { "$ref": "#/$defs/sharing" },
53
+ "contactForm": { "$ref": "#/$defs/contactForm" },
52
54
  "crawlers": { "$ref": "#/$defs/crawlers" },
53
55
  "extensions": { "$ref": "#/$defs/extensionMap" }
54
56
  },
@@ -262,17 +264,93 @@
262
264
  "additionalProperties": false
263
265
  },
264
266
  "newsletter": {
267
+ "description": "SCHEMA-3.22.0: the newsletter panel. Link mode (`mode` absent or `link`) sends readers to the author's own newsletter at `url`; Galascribe mode (`mode: \"galascribe\"`) collects and mails subscribers itself. An absent `newsletter` member means the panel is off.",
268
+ "oneOf": [
269
+ {
270
+ "type": "object",
271
+ "required": ["url", "title", "text"],
272
+ "properties": {
273
+ "mode": { "const": "link" },
274
+ "url": { "$ref": "#/$defs/urlHttps" },
275
+ "title": { "$ref": "#/$defs/plainLabel" },
276
+ "text": {
277
+ "$ref": "#/$defs/plainText",
278
+ "x-gala-graphemeLength": { "minimum": 1, "maximum": 300 }
279
+ }
280
+ },
281
+ "additionalProperties": false,
282
+ "description": "Link mode: the panel links to an external newsletter. `mode` is optional and `link` when present, so a 3.21 publication still validates."
283
+ },
284
+ {
285
+ "type": "object",
286
+ "required": ["mode", "title", "text", "emailContent", "replyToOwner"],
287
+ "properties": {
288
+ "mode": { "const": "galascribe" },
289
+ "title": { "$ref": "#/$defs/plainLabel" },
290
+ "text": {
291
+ "$ref": "#/$defs/plainText",
292
+ "x-gala-graphemeLength": { "minimum": 1, "maximum": 300 }
293
+ },
294
+ "emailContent": {
295
+ "enum": ["card", "full"],
296
+ "description": "`card` mails the post title, description, image and a Read the post button; the template renders no email fragment. `full` mails the whole post: for each published post the template renders an email-safe HTML fragment at `/_gala/email/<content id>.html` (`<content id>` is the post's front-matter `id`; absolute URLs and inline-safe markup only; `noindex`; absent from the sitemap, the feeds, the search index and the llms files), which the API fetches after the generation is certified live."
297
+ },
298
+ "replyToOwner": {
299
+ "type": "boolean",
300
+ "description": "When true, subscribers' replies go to the site owner's contact email; when false the emails carry no Reply-To."
301
+ }
302
+ },
303
+ "additionalProperties": false,
304
+ "description": "Galascribe mode: the panel is an email field and a Subscribe button. `title` and `text` are the panel heading and sentence; there is no `url`. The API refuses to turn this mode on until the site owner has a postal address in their Account."
305
+ }
306
+ ]
307
+ },
308
+ "sharing": {
265
309
  "type": "object",
266
- "required": ["url", "title", "text"],
310
+ "required": ["targets"],
267
311
  "properties": {
268
- "url": { "$ref": "#/$defs/urlHttps" },
269
- "title": { "$ref": "#/$defs/plainLabel" },
270
- "text": {
271
- "$ref": "#/$defs/plainText",
272
- "x-gala-graphemeLength": { "minimum": 1, "maximum": 300 }
312
+ "targets": {
313
+ "type": "array",
314
+ "items": {
315
+ "enum": [
316
+ "copy-link",
317
+ "native",
318
+ "x",
319
+ "bluesky",
320
+ "linkedin",
321
+ "facebook",
322
+ "hacker-news",
323
+ "whatsapp",
324
+ "telegram",
325
+ "reddit",
326
+ "email"
327
+ ]
328
+ },
329
+ "minItems": 0,
330
+ "maxItems": 11,
331
+ "uniqueItems": true
273
332
  }
274
333
  },
275
- "additionalProperties": false
334
+ "additionalProperties": false,
335
+ "description": "SCHEMA-3.22.0: the share buttons shown on each article. `targets` is the display order: 0 to 11 unique members of the closed target set. An absent `sharing` member means `[\"copy-link\"]` (the Copy link button only); an empty `targets` array means no share buttons. `native` is shown only where the browser offers the system share sheet; every other target is a plain link to that service's documented share URL. Bookmark is not a share target and is unaffected."
336
+ },
337
+ "contactForm": {
338
+ "type": "object",
339
+ "required": ["enabled", "fields"],
340
+ "properties": {
341
+ "enabled": { "type": "boolean" },
342
+ "fields": {
343
+ "type": "object",
344
+ "required": ["website", "phone"],
345
+ "properties": {
346
+ "website": { "type": "boolean" },
347
+ "phone": { "type": "boolean" }
348
+ },
349
+ "additionalProperties": false
350
+ }
351
+ },
352
+ "additionalProperties": false,
353
+ "description": "SCHEMA-3.22.0: the site's contact form. `enabled` turns the form on, and the template renders the `/contact/` page only when it is true. `fields.website` and `fields.phone` add the optional Website and Phone inputs; name, email and message are always present. The intro text is not a member: it is the optional document `gala/theme-pages/contact.md`, and the template shows a default intro when it is absent. An absent `contactForm` means no form. The API, not the schema, limits the form to the Pro and Business plans."
276
354
  },
277
355
  "crawlers": {
278
356
  "type": "object",