@taprootio/site-authoring 0.30.0 → 0.32.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/README.md +87 -12
- package/examples/riverbend-wellness/settings/taproot-styles.json +1 -0
- package/package.json +2 -2
- package/src/api.js +110 -3
- package/src/appearance-contract.js +11 -0
- package/src/block-flow.js +117 -0
- package/src/changelog.json +45 -0
- package/src/cli.js +102 -6
- package/src/constants.js +3 -0
- package/src/content/components.js +28 -15
- package/src/content/external-link.js +77 -0
- package/src/content/free-form-section-registry.json +30 -4
- package/src/content/free-form-sections.js +29 -1
- package/src/content/image-description-document.js +108 -0
- package/src/content/image-description-validation.js +70 -0
- package/src/content/tiptap-prosemirror.ts +85 -27
- package/src/content/validate-document.js +2 -0
- package/src/image-description-contract.js +84 -0
- package/src/navigation-readiness.js +47 -0
- package/src/output.js +8 -0
- package/src/page-metadata.js +10 -10
- package/src/page-structure-checks.js +50 -45
- package/src/presentation-reference.js +9 -0
- package/src/reference-help.js +23 -10
- package/src/setting-reference.js +9 -0
- package/src/settings-catalog.js +1 -0
- package/src/site-check-findings.js +18 -12
- package/src/site-check-rules.json +10 -6
- package/src/typed-pages.js +25 -9
- package/src/verbs/deploy.js +11 -0
- package/src/verbs/index.js +10 -0
- package/src/verbs/media-images.js +67 -0
- package/src/verbs/pages-push.js +2 -1
- package/src/verbs/status.js +19 -5
package/src/page-metadata.js
CHANGED
|
@@ -4,9 +4,9 @@ import { boundedByBytes } from "./session.js";
|
|
|
4
4
|
import { displayPagePath } from "./workspace.js";
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* is
|
|
7
|
+
* Description length advice is shared with the document rules. The separate
|
|
8
|
+
* descriptionWarnings output stays compatible; the 1000-character storage
|
|
9
|
+
* maximum is still enforced here and by the API (TR01194).
|
|
10
10
|
*/
|
|
11
11
|
export { DESCRIPTION_WARN_LENGTH };
|
|
12
12
|
export const DESCRIPTION_MAXIMUM_LENGTH = 1000;
|
|
@@ -20,27 +20,27 @@ export function requireDescriptionLength(description, field = "description") {
|
|
|
20
20
|
throw new SiteAuthoringError(
|
|
21
21
|
"pages.description_too_long",
|
|
22
22
|
`The description is ${length(description)} characters; it may be at most ${DESCRIPTION_MAXIMUM_LENGTH} `
|
|
23
|
-
+ `(
|
|
23
|
+
+ `(the writing guidance is at most ${DESCRIPTION_WARN_LENGTH}).`,
|
|
24
24
|
{ field },
|
|
25
25
|
);
|
|
26
26
|
}
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
/**
|
|
29
|
+
/** The compatible length-warning output for known author-entered descriptions. */
|
|
30
30
|
export function descriptionWarnings(pages, onProgress) {
|
|
31
31
|
const items = [];
|
|
32
32
|
for (const page of pages ?? []) {
|
|
33
|
-
const description = page.declaredDescription ?? page.description;
|
|
34
|
-
if (typeof description !== "string" || length(description) <= DESCRIPTION_WARN_LENGTH) continue;
|
|
33
|
+
const description = page.metadataDescriptionKnown === false ? undefined : page.declaredDescription ?? page.description;
|
|
34
|
+
if (typeof description !== "string" || length(description.trim()) <= DESCRIPTION_WARN_LENGTH) continue;
|
|
35
35
|
const item = {
|
|
36
36
|
code: PAGE_DESCRIPTION_LONG,
|
|
37
37
|
file: page.file,
|
|
38
38
|
path: displayPagePath(page.pagePath ?? page.path),
|
|
39
|
-
length: length(description),
|
|
39
|
+
length: length(description.trim()),
|
|
40
40
|
};
|
|
41
41
|
onProgress(
|
|
42
|
-
`Warning: ${item.file}'s description is ${item.length} characters;
|
|
43
|
-
+
|
|
42
|
+
`Warning: ${item.file}'s description is ${item.length} characters; consider shortening it `
|
|
43
|
+
+ `(guidance: at most ${DESCRIPTION_WARN_LENGTH}).`,
|
|
44
44
|
);
|
|
45
45
|
items.push(item);
|
|
46
46
|
}
|
|
@@ -26,11 +26,16 @@ export const RULE_H1_DEMOTED = "seo.h1_demoted";
|
|
|
26
26
|
export const RULE_H1_MULTIPLE = "seo.h1_multiple";
|
|
27
27
|
export const RULE_HEADING_RENUMBERED = "seo.heading_renumbered";
|
|
28
28
|
export const RULE_HEADING_BEFORE_TITLE = "seo.heading_before_title";
|
|
29
|
-
|
|
29
|
+
// Image completeness is assessed from frozen library defaults during deployment.
|
|
30
|
+
export const RULE_IMAGE_DEFAULT_MISSING = "a11y.image_default_missing";
|
|
30
31
|
export const RULE_LINK_TEXT_EMPTY = "a11y.link_text_empty";
|
|
32
|
+
export const RULE_TITLE_MISSING = "seo.title_missing";
|
|
33
|
+
export const RULE_TITLE_SHORT = "seo.title_short";
|
|
31
34
|
export const RULE_TITLE_LONG = "seo.title_long";
|
|
32
35
|
export const RULE_DESCRIPTION_MISSING = "seo.description_missing";
|
|
36
|
+
export const RULE_DESCRIPTION_SHORT = "seo.description_short";
|
|
33
37
|
export const RULE_DESCRIPTION_LONG = "seo.description_long";
|
|
38
|
+
export const RULE_DESCRIPTION_MATCHES_TITLE = "seo.description_matches_title";
|
|
34
39
|
export const RULE_SOCIAL_IMAGE_FALLBACK = "seo.social_image_fallback";
|
|
35
40
|
const RULE_ORDER = [
|
|
36
41
|
RULE_H1_MISSING,
|
|
@@ -38,17 +43,42 @@ const RULE_ORDER = [
|
|
|
38
43
|
RULE_H1_MULTIPLE,
|
|
39
44
|
RULE_HEADING_RENUMBERED,
|
|
40
45
|
RULE_HEADING_BEFORE_TITLE,
|
|
41
|
-
RULE_IMAGE_ALT_MISSING,
|
|
42
46
|
RULE_LINK_TEXT_EMPTY,
|
|
47
|
+
RULE_TITLE_MISSING,
|
|
48
|
+
RULE_TITLE_SHORT,
|
|
43
49
|
RULE_TITLE_LONG,
|
|
44
50
|
RULE_DESCRIPTION_MISSING,
|
|
51
|
+
RULE_DESCRIPTION_SHORT,
|
|
45
52
|
RULE_DESCRIPTION_LONG,
|
|
53
|
+
RULE_DESCRIPTION_MATCHES_TITLE,
|
|
46
54
|
RULE_SOCIAL_IMAGE_FALLBACK,
|
|
47
55
|
];
|
|
48
|
-
/**
|
|
49
|
-
export const
|
|
50
|
-
|
|
56
|
+
/** Product guidance for useful metadata, not search-engine limits. */
|
|
57
|
+
export const TITLE_MINIMUM_LENGTH = 15;
|
|
58
|
+
export const TITLE_WARN_LENGTH = 70;
|
|
59
|
+
export const DESCRIPTION_MINIMUM_LENGTH = 50;
|
|
51
60
|
export const DESCRIPTION_WARN_LENGTH = 160;
|
|
61
|
+
export const METADATA_RULE_IDS = [
|
|
62
|
+
RULE_TITLE_MISSING,
|
|
63
|
+
RULE_TITLE_SHORT,
|
|
64
|
+
RULE_TITLE_LONG,
|
|
65
|
+
RULE_DESCRIPTION_MISSING,
|
|
66
|
+
RULE_DESCRIPTION_SHORT,
|
|
67
|
+
RULE_DESCRIPTION_LONG,
|
|
68
|
+
RULE_DESCRIPTION_MATCHES_TITLE,
|
|
69
|
+
];
|
|
70
|
+
/** Local authoring cannot resolve a home page's site settings or generated fallbacks. */
|
|
71
|
+
export function localAuthoringMetadata(input) {
|
|
72
|
+
if (input.homePage)
|
|
73
|
+
return {};
|
|
74
|
+
if (input.generated) {
|
|
75
|
+
return {
|
|
76
|
+
title: input.generated.customTitle?.trim() ? input.generated.customTitle : undefined,
|
|
77
|
+
description: input.generated.customDescription?.trim() ? input.generated.customDescription : undefined,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
return { title: input.title, description: input.description };
|
|
81
|
+
}
|
|
52
82
|
const MAXIMUM_DEPTH = 256;
|
|
53
83
|
const MAXIMUM_SUBJECT_LENGTH = 80;
|
|
54
84
|
const characters = (value) => [...value].length;
|
|
@@ -162,16 +192,6 @@ function walkBody(body) {
|
|
|
162
192
|
case "taprootImage": {
|
|
163
193
|
imageCount++;
|
|
164
194
|
seeContent();
|
|
165
|
-
if (!nonBlank(node.attrs?.alt)) {
|
|
166
|
-
const id = node.attrs?.imageId;
|
|
167
|
-
findings.push({
|
|
168
|
-
ruleId: RULE_IMAGE_ALT_MISSING,
|
|
169
|
-
severity: "warning",
|
|
170
|
-
subject: typeof id === "string" ? id : "",
|
|
171
|
-
detail: "",
|
|
172
|
-
path,
|
|
173
|
-
});
|
|
174
|
-
}
|
|
175
195
|
return;
|
|
176
196
|
}
|
|
177
197
|
case "componentBlock": {
|
|
@@ -182,19 +202,10 @@ function walkBody(body) {
|
|
|
182
202
|
const fixedLevel = componentFixedHeadingLevel(type, data);
|
|
183
203
|
if (type === "image-banner" && imageSource(data.image)) {
|
|
184
204
|
imageCount++;
|
|
185
|
-
if (!nonBlank(data.altText)) {
|
|
186
|
-
findings.push({ ruleId: RULE_IMAGE_ALT_MISSING, severity: "warning", subject: "", detail: "", path });
|
|
187
|
-
}
|
|
188
205
|
}
|
|
189
206
|
// The social preview also draws on a hero's picture.
|
|
190
207
|
if (type === "hero-section" && imageSource(data.media)) {
|
|
191
208
|
imageCount++;
|
|
192
|
-
if (!nonBlank(data.media.alt)) {
|
|
193
|
-
findings.push({ ruleId: RULE_IMAGE_ALT_MISSING, severity: "warning", subject: "", detail: "", path });
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
if (type === "cta" && imageSource(data.mediaImage) && !nonBlank(data.mediaImage.alt)) {
|
|
197
|
-
findings.push({ ruleId: RULE_IMAGE_ALT_MISSING, severity: "warning", subject: "", detail: "", path });
|
|
198
209
|
}
|
|
199
210
|
if (opensPage) {
|
|
200
211
|
recordFixedHeading(outline, 1);
|
|
@@ -322,27 +333,21 @@ function collectFindings(page) {
|
|
|
322
333
|
findings.push(...walked.findings);
|
|
323
334
|
imageCount = walked.imageCount;
|
|
324
335
|
}
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
ruleId: RULE_DESCRIPTION_LONG,
|
|
341
|
-
severity: "warning",
|
|
342
|
-
subject: "",
|
|
343
|
-
detail: String(characters(description)),
|
|
344
|
-
});
|
|
345
|
-
}
|
|
336
|
+
const checkText = (value, minimum, maximum, missingRule, shortRule, longRule) => {
|
|
337
|
+
if (typeof value !== "string")
|
|
338
|
+
return;
|
|
339
|
+
const count = characters(value.trim());
|
|
340
|
+
const ruleId = count === 0 ? missingRule : count < minimum ? shortRule : count > maximum ? longRule : undefined;
|
|
341
|
+
if (ruleId)
|
|
342
|
+
findings.push({ ruleId, severity: "warning", subject: "", detail: count === 0 ? "" : String(count) });
|
|
343
|
+
};
|
|
344
|
+
checkText(page.title, TITLE_MINIMUM_LENGTH, TITLE_WARN_LENGTH, RULE_TITLE_MISSING, RULE_TITLE_SHORT, RULE_TITLE_LONG);
|
|
345
|
+
checkText(page.description, DESCRIPTION_MINIMUM_LENGTH, DESCRIPTION_WARN_LENGTH, RULE_DESCRIPTION_MISSING, RULE_DESCRIPTION_SHORT, RULE_DESCRIPTION_LONG);
|
|
346
|
+
const normalize = (value) => typeof value === "string" ? value.trim().replace(/\s+/gu, " ").toLowerCase() : undefined;
|
|
347
|
+
const title = normalize(page.title);
|
|
348
|
+
const description = normalize(page.description);
|
|
349
|
+
if (title && description && title === description) {
|
|
350
|
+
findings.push({ ruleId: RULE_DESCRIPTION_MATCHES_TITLE, severity: "warning", subject: "", detail: "" });
|
|
346
351
|
}
|
|
347
352
|
if (page.freeForm && page.hasCover === false) {
|
|
348
353
|
findings.push({
|
|
@@ -520,6 +520,14 @@ export function getAppearanceReference() {
|
|
|
520
520
|
+ "so full-bleed designs usually pair headerWidth 'wide' with headerLayout 'centered-menu', which keeps the "
|
|
521
521
|
+ "brand left, the buttons right, and the navigation centered. validate prints a hint, never a failure, when a "
|
|
522
522
|
+ "root-band page ships with a contained header.",
|
|
523
|
+
blockSpacingContract:
|
|
524
|
+
"Blocks on a page space themselves: text, tables and components all sit one gap apart, and the gap follows the "
|
|
525
|
+
+ "site's text size. blockSpacing scales it for the whole site in both color schemes: 'standard' keeps the gap "
|
|
526
|
+
+ "equal to the text spacing, 'compact' is three quarters of it and 'roomy' is one and a half times it. "
|
|
527
|
+
+ "A heading's top margin scales the same way. The last text block in a padded section gets the gap as extra "
|
|
528
|
+
+ "room before the section ends, and an image banner stays flush with its neighbors. A spacer block adds its "
|
|
529
|
+
+ "own height to the gap, so review spacers placed by hand. A site shows the change after its next whole-site "
|
|
530
|
+
+ "render.",
|
|
523
531
|
menuContract:
|
|
524
532
|
"navDrawerStyle 'full-screen' (the published default) opens the mobile menu over the whole viewport with the "
|
|
525
533
|
+ "brand centered above large centered items and navDrawerTransition choosing how it appears (fade, slide-down, "
|
|
@@ -841,6 +849,7 @@ export function formatPresentationReference(reference) {
|
|
|
841
849
|
+ "\nLogo contrast: " + reference.logoContrastContract
|
|
842
850
|
+ "\nFavicon: " + reference.faviconContract
|
|
843
851
|
+ "\nHeader width: " + reference.headerWidthContract
|
|
852
|
+
+ "\nBlock spacing: " + reference.blockSpacingContract
|
|
844
853
|
+ "\nMobile menu and menu font: " + reference.menuContract
|
|
845
854
|
+ "\nFooter: " + reference.footerBoundary
|
|
846
855
|
+ "\nFooter content guard: " + reference.footerContentGuard + "\n\nAtomic change set (one transaction):\n"
|
package/src/reference-help.js
CHANGED
|
@@ -95,7 +95,7 @@ import {
|
|
|
95
95
|
|
|
96
96
|
export { getAppearanceReference, getFooterReference, getSettingsReference, getThemeReference };
|
|
97
97
|
|
|
98
|
-
export const REFERENCE_VERSION =
|
|
98
|
+
export const REFERENCE_VERSION = 51;
|
|
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({
|
|
@@ -695,7 +695,8 @@ const WORKFLOW_REFERENCES = Object.freeze({
|
|
|
695
695
|
"PAGE uses resourceId from .taproot-site-manifest.json, never pageId or path.",
|
|
696
696
|
"EXTERNAL_URL uses externalUrl; GROUP_HEADER has no target; every kind may carry children.",
|
|
697
697
|
"externalUrl is an absolute credential-free http/https URL, or a mailto:/tel: contact URL such as "
|
|
698
|
-
+ "tel:+15555550123, published exactly as authored."
|
|
698
|
+
+ "tel:+15555550123, published exactly as authored. An http or https address on another host opens in a new tab, "
|
|
699
|
+
+ "and the link says so to a screen reader.",
|
|
699
700
|
]),
|
|
700
701
|
example: Object.freeze({
|
|
701
702
|
siteId: "<site-uuid>",
|
|
@@ -1018,10 +1019,14 @@ const WORKFLOW_REFERENCES = Object.freeze({
|
|
|
1018
1019
|
}),
|
|
1019
1020
|
}),
|
|
1020
1021
|
media: Object.freeze({
|
|
1021
|
-
title: "
|
|
1022
|
-
summary: "
|
|
1023
|
-
usage: `${CLI_BINARY_NAME} media upload [path...]`,
|
|
1022
|
+
title: "Image defaults and media upload",
|
|
1023
|
+
summary: "Read and change site-owned image defaults by ID, or upload workspace-root-relative raster files and videos.",
|
|
1024
|
+
usage: `${CLI_BINARY_NAME} media images list|get|describe | media upload [path...]`,
|
|
1024
1025
|
details: Object.freeze([
|
|
1026
|
+
"media images list returns one page in images.items and images.nextPageToken; pass --page-size 1–100 (default 25) and --page-token with that exact cursor for the next page. --missing-description is a server-side filter for a null image-owned default, independent of placement alt text, captions or decorative mode. No per-result reads or page-body scans run. The authoritative default-completeness findings are in the frozen deployment report that status reads; local validate does not infer them from placement strings.",
|
|
1027
|
+
"media images get <image-id> reads one library image in the selected site. Both reads expose imageId, uploadedName, defaultDescription (null when missing), descriptionRevision as a decimal string, width, height, processingState, previewUrl and responsivePreviewUrls. Preview URLs are for inspection, may expire, and are omitted from GITHUB_OUTPUT. They are not descriptions and these verbs write no workspace file.",
|
|
1028
|
+
"media images describe <image-id> --description <text> --expected-revision <n> saves the default; --clear replaces --description to remove it. Supply the exact revision from list/get, including 0. --if-missing is an atomic server condition, requires nonblank text, and refuses an existing default. Conflicts report media.image_description_conflict or media.image_description_exists with exit 1; read the current image and reconcile, never retry by overwriting it blindly. Input/option errors use exit 2.",
|
|
1029
|
+
"The server owns the 1000-Unicode-scalar limit: outer whitespace is trimmed, blank text becomes null, and oversized text is rejected without truncation. Descriptions may contain Unicode and newlines. The CLI preserves their text in JSON and never copies preview URLs or resolved defaults into page bodies. Site media management is required for all three verbs; profile/account/installation media are excluded. A default edit affects inherited usages at the next selected deployment; current published and in-flight snapshots keep their frozen text, while contextual overrides and decorative placements keep their choices.",
|
|
1025
1030
|
"Paths are relative to the configured workspace root, not the shell's current directory.",
|
|
1026
1031
|
"PNG, JPEG, GIF, and WebP are accepted. Each file uploads as its own image: a name such as logo@2x.png is only a filename, nothing pairs it with logo.png, and the platform generates the display sizes from the one file you assign (see help appearance for logos).",
|
|
1027
1032
|
"For header logos prefer genuine transparent PNG/WebP, check contrast in both themes, and include a simplified square favicon. See help appearance for branding guidance.",
|
|
@@ -1405,6 +1410,8 @@ function decorationReference() {
|
|
|
1405
1410
|
}\n## Rooted in warmth\n\nMove with confidence.\n:::`,
|
|
1406
1411
|
maskGuidance:
|
|
1407
1412
|
"Use a transparent PNG or WebP whose alpha channel contains only the mark. The active section context supplies the tint in both schemes.",
|
|
1413
|
+
sizeGuidance:
|
|
1414
|
+
"Set inlineSizePercent to size the art by the section's width, or blockSizePercent to size it by the section's height. The two cannot be combined.",
|
|
1408
1415
|
opaqueWarning:
|
|
1409
1416
|
"An opaque rectangle uses its whole alpha channel and will render as a tinted rectangle, not as a cut-out mark.",
|
|
1410
1417
|
});
|
|
@@ -1449,7 +1456,7 @@ function inlineFactsReference() {
|
|
|
1449
1456
|
+ "mix labelled and standalone facts. An empty string is rejected, so omitting a label stays explicit. "
|
|
1450
1457
|
+ "Unicode spaces, tabs, and LF/CRLF line breaks are allowed; other C0/C1 controls, U+2028/U+2029, U+FEFF, and literal <br> tags are rejected. The label follows the value in reading order and is outside its link.",
|
|
1451
1458
|
urlPolicy:
|
|
1452
|
-
"Optional safe HTTP(S), mailto, tel, non-protocol-relative root, fragment, query, or relative URL. Empty/whitespace-only values, other schemes, backslashes, and ASCII controls are rejected; tel: remains a native link. Contact URLs require a body and refuse tel:// and mailto:// authority forms or control characters, consistently with navigation and footer links.",
|
|
1459
|
+
"Optional safe HTTP(S), mailto, tel, non-protocol-relative root, fragment, query, or relative URL. Empty/whitespace-only values, other schemes, backslashes, and ASCII controls are rejected; tel: remains a native link. Contact URLs require a body and refuse tel:// and mailto:// authority forms or control characters, consistently with navigation and footer links. An absolute http(s) address on another site opens in a new tab with nothing to set; the site's own addresses and all other values keep the same tab.",
|
|
1453
1460
|
markdown: Object.freeze({
|
|
1454
1461
|
fence: INLINE_FACTS_DEFINITION.markdown.fence,
|
|
1455
1462
|
body: INLINE_FACTS_DEFINITION.markdown.body,
|
|
@@ -1815,10 +1822,10 @@ const FREE_FORM_REFERENCE = Object.freeze({
|
|
|
1815
1822
|
+ "choice is per link, internal links included.",
|
|
1816
1823
|
markdownRule:
|
|
1817
1824
|
'Write {rel="…" target="…"} directly after the link\'s closing parenthesis: either attribute alone or both, '
|
|
1818
|
-
+ 'each double-quoted once. rel is space-separated; target="_blank" opens a new tab.',
|
|
1825
|
+
+ 'each double-quoted once. rel is space-separated; an external web address opens in a new tab on its own, target="_blank" opens a new tab for any link, and target="_self" keeps a link in the same tab.',
|
|
1819
1826
|
pmJson:
|
|
1820
1827
|
'In .pm.json, a link mark carries attrs.rel (a space-separated string, or null) and attrs.target ("_blank" '
|
|
1821
|
-
+ 'opens a new tab and publishes with noopener noreferrer added, "_self"
|
|
1828
|
+
+ 'opens a new tab and publishes with noopener noreferrer added, "_self" keeps the same tab, and null leaves it to the default: an absolute http(s) link to another site opens in a new tab, and any other link keeps the same tab).',
|
|
1822
1829
|
refusals:
|
|
1823
1830
|
`Any other rel token, opener included, is refused (${CONTENT_ERROR_CODES.linkRel}); so is another target `
|
|
1824
1831
|
+ `(${CONTENT_ERROR_CODES.linkTarget}). The editor's old default, "noopener noreferrer nofollow", reads as no choice.`,
|
|
@@ -2035,9 +2042,15 @@ function formatRegistryField(field, indent = " ") {
|
|
|
2035
2042
|
: `; ${field.minScalars} through ${field.maxScalars} Unicode scalar values${
|
|
2036
2043
|
field.nonWhitespace ? "; non-whitespace" : ""
|
|
2037
2044
|
}`;
|
|
2045
|
+
// TR01329: a field with no default key is absent until an author sets it.
|
|
2046
|
+
const requirement = field.required
|
|
2047
|
+
? "required"
|
|
2048
|
+
: Object.hasOwn(field, "default")
|
|
2049
|
+
? `default ${JSON.stringify(field.default)}`
|
|
2050
|
+
: "optional; no default";
|
|
2038
2051
|
const line = `${indent}${field.name.padEnd(Math.max(1, 20 - indent.length))} ${type}${
|
|
2039
2052
|
field.nullable ? " | null" : ""
|
|
2040
|
-
}; ${
|
|
2053
|
+
}; ${requirement}${bounds}${itemBounds}${scalarBounds}`;
|
|
2041
2054
|
return [line, ...(field.fields ?? []).map((child) => formatRegistryField(child, `${indent} `))].join("\n");
|
|
2042
2055
|
}
|
|
2043
2056
|
|
|
@@ -2124,7 +2137,7 @@ export function formatReferenceResult(result) {
|
|
|
2124
2137
|
}\n delivery URLs ${page.sections.decoration.deliveryUrlPolicy}\n\nTint mapping:\n${
|
|
2125
2138
|
Object.entries(page.sections.decoration.tintTokens).map(([name, token]) => ` ${name.padEnd(20)} ${token}`)
|
|
2126
2139
|
.join("\n")
|
|
2127
|
-
}\n\nTransparent-mask example:\n${page.sections.decoration.markdownExample}\n\nMask guidance: ${page.sections.decoration.maskGuidance}\nWarning: ${page.sections.decoration.opaqueWarning}\n\nRoot defaults and placement:\n${
|
|
2140
|
+
}\n\nTransparent-mask example:\n${page.sections.decoration.markdownExample}\n\nMask guidance: ${page.sections.decoration.maskGuidance}\nSize guidance: ${page.sections.decoration.sizeGuidance}\nWarning: ${page.sections.decoration.opaqueWarning}\n\nRoot defaults and placement:\n${
|
|
2128
2141
|
page.sections.placementMatrix.map(formatPlacement).join("\n")
|
|
2129
2142
|
}\n\nSemantic tables:\n placement document root or directly inside a top-level section; wide/full-well measure\n rows exactly ${page.tables.structure.headerRows} header row, then ${page.tables.limits.dataRows.min} through ${page.tables.limits.dataRows.max} data rows\n columns ${page.tables.limits.columns.min} through ${page.tables.limits.columns.max}; delimiter and every row must match the header width\n caption optional non-empty plain text; maximum ${page.tables.limits.captionScalars.max} Unicode scalar values; syntax ${page.tables.markdown.captionSyntax}\n caption guidance ${page.tables.captionGuidance}\n cells exactly ${page.tables.structure.paragraphChildrenPerCell} paragraph; headers need non-whitespace text; data cells may be empty; maximum ${page.tables.limits.cellTextScalars.max} text scalars\n inline ${
|
|
2130
2143
|
page.tables.structure.inlineNodes.join(", ")
|
package/src/setting-reference.js
CHANGED
|
@@ -83,6 +83,15 @@ const TAPROOT_STYLES_ENTRIES = {
|
|
|
83
83
|
"The color scheme a first-time visitor sees: light, dark, or system to follow the visitor's device.",
|
|
84
84
|
"The starting scheme of every published page. A visitor who uses the theme toggle keeps their own choice.",
|
|
85
85
|
),
|
|
86
|
+
blockSpacing: entry(
|
|
87
|
+
`${BRAND} > Block spacing`,
|
|
88
|
+
"The space between blocks on every page: compact, standard or roomy. Standard keeps the gap equal to the text "
|
|
89
|
+
+ "spacing; compact scales it to three quarters and roomy to one and a half times. Spacer blocks add their "
|
|
90
|
+
+ "height on top.",
|
|
91
|
+
"Both color schemes, on every published page. A site shows the change after its next whole-site render, which "
|
|
92
|
+
+ "happens when any settings group or navigation is selected for a stage or deploy. Pages copied forward from "
|
|
93
|
+
+ "an earlier deploy keep their old spacing until then.",
|
|
94
|
+
),
|
|
86
95
|
lightLogoId: entry(
|
|
87
96
|
`${BRAND} > Logo`,
|
|
88
97
|
"The site-owned image used as the brand logo in the light scheme.",
|
package/src/settings-catalog.js
CHANGED
|
@@ -2,7 +2,7 @@ import { sanitizeDiagnostic } from "./errors.js";
|
|
|
2
2
|
import { boundedByBytes } from "./session.js";
|
|
3
3
|
import rules from "./site-check-rules.json" with { type: "json" };
|
|
4
4
|
import { isFormKey } from "./forms-contract.js";
|
|
5
|
-
import { DESCRIPTION_WARN_LENGTH, pageStructureFindings, TITLE_WARN_LENGTH } from "./page-structure-checks.js";
|
|
5
|
+
import { DESCRIPTION_MINIMUM_LENGTH, DESCRIPTION_WARN_LENGTH, localAuthoringMetadata, METADATA_RULE_IDS, pageStructureFindings, TITLE_MINIMUM_LENGTH, TITLE_WARN_LENGTH } from "./page-structure-checks.js";
|
|
6
6
|
import { missingPageLinkFindings, normalizedPathSet } from "./site-checks.js";
|
|
7
7
|
import { documentTemplate, TEMPLATE_FREE_FORM, TEMPLATE_GENERATED } from "./typed-pages.js";
|
|
8
8
|
import { displayPagePath } from "./workspace.js";
|
|
@@ -51,13 +51,15 @@ export const SITE_CHECK_WORDING = Object.freeze({
|
|
|
51
51
|
},
|
|
52
52
|
"seo.heading_before_title": (finding) =>
|
|
53
53
|
`has the heading '${finding.subject}' before the page's h1; it publishes as an h${finding.detail}`,
|
|
54
|
-
"a11y.
|
|
54
|
+
"a11y.image_default_missing": () => "has no default image description; set one with media images describe, then deploy",
|
|
55
55
|
"a11y.link_text_empty": (finding) => `has a link to ${finding.subject} with no text`,
|
|
56
|
-
"seo.
|
|
57
|
-
|
|
58
|
-
"seo.
|
|
59
|
-
"seo.
|
|
60
|
-
|
|
56
|
+
"seo.title_missing": () => "has no title; add one that identifies the page",
|
|
57
|
+
"seo.title_short": (finding) => `has a title of ${finding.detail} characters; consider a more specific title (guidance: ${TITLE_MINIMUM_LENGTH}–${TITLE_WARN_LENGTH})`,
|
|
58
|
+
"seo.title_long": (finding) => `has a title of ${finding.detail} characters; consider shortening it (guidance: ${TITLE_MINIMUM_LENGTH}–${TITLE_WARN_LENGTH})`,
|
|
59
|
+
"seo.description_missing": () => "has no description; add a short account of what the page offers",
|
|
60
|
+
"seo.description_short": (finding) => `has a description of ${finding.detail} characters; consider adding useful detail (guidance: ${DESCRIPTION_MINIMUM_LENGTH}–${DESCRIPTION_WARN_LENGTH})`,
|
|
61
|
+
"seo.description_long": (finding) => `has a description of ${finding.detail} characters; consider shortening it (guidance: ${DESCRIPTION_MINIMUM_LENGTH}–${DESCRIPTION_WARN_LENGTH})`,
|
|
62
|
+
"seo.description_matches_title": () => "has a description that repeats its title; explain what readers will find",
|
|
61
63
|
"seo.social_image_fallback": (finding) =>
|
|
62
64
|
finding.detail === "first_image"
|
|
63
65
|
? "has no cover image; link previews use the page's first image only when it is public and at least 1200 pixels wide"
|
|
@@ -91,15 +93,17 @@ const OFFLINE_DOCUMENT_RULES = new Set(
|
|
|
91
93
|
function structureFindingsFor(page) {
|
|
92
94
|
if (page.document === null || typeof page.document !== "object") return [];
|
|
93
95
|
const template = documentTemplate(page.document);
|
|
94
|
-
// A generated page's title and description come from the site, not from an author.
|
|
95
|
-
if (template === TEMPLATE_GENERATED) return [];
|
|
96
96
|
const freeForm = template === TEMPLATE_FREE_FORM;
|
|
97
97
|
return pageStructureFindings(
|
|
98
98
|
{
|
|
99
99
|
freeForm,
|
|
100
100
|
body: freeForm ? page.document : undefined,
|
|
101
|
-
|
|
102
|
-
|
|
101
|
+
...localAuthoringMetadata({
|
|
102
|
+
title: page.title,
|
|
103
|
+
description: page.metadataDescriptionKnown === false ? undefined : page.declaredDescription ?? page.description,
|
|
104
|
+
homePage: page.pagePath === "" || page.pagePath === "/",
|
|
105
|
+
generated: template === TEMPLATE_GENERATED ? page.document.data ?? {} : undefined,
|
|
106
|
+
}),
|
|
103
107
|
},
|
|
104
108
|
OFFLINE_DOCUMENT_RULES,
|
|
105
109
|
);
|
|
@@ -166,6 +170,8 @@ export function evaluateOfflineSiteChecks({
|
|
|
166
170
|
},
|
|
167
171
|
siteChecks: {
|
|
168
172
|
ranOffline: rulesRunningAt("validate"),
|
|
173
|
+
// Local values are checked above; only a deployment knows every effective fallback.
|
|
174
|
+
deferredToDeployment: [...METADATA_RULE_IDS],
|
|
169
175
|
// Rules that need the site's own records; `taproot-site status` shows what the site finds.
|
|
170
176
|
checkedBySite: SITE_CHECK_RULES.filter((rule) => !rule.runsAt.includes("validate") && rule.runsAt.includes("health"))
|
|
171
177
|
.map((rule) => rule.id),
|
|
@@ -173,7 +179,7 @@ export function evaluateOfflineSiteChecks({
|
|
|
173
179
|
checkedAtDeploy: SITE_CHECK_RULES.filter((rule) =>
|
|
174
180
|
!rule.runsAt.includes("validate") && !rule.runsAt.includes("health") && rule.runsAt.includes("deploy")
|
|
175
181
|
).map((rule) => rule.id),
|
|
176
|
-
// Rules only the page editor evaluates
|
|
182
|
+
// Rules only the page editor evaluates, such as its selected cover.
|
|
177
183
|
checkedInEditor: SITE_CHECK_RULES.filter((rule) =>
|
|
178
184
|
!rule.runsAt.includes("validate") && !rule.runsAt.includes("health") && !rule.runsAt.includes("deploy")
|
|
179
185
|
&& rule.runsAt.includes("edit")
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version":
|
|
3
|
-
"about": "The catalogue of site checks (TR01085, TR00947). Severity: 'error' blocks a deploy, 'warning' informs. Scope: 'page' rules read one page body plus cheap site lookups, 'site' rules read the site's own records, 'document' rules read only one document and its metadata, so the editor and the CLI evaluate them from shared/page-structure-checks.ts and the
|
|
2
|
+
"version": 2,
|
|
3
|
+
"about": "The catalogue of site checks (TR01085, TR00947). Severity: 'error' blocks a deploy, 'warning' informs. Scope: 'page' rules read one page body plus cheap site lookups, 'site' rules read the site's own records, 'document' rules read only one document and its metadata, so the editor and the CLI evaluate them from shared/page-structure-checks.ts and the generator compiles deployment advice from the same pure evaluator. runsAt names the run points that evaluate the rule: save, validate (the CLI, offline), deploy, health, edit (the editor, over the open document in the browser). appliesTo, when present, limits a rule to those page types. subject and detail say what a finding's two values hold. The typed registry in Taproot.Domain/SiteChecks/SiteCheckRules.cs must list the same rules; SiteCheckTests fails on a difference.",
|
|
4
4
|
"runPoints": ["save", "validate", "deploy", "health", "edit"],
|
|
5
5
|
"rules": [
|
|
6
6
|
{ "id": "link.page_missing", "severity": "warning", "scope": "page", "runsAt": ["save", "validate", "health"], "subject": "page_path", "detail": "none" },
|
|
7
7
|
{ "id": "image.missing", "severity": "warning", "scope": "page", "runsAt": ["save", "health"], "subject": "image_id", "detail": "none" },
|
|
8
|
+
{ "id": "a11y.image_default_missing", "severity": "warning", "scope": "site", "runsAt": ["deploy", "health"], "subject": "image_id", "detail": "none" },
|
|
8
9
|
{ "id": "form.none_chosen", "severity": "error", "scope": "page", "runsAt": ["save", "deploy", "health"], "subject": "none", "detail": "none" },
|
|
9
10
|
{ "id": "form.unreadable", "severity": "error", "scope": "page", "runsAt": ["save", "deploy", "health"], "subject": "form_reference_text", "detail": "none" },
|
|
10
11
|
{ "id": "form.missing", "severity": "error", "scope": "page", "runsAt": ["save", "deploy", "health"], "subject": "form_id", "detail": "none" },
|
|
@@ -24,11 +25,14 @@
|
|
|
24
25
|
{ "id": "seo.h1_multiple", "severity": "warning", "scope": "document", "appliesTo": ["free_form"], "runsAt": ["validate", "edit"], "subject": "heading_text", "detail": "h1_claimant" },
|
|
25
26
|
{ "id": "seo.heading_renumbered", "severity": "warning", "scope": "document", "appliesTo": ["free_form"], "runsAt": ["validate", "edit"], "subject": "heading_text", "detail": "authored_and_published_level" },
|
|
26
27
|
{ "id": "seo.heading_before_title", "severity": "warning", "scope": "document", "appliesTo": ["free_form"], "runsAt": ["validate", "edit"], "subject": "heading_text", "detail": "published_level" },
|
|
27
|
-
{ "id": "a11y.image_alt_missing", "severity": "warning", "scope": "document", "appliesTo": ["free_form"], "runsAt": ["validate", "edit"], "subject": "image_id", "detail": "none" },
|
|
28
28
|
{ "id": "a11y.link_text_empty", "severity": "warning", "scope": "document", "appliesTo": ["free_form"], "runsAt": ["validate", "edit"], "subject": "link_target", "detail": "none" },
|
|
29
|
-
{ "id": "seo.
|
|
30
|
-
{ "id": "seo.
|
|
31
|
-
{ "id": "seo.
|
|
29
|
+
{ "id": "seo.title_missing", "severity": "warning", "scope": "document", "runsAt": ["validate", "edit", "deploy", "health"], "subject": "none", "detail": "none" },
|
|
30
|
+
{ "id": "seo.title_short", "severity": "warning", "scope": "document", "runsAt": ["validate", "edit", "deploy", "health"], "subject": "none", "detail": "character_count" },
|
|
31
|
+
{ "id": "seo.title_long", "severity": "warning", "scope": "document", "runsAt": ["validate", "edit", "deploy", "health"], "subject": "none", "detail": "character_count" },
|
|
32
|
+
{ "id": "seo.description_missing", "severity": "warning", "scope": "document", "runsAt": ["validate", "edit", "deploy", "health"], "subject": "none", "detail": "none" },
|
|
33
|
+
{ "id": "seo.description_short", "severity": "warning", "scope": "document", "runsAt": ["validate", "edit", "deploy", "health"], "subject": "none", "detail": "character_count" },
|
|
34
|
+
{ "id": "seo.description_long", "severity": "warning", "scope": "document", "runsAt": ["validate", "edit", "deploy", "health"], "subject": "none", "detail": "character_count" },
|
|
35
|
+
{ "id": "seo.description_matches_title", "severity": "warning", "scope": "document", "runsAt": ["validate", "edit", "deploy", "health"], "subject": "none", "detail": "none" },
|
|
32
36
|
{ "id": "seo.social_image_fallback", "severity": "warning", "scope": "document", "appliesTo": ["free_form"], "runsAt": ["edit"], "subject": "none", "detail": "social_image_source" }
|
|
33
37
|
]
|
|
34
38
|
}
|
package/src/typed-pages.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { FREE_FORM_TEMPLATE_VERSION, TEMPLATE_TYPE_FREE_FORM } from "./api.js";
|
|
2
2
|
import { isVideoEmbedId } from "./content/video-embed-url.js";
|
|
3
3
|
import { SiteAuthoringError } from "./errors.js";
|
|
4
|
+
import { isImageDescriptionMode } from "./content/image-description-validation.js";
|
|
5
|
+
import { stripImageDescriptionHydration } from "./content/image-description-document.js";
|
|
4
6
|
import { requireDescriptionLength } from "./page-metadata.js";
|
|
5
7
|
|
|
6
8
|
/**
|
|
@@ -323,7 +325,7 @@ const RECIPE_KEYS = [
|
|
|
323
325
|
"cookingMethod",
|
|
324
326
|
];
|
|
325
327
|
const ALBUM_KEYS = ["introductionBody", "images", "seamless", "borderWidth", "place"];
|
|
326
|
-
const ALBUM_IMAGE_KEYS = ["imageId", "caption", "width", "height"];
|
|
328
|
+
const ALBUM_IMAGE_KEYS = ["imageId", "caption", "width", "height", "altText", "descriptionMode"];
|
|
327
329
|
const ALBUM_MAXIMUM_BORDER_WIDTH = 20;
|
|
328
330
|
const MAXIMUM_MINUTES = 100_000;
|
|
329
331
|
const MAXIMUM_SERVINGS = 10_000;
|
|
@@ -372,12 +374,17 @@ const DATA_BUILDERS = Object.freeze({
|
|
|
372
374
|
["introductionBody", raw.introductionBody],
|
|
373
375
|
[
|
|
374
376
|
"images",
|
|
375
|
-
raw.images.map((image) =>
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
377
|
+
raw.images.map((image) => {
|
|
378
|
+
const authored = stripImageDescriptionHydration(image);
|
|
379
|
+
return definedEntries([
|
|
380
|
+
["imageId", authored.imageId],
|
|
381
|
+
["caption", authored.caption ?? ""],
|
|
382
|
+
["width", authored.width ?? 0],
|
|
383
|
+
["height", authored.height ?? 0],
|
|
384
|
+
["altText", authored.altText],
|
|
385
|
+
["descriptionMode", authored.descriptionMode],
|
|
386
|
+
]);
|
|
387
|
+
}),
|
|
381
388
|
],
|
|
382
389
|
["seamless", raw.seamless],
|
|
383
390
|
["borderWidth", raw.borderWidth],
|
|
@@ -486,6 +493,14 @@ function canonicalSourceData(template, data, file) {
|
|
|
486
493
|
requireKeys(image, ALBUM_IMAGE_KEYS, file, at);
|
|
487
494
|
requireIdentifier(image.imageId, file, `${at}.imageId`);
|
|
488
495
|
if (image.caption !== undefined) requireString(image.caption, file, `${at}.caption`);
|
|
496
|
+
if (!isImageDescriptionMode(image.descriptionMode)) {
|
|
497
|
+
throw fail(
|
|
498
|
+
"pages.document_shape",
|
|
499
|
+
`'${file}' ${at}.descriptionMode must be inherit, custom or decorative.`,
|
|
500
|
+
`${file}:${at}.descriptionMode`,
|
|
501
|
+
);
|
|
502
|
+
}
|
|
503
|
+
if (image.altText !== undefined && image.altText !== null) requireString(image.altText, file, `${at}.altText`);
|
|
489
504
|
if (image.width !== undefined) requireCount(image.width, file, `${at}.width`, 100_000);
|
|
490
505
|
if (image.height !== undefined) requireCount(image.height, file, `${at}.height`, 100_000);
|
|
491
506
|
});
|
|
@@ -684,8 +699,9 @@ export function workspaceDocumentFromPage(page, listedTemplateType) {
|
|
|
684
699
|
if (name === undefined) return undefined;
|
|
685
700
|
const raw = template[PAGE_TEMPLATES[name].dataKey];
|
|
686
701
|
if (!isPlainObject(raw)) return undefined;
|
|
687
|
-
|
|
688
|
-
|
|
702
|
+
const authored = stripImageDescriptionHydration(raw);
|
|
703
|
+
if (name === TEMPLATE_FREE_FORM) return isPlainObject(authored.body) ? authored.body : undefined;
|
|
704
|
+
const data = PAGE_PROJECTIONS[name](authored);
|
|
689
705
|
if (data === undefined) return undefined;
|
|
690
706
|
// A generated page's date and cover are not part of its source: a push leaves them as the site holds them.
|
|
691
707
|
const generated = name === TEMPLATE_GENERATED || name === TEMPLATE_LEGAL;
|
package/src/verbs/deploy.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { describeNavigationReadiness, isNavigationReady, reportNavigationReadiness } from "../navigation-readiness.js";
|
|
1
2
|
import {
|
|
2
3
|
DEPLOYMENT_ENVIRONMENT_PRODUCTION,
|
|
3
4
|
DEPLOYMENT_ENVIRONMENT_STAGING,
|
|
@@ -154,6 +155,7 @@ function reportReadiness(readiness) {
|
|
|
154
155
|
const blockers = boundedList(readiness.blockers, MAXIMUM_REPORTED_BLOCKERS);
|
|
155
156
|
return {
|
|
156
157
|
state: readiness.state,
|
|
158
|
+
navigationReadiness: reportNavigationReadiness(readiness.navigationReadiness),
|
|
157
159
|
approvedPageCount: readiness.approvedPageCount,
|
|
158
160
|
selectedPageCount: readiness.selectedPageCount,
|
|
159
161
|
blockedPageCount: readiness.blockedPageCount,
|
|
@@ -350,6 +352,15 @@ export async function deploy(invocation) {
|
|
|
350
352
|
// The estimate must recognize the request it is about to be followed by.
|
|
351
353
|
...(invocation.allowFailedPreview === true ? { allowFailedPreview: true } : {}),
|
|
352
354
|
});
|
|
355
|
+
onProgress(describeNavigationReadiness(readiness.navigationReadiness));
|
|
356
|
+
if (!isNavigationReady(readiness.navigationReadiness)) {
|
|
357
|
+
const media = readiness.blockers.length ? ` This candidate also has ${readiness.blockers.length} media blocker(s).` : "";
|
|
358
|
+
throw new SiteAuthoringError(
|
|
359
|
+
"deploy.navigation_blocked",
|
|
360
|
+
describeNavigationReadiness(readiness.navigationReadiness) + media,
|
|
361
|
+
{ field: "Navigation", status: readiness.navigationReadiness.state },
|
|
362
|
+
);
|
|
363
|
+
}
|
|
353
364
|
if (readiness.blockers.length > 0) {
|
|
354
365
|
throw new SiteAuthoringError(
|
|
355
366
|
"deploy.media_blocked",
|
package/src/verbs/index.js
CHANGED
|
@@ -17,6 +17,9 @@ import {
|
|
|
17
17
|
VERB_MARKETING_PULL,
|
|
18
18
|
VERB_MARKETING_PUSH,
|
|
19
19
|
VERB_MEDIA_UPLOAD,
|
|
20
|
+
VERB_MEDIA_IMAGES_LIST,
|
|
21
|
+
VERB_MEDIA_IMAGES_GET,
|
|
22
|
+
VERB_MEDIA_IMAGES_DESCRIBE,
|
|
20
23
|
VERB_NAV_PUSH,
|
|
21
24
|
VERB_PAGES_DELETE,
|
|
22
25
|
VERB_PAGES_META_SET,
|
|
@@ -58,6 +61,7 @@ import { logout } from "./logout.js";
|
|
|
58
61
|
import { marketingPull } from "./marketing-pull.js";
|
|
59
62
|
import { marketingPush } from "./marketing-push.js";
|
|
60
63
|
import { mediaUpload } from "./media-upload.js";
|
|
64
|
+
import { mediaImagesDescribe, mediaImagesGet, mediaImagesList } from "./media-images.js";
|
|
61
65
|
import { navPush } from "./nav-push.js";
|
|
62
66
|
import { pagesDelete } from "./pages-delete.js";
|
|
63
67
|
import { pagesMetaSet } from "./pages-meta-set.js";
|
|
@@ -118,6 +122,9 @@ import { whoami } from "./whoami.js";
|
|
|
118
122
|
* ```
|
|
119
123
|
* {
|
|
120
124
|
* fixturePath, // validate: one offline authoring-fixture directory
|
|
125
|
+
* imageId, // media images get/describe: site-owned library image UUID
|
|
126
|
+
* pageSize, pageToken, missingDescription, // media images list
|
|
127
|
+
* imageDescription, clearDescription, expectedRevision, onlyIfMissing, // media images describe
|
|
121
128
|
* paths, // media upload: files and/or directories to upload;
|
|
122
129
|
* // with none, the workspace's media/ directory is walked
|
|
123
130
|
* formKeys, // forms validate / push: narrow to these form keys;
|
|
@@ -187,6 +194,9 @@ export const VERB_HANDLERS = Object.freeze({
|
|
|
187
194
|
[VERB_MARKETING_PUSH]: marketingPush,
|
|
188
195
|
[VERB_LINK_BUILD]: linkBuild,
|
|
189
196
|
[VERB_MEDIA_UPLOAD]: mediaUpload,
|
|
197
|
+
[VERB_MEDIA_IMAGES_LIST]: mediaImagesList,
|
|
198
|
+
[VERB_MEDIA_IMAGES_GET]: mediaImagesGet,
|
|
199
|
+
[VERB_MEDIA_IMAGES_DESCRIBE]: mediaImagesDescribe,
|
|
190
200
|
[VERB_APPROVE]: approve,
|
|
191
201
|
[VERB_DEPLOY]: deploy,
|
|
192
202
|
[VERB_PREVIEW_PAGE]: previewPage,
|