@taprootio/site-authoring 0.26.0 → 0.27.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@taprootio/site-authoring",
3
- "version": "0.26.0",
3
+ "version": "0.27.1",
4
4
  "description": "Taproot site authoring CLI: sign in once in the browser, then pull, validate, push, preview, approve, and deploy a site from the terminal",
5
5
  "type": "module",
6
6
  "license": "ISC",
@@ -1,5 +1,30 @@
1
1
  {
2
2
  "entries": [
3
+ {
4
+ "version": "0.27.1",
5
+ "note": "The site checks refuse a page or redirect source named like a search-engine key file, which the platform now publishes at the site root.",
6
+ "behavior": [
7
+ "A redirect source whose first segment is 32 lowercase hexadecimal characters followed by .txt (any capitals, with or without a path beneath it) is refused with redirect.source_generated_file, like sitemap.xml and robots.txt. Taproot writes a file with that name at the root of a site that tells search engines about its releases, so nothing else can sit there. No command changed.",
8
+ "The help reference version is 45."
9
+ ],
10
+ "help": [
11
+ "redirects"
12
+ ]
13
+ },
14
+ {
15
+ "version": "0.27.0",
16
+ "note": "Embedded forms inherit configurable background, border and shadow defaults from the site, with per-form overrides.",
17
+ "behavior": [
18
+ "Form definitions accept sparse presentation.background, presentation.border and presentation.shadow boolean overrides. Omitted values inherit the site; explicit false disables the decoration. An empty presentation object is omitted by the form editor.",
19
+ "Site publishing preferences expose formBoxBackground, formBoxBorder and formBoxShadow. Background and shadow default on; border defaults off. Stage publishing preferences and the form version to apply appearance changes.",
20
+ "Forms pull and push preserve appearance overrides, including explicit false.",
21
+ "The help reference version is 44."
22
+ ],
23
+ "help": [
24
+ "forms",
25
+ "settings"
26
+ ]
27
+ },
3
28
  {
4
29
  "version": "0.26.0",
5
30
  "note": "marketing pull and marketing push keep a site's campaigns and placements as files, and link build prints a tracked address, so an agent can run a site's attribution over the CLI.",
@@ -22,6 +22,17 @@
22
22
  "type": "string",
23
23
  "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"
24
24
  },
25
+ "presentation": {
26
+ "description": "Sparse embedded-form appearance choices. Omitted properties inherit the site's staged defaults; explicit false disables a choice. Omit the object when all choices inherit.",
27
+ "type": "object",
28
+ "minProperties": 1,
29
+ "additionalProperties": false,
30
+ "properties": {
31
+ "background": { "type": "boolean" },
32
+ "border": { "type": "boolean" },
33
+ "shadow": { "type": "boolean" }
34
+ }
35
+ },
25
36
  "submit_label": {
26
37
  "description": "The text on the submit button. Omit it for Taproot's default. Must contain more than whitespace; the validator also rejects a blank label, which a schema cannot express.",
27
38
  "type": "string",
@@ -106,7 +106,7 @@ export function contactFieldId(definition) {
106
106
  const emails = definition.fields.filter((field) => field.type === "email");
107
107
  return emails.length === 1 ? emails[0].id : undefined;
108
108
  }
109
- const DEFINITION_KEYS = ["fields", "contact_field", "submit_label", "after_submit", "newsletter_audience_id"];
109
+ const DEFINITION_KEYS = ["fields", "contact_field", "submit_label", "after_submit", "newsletter_audience_id", "presentation"];
110
110
  const BASE_FIELD_KEYS = ["id", "type", "label", "hint"];
111
111
  const isPlainObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
112
112
  const isInteger = (value) => typeof value === "number" && Number.isInteger(value);
@@ -427,6 +427,22 @@ export function validateDefinition(definition) {
427
427
  }
428
428
  /** The button text and the after-submit choice, which sit beside the fields in a definition. */
429
429
  function validateFormSettings(definition, errors) {
430
+ if ("presentation" in definition) {
431
+ const presentation = definition.presentation;
432
+ if (!isPlainObject(presentation) || Object.keys(presentation).length === 0) {
433
+ errors.push({ path: "presentation", code: "invalid_presentation" });
434
+ }
435
+ else {
436
+ for (const key of sortedKeys(presentation)) {
437
+ if (key !== "background" && key !== "border" && key !== "shadow") {
438
+ errors.push({ path: `presentation.${key}`, code: "unknown_property" });
439
+ }
440
+ else if (typeof presentation[key] !== "boolean") {
441
+ errors.push({ path: `presentation.${key}`, code: "invalid_presentation_value" });
442
+ }
443
+ }
444
+ }
445
+ }
430
446
  const audience = definition.newsletter_audience_id;
431
447
  if (audience !== undefined && (typeof audience !== "string" || !isResourceId(audience))) {
432
448
  errors.push({ path: "newsletter_audience_id", code: "invalid_newsletter_audience" });
@@ -6,7 +6,7 @@ import { contactFieldId, validateDefinition } from "./field-validation.js";
6
6
  *
7
7
  * A form is one JSON file, `forms/<key>.json`, holding what an author decides:
8
8
  * its key, name, sink, retention, owner-notification preference and definition. The definition holds the fields
9
- * and the form's own settings (`contact_field`, `submit_label`, `after_submit`),
9
+ * and the form's own settings (`contact_field`, `submit_label`, `after_submit`, `presentation`),
10
10
  * exactly as the site stores it, so one validator reads one shape. The
11
11
  * definition's own rules belong to `shared/field-validation.ts` and are run
12
12
  * here through the generated copy, so this file never restates them. The one
@@ -289,6 +289,20 @@ export function projectFormForWorkspace(form, pathById) {
289
289
  if (form.definition.after_submit !== undefined) {
290
290
  definition.after_submit = canonicalAfterSubmit(form.definition.after_submit, pathById);
291
291
  }
292
+ if (form.definition.newsletter_audience_id !== undefined) {
293
+ definition.newsletter_audience_id = form.definition.newsletter_audience_id;
294
+ }
295
+ if (form.definition.presentation !== undefined) {
296
+ const presentation = {};
297
+ for (const name of ["background", "border", "shadow", ...Object.keys(form.definition.presentation).sort()]) {
298
+ if (form.definition.presentation[name] !== undefined) presentation[name] = form.definition.presentation[name];
299
+ }
300
+ definition.presentation = presentation;
301
+ }
302
+ // Preserve future properties on pull so an older workspace cannot silently erase them.
303
+ for (const name of Object.keys(form.definition).sort()) {
304
+ if (name !== "contact_field" && definition[name] === undefined) definition[name] = form.definition[name];
305
+ }
292
306
  return {
293
307
  key: form.key,
294
308
  ...projectFormSettings(form),
@@ -95,7 +95,7 @@ import {
95
95
 
96
96
  export { getAppearanceReference, getFooterReference, getSettingsReference, getThemeReference };
97
97
 
98
- export const REFERENCE_VERSION = 43;
98
+ export const REFERENCE_VERSION = 45;
99
99
 
100
100
  // The date-limit examples `help forms` prints; tests run them through the form validator.
101
101
  export const FORM_DATE_WINDOW_FIELD = Object.freeze({
@@ -786,7 +786,9 @@ const WORKFLOW_REFERENCES = Object.freeze({
786
786
  + "check does not see may belong to a release the edge has not switched to yet; deploy --staging waits for "
787
787
  + "staging to serve the new release and reports stagingServed.",
788
788
  "A source cannot name a file the site generates: '/index.html' or any page's own 'index.html', '/404.html', "
789
- + "'/sitemap.xml', '/robots.txt', '/favicon.ico', or a path under the sitemap or robots names. Nothing under "
789
+ + "'/sitemap.xml', '/robots.txt', '/favicon.ico', a path under the sitemap or robots names, or the "
790
+ + "search-engine key file Taproot publishes when a site tells search engines about its releases (32 "
791
+ + "lowercase hexadecimal characters followed by '.txt', in any capitals). Nothing under "
790
792
  + "'/taproot/<version>/' (for example '/taproot/5.0.62/manifest.json') can be a source either: the platform "
791
793
  + "serves the published-site runtime there before any redirect. The platform "
792
794
  + "already redirects every '…/index.html' address to its directory, so no entry is needed for one. A source "
@@ -883,6 +885,11 @@ const WORKFLOW_REFERENCES = Object.freeze({
883
885
  + "the visitor saw it, so a response shows what each box agreed to.",
884
886
  "Editing a definition appends a version on the site; versions are never changed. A page keeps the form "
885
887
  + "version it was published with until the next deploy, and a rollback restores the form it shipped with.",
888
+ "definition.presentation optionally overrides the site's form appearance: background, border and shadow are "
889
+ + "optional booleans. Omit a property to inherit the corresponding site preference (formBoxBackground, "
890
+ + "formBoxBorder, formBoxShadow); false turns that choice off. Omit presentation when all three inherit: an empty "
891
+ + "object is refused. Site defaults are background on, border off and shadow on. Background can be off while border "
892
+ + "and shadow remain on. The release freezes both the form choices and the site defaults.",
886
893
  "Both verbs write the file in one canonical key order, so a pulled file pushed back unchanged reports "
887
894
  + "'unchanged' and leaves the bytes as they were. Write your own files in that order and a push rewrites "
888
895
  + "nothing you did not change.",
@@ -272,6 +272,24 @@ const PREFERENCES_ENTRIES = {
272
272
  + "a deployment.",
273
273
  false,
274
274
  ),
275
+ formBoxBackground: entry(
276
+ `${FEATURES} > Form appearance > Background`,
277
+ "Whether embedded forms have a box background by default. A form's presentation.background overrides this choice.",
278
+ "Embedded forms, including validation and confirmation. A change applies after deployment.",
279
+ true,
280
+ ),
281
+ formBoxBorder: entry(
282
+ `${FEATURES} > Form appearance > Border`,
283
+ "Whether embedded forms have a border by default. A form's presentation.border overrides this choice.",
284
+ "Embedded forms. Independent of background and shadow; applies after deployment.",
285
+ false,
286
+ ),
287
+ formBoxShadow: entry(
288
+ `${FEATURES} > Form appearance > Shadow`,
289
+ "Whether embedded forms have a shadow by default. A form's presentation.shadow overrides this choice.",
290
+ "Embedded forms. Independent of background and border; applies after deployment.",
291
+ true,
292
+ ),
275
293
  tiptapImageMaxHeightVh: entry(
276
294
  `${FEATURES} > Image height`,
277
295
  "The tallest an image in a page body may be, as a percent of the screen height (45, 60, 75 or 90).",
@@ -130,6 +130,9 @@ export const SETTINGS_GROUPS = Object.freeze([
130
130
  // Asks readers before the visit cookie is set (TR01263). Off is what an omitted value means, so a snapshot of
131
131
  // an untouched site reads the same as the site behaves.
132
132
  { name: "analyticsConsentPrompt", wireType: "boolean" },
133
+ { name: "formBoxBackground", wireType: "boolean", zero: true },
134
+ { name: "formBoxBorder", wireType: "boolean" },
135
+ { name: "formBoxShadow", wireType: "boolean", zero: true },
133
136
  { name: "tiptapImageMaxHeightVh", wireType: "int" },
134
137
  { name: "tiptapImageMaxImagesPerRow", wireType: "int" },
135
138
  { name: "tiptapImageBorderWidth", wireType: "int" },
@@ -146,7 +149,7 @@ export const CANDIDATE_SELECTABLE_SETTINGS_TYPES = Object.freeze(
146
149
  );
147
150
 
148
151
  function zeroValue(field) {
149
- if (field.wireType === "boolean") return false;
152
+ if (field.wireType === "boolean") return field.zero ?? false;
150
153
  if (field.wireType === "int" || field.wireType === "double") return 0;
151
154
  if (field.wireType === "theme" || field.wireType === "document") return field.zero ?? {};
152
155
  return field.zero ?? "";
@@ -280,6 +280,8 @@ export function normalizeRedirectPath(value) {
280
280
  return trimmed === "" ? "/" : trimmed;
281
281
  }
282
282
  const GENERATED_ROOT_FILES = ["404.html", "sitemap.xml", "robots.txt", "favicon.ico"];
283
+ // The IndexNow key file (TR01304) is named by a pattern, so it cannot join the list above.
284
+ const INDEX_NOW_KEY_FILE = /^[a-f0-9]{32}\.txt$/u;
283
285
  const INTEGRATION_DATA_ROOT = "integrations";
284
286
  const MAX_MARKER_SEGMENT_BYTES = 200;
285
287
  const MAX_OUTPUT_PATH_LENGTH = 1_024;
@@ -317,8 +319,9 @@ export function redirectSourceRule(edgePath) {
317
319
  return refuse(RULE_REDIRECT_SOURCE_INTEGRATION_DATA);
318
320
  if (isRuntimeMirrorPath(decoded))
319
321
  return refuse(RULE_REDIRECT_SOURCE_RUNTIME_MIRROR);
320
- if (GENERATED_ROOT_FILES.includes(first.toLowerCase()))
322
+ if (GENERATED_ROOT_FILES.includes(first.toLowerCase()) || INDEX_NOW_KEY_FILE.test(first.toLowerCase())) {
321
323
  return refuse(RULE_REDIRECT_SOURCE_GENERATED_FILE);
324
+ }
322
325
  if (!isStorable(decoded))
323
326
  return refuse(RULE_REDIRECT_SOURCE_NOT_STORABLE);
324
327
  return undefined;