@taprootio/site-authoring 0.11.1 → 0.12.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 +15 -3
- package/examples/riverbend-wellness/manifest.fixture.json +56 -0
- package/examples/riverbend-wellness/pages/green-smoothie.md +28 -0
- package/examples/riverbend-wellness/pages/journal-welcome.md +13 -0
- package/examples/riverbend-wellness/pages/juice-bar.md +11 -0
- package/examples/riverbend-wellness/pages/studio-tour.pm.json +34 -0
- package/package.json +1 -1
- package/src/api.js +3 -9
- package/src/cli.js +2 -0
- package/src/content/components.js +0 -6
- package/src/reference-help.js +89 -15
- package/src/typed-page-reference.js +339 -0
- package/src/typed-pages.js +952 -0
- package/src/verbs/delivery-check.js +23 -3
- package/src/verbs/pages-push.js +200 -61
- package/src/verbs/pull.js +47 -20
- package/src/verbs/validate.js +24 -3
- package/src/workspace.js +42 -8
package/README.md
CHANGED
|
@@ -99,6 +99,7 @@ taproot-site help
|
|
|
99
99
|
taproot-site help designs
|
|
100
100
|
taproot-site help design professional-portfolio
|
|
101
101
|
taproot-site help page free-form
|
|
102
|
+
taproot-site help page recipe
|
|
102
103
|
taproot-site help component hero-section --json
|
|
103
104
|
```
|
|
104
105
|
|
|
@@ -141,8 +142,8 @@ taproot-site validate "$(npm root --global)/@taprootio/site-authoring/examples/r
|
|
|
141
142
|
|
|
142
143
|
The fixture is a fictional wellness studio — `example.test` hostnames, a
|
|
143
144
|
reserved `555-01xx` telephone number, an invented street and town — with two
|
|
144
|
-
free-form pages
|
|
145
|
-
documents. Nothing in it describes a real business. Copy the directory
|
|
145
|
+
free-form pages and one each of the article, recipe, album and place-review
|
|
146
|
+
templates, a three-item navigation tree, and all four settings documents. Nothing in it describes a real business. Copy the directory
|
|
146
147
|
somewhere writable before you edit it; `validate` never writes to the fixture
|
|
147
148
|
it reads.
|
|
148
149
|
|
|
@@ -338,6 +339,17 @@ the site's own document is kept as internal state under
|
|
|
338
339
|
edited on the site since the last pull is a `pages.pull_conflict` refusal
|
|
339
340
|
before anything under `pages/` changes, with both recovery paths named.
|
|
340
341
|
|
|
342
|
+
Pages are not only free-form. A source chooses its template with `template:` in
|
|
343
|
+
its front matter — `article`, `recipe`, `album` or `place-review`; free-form is
|
|
344
|
+
the default — and `pull` writes the other four as a `.pm.json` wrapper
|
|
345
|
+
(`{ "template", "displayDate", "coverImageId", "data" }`) that `pages push`
|
|
346
|
+
sends back unchanged. Typed pages also carry `displayDate` (so an import keeps
|
|
347
|
+
its original publication date) and `coverImage`, which the site requires to be an
|
|
348
|
+
image the page itself uses (place it in the body too; `pages.cover_image_unused`). A page's template is fixed when
|
|
349
|
+
it is created: a source for another template is refused with
|
|
350
|
+
`pages.template_immutable`. `taproot-site help page <type>` gives each
|
|
351
|
+
template's fields and one worked example.
|
|
352
|
+
|
|
341
353
|
`pages push [page-path...]` validates and sends the selected pages, or every
|
|
342
354
|
workspace page when none is named; the homepage is addressed as `/`. It fails
|
|
343
355
|
closed with `pages.push_conflict` when a page's stored-state revision has
|
|
@@ -549,7 +561,7 @@ taproot-site validate ../authoring-fixture
|
|
|
549
561
|
The destination must be new, outside the source workspace, and its parent must exist. Use
|
|
550
562
|
`taproot-site --config path/to/taproot-site.json validate --init ../authoring-fixture`
|
|
551
563
|
to select another source configuration. Initialization needs no credential or
|
|
552
|
-
network. It exports editable
|
|
564
|
+
network. It exports editable pages of every template as workspace documents with navigation,
|
|
553
565
|
redirects, settings, and version-7 appearance/footer metadata, validates the
|
|
554
566
|
result, and leaves the source unchanged. Excluded metadata-only pages are
|
|
555
567
|
counted; unresolved references to them fail validation.
|
|
@@ -30,6 +30,62 @@
|
|
|
30
30
|
"file": "pages/visit.md",
|
|
31
31
|
"sourceFormat": "markdown",
|
|
32
32
|
"workspaceMode": "editable"
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
"pageId": "a0000000-0000-4000-8000-000000000040",
|
|
36
|
+
"resourceId": "a0000000-0000-4000-8000-000000000041",
|
|
37
|
+
"path": "journal/welcome",
|
|
38
|
+
"title": "Welcome to the Riverbend journal",
|
|
39
|
+
"status": "PAGE_STATUS_DRAFT",
|
|
40
|
+
"templateType": "TEMPLATE_TYPE_ARTICLE",
|
|
41
|
+
"hasDraft": true,
|
|
42
|
+
"isGenerated": false,
|
|
43
|
+
"description": "An example article for the offline authoring fixture.",
|
|
44
|
+
"file": "pages/journal-welcome.md",
|
|
45
|
+
"sourceFormat": "markdown",
|
|
46
|
+
"workspaceMode": "editable"
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
"pageId": "a0000000-0000-4000-8000-000000000042",
|
|
50
|
+
"resourceId": "a0000000-0000-4000-8000-000000000043",
|
|
51
|
+
"path": "recipes/green-smoothie",
|
|
52
|
+
"title": "Green morning smoothie",
|
|
53
|
+
"status": "PAGE_STATUS_DRAFT",
|
|
54
|
+
"templateType": "TEMPLATE_TYPE_RECIPE",
|
|
55
|
+
"hasDraft": true,
|
|
56
|
+
"isGenerated": false,
|
|
57
|
+
"description": "An example recipe for the offline authoring fixture.",
|
|
58
|
+
"file": "pages/green-smoothie.md",
|
|
59
|
+
"sourceFormat": "markdown",
|
|
60
|
+
"workspaceMode": "editable"
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
"pageId": "a0000000-0000-4000-8000-000000000044",
|
|
64
|
+
"resourceId": "a0000000-0000-4000-8000-000000000045",
|
|
65
|
+
"path": "albums/studio-tour",
|
|
66
|
+
"title": "A tour of the studio",
|
|
67
|
+
"status": "PAGE_STATUS_DRAFT",
|
|
68
|
+
"templateType": "TEMPLATE_TYPE_ALBUM",
|
|
69
|
+
"hasDraft": true,
|
|
70
|
+
"isGenerated": false,
|
|
71
|
+
"description": "An example album for the offline authoring fixture.",
|
|
72
|
+
"file": "pages/studio-tour.pm.json",
|
|
73
|
+
"sourceFormat": "prosemirror",
|
|
74
|
+
"workspaceMode": "editable"
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
"pageId": "a0000000-0000-4000-8000-000000000046",
|
|
78
|
+
"resourceId": "a0000000-0000-4000-8000-000000000047",
|
|
79
|
+
"path": "reviews/juice-bar",
|
|
80
|
+
"title": "The corner juice bar",
|
|
81
|
+
"status": "PAGE_STATUS_DRAFT",
|
|
82
|
+
"templateType": "TEMPLATE_TYPE_PLACE_REVIEW",
|
|
83
|
+
"hasDraft": true,
|
|
84
|
+
"isGenerated": false,
|
|
85
|
+
"description": "An example place review for the offline authoring fixture.",
|
|
86
|
+
"file": "pages/juice-bar.md",
|
|
87
|
+
"sourceFormat": "markdown",
|
|
88
|
+
"workspaceMode": "editable"
|
|
33
89
|
}
|
|
34
90
|
],
|
|
35
91
|
"pagesTruncated": false,
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Green morning smoothie
|
|
3
|
+
path: recipes/green-smoothie
|
|
4
|
+
description: An example recipe for the offline authoring fixture.
|
|
5
|
+
template: recipe
|
|
6
|
+
prepTimeMinutes: 5
|
|
7
|
+
servings: 2
|
|
8
|
+
recipeCategory: Drink
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
A fictional studio favorite that takes five minutes.
|
|
12
|
+
|
|
13
|
+
## Ingredients
|
|
14
|
+
|
|
15
|
+
### Base
|
|
16
|
+
|
|
17
|
+
- 1 cup spinach
|
|
18
|
+
- 1 banana
|
|
19
|
+
- 1 cup oat milk
|
|
20
|
+
|
|
21
|
+
### Finish
|
|
22
|
+
|
|
23
|
+
- 1 tablespoon chia seeds
|
|
24
|
+
|
|
25
|
+
## Instructions
|
|
26
|
+
|
|
27
|
+
1. Blend the base until smooth.
|
|
28
|
+
2. Stir in the chia seeds and rest for two minutes.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Welcome to the Riverbend journal
|
|
3
|
+
path: journal/welcome
|
|
4
|
+
description: An example article for the offline authoring fixture.
|
|
5
|
+
template: article
|
|
6
|
+
displayDate: 2024-03-01
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
The journal is where the fictional Riverbend Wellness studio shares notes from
|
|
10
|
+
the studio floor. This page is an **article**: a title, a body, and an
|
|
11
|
+
original publication date kept with `displayDate`.
|
|
12
|
+
|
|
13
|
+
A page's template is chosen once, when it is created, and never changes.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: The corner juice bar
|
|
3
|
+
path: reviews/juice-bar
|
|
4
|
+
description: An example place review for the offline authoring fixture.
|
|
5
|
+
template: place-review
|
|
6
|
+
placeId: a0000000-0000-4000-8000-0000000000b0
|
|
7
|
+
rating: will-return
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
An invented juice bar in the fictional town of Elm Harbor. Nothing here
|
|
11
|
+
describes a real business.
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"template": "album",
|
|
3
|
+
"coverImageId": "a0000000-0000-4000-8000-0000000000a1",
|
|
4
|
+
"data": {
|
|
5
|
+
"introductionBody": {
|
|
6
|
+
"type": "doc",
|
|
7
|
+
"content": [
|
|
8
|
+
{
|
|
9
|
+
"type": "paragraph",
|
|
10
|
+
"content": [
|
|
11
|
+
{
|
|
12
|
+
"type": "text",
|
|
13
|
+
"text": "A fictional tour of the studio. An album keeps its images by id, so its source is a JSON document."
|
|
14
|
+
}
|
|
15
|
+
]
|
|
16
|
+
}
|
|
17
|
+
]
|
|
18
|
+
},
|
|
19
|
+
"images": [
|
|
20
|
+
{
|
|
21
|
+
"imageId": "a0000000-0000-4000-8000-0000000000a1",
|
|
22
|
+
"caption": "The main room",
|
|
23
|
+
"width": 1920,
|
|
24
|
+
"height": 1080
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"imageId": "a0000000-0000-4000-8000-0000000000a2",
|
|
28
|
+
"caption": "The tea corner",
|
|
29
|
+
"width": 1200,
|
|
30
|
+
"height": 1600
|
|
31
|
+
}
|
|
32
|
+
]
|
|
33
|
+
}
|
|
34
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@taprootio/site-authoring",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.0",
|
|
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",
|
package/src/api.js
CHANGED
|
@@ -490,6 +490,9 @@ export function normalizePageSummary(value) {
|
|
|
490
490
|
// at" — and both are carried as `undefined` rather than as a sentinel that
|
|
491
491
|
// would compare unequal to every recorded baseline and refuse every push.
|
|
492
492
|
bodyRevision: normalizePageBodyRevision(summary.bodyRevision),
|
|
493
|
+
// The page's calendar date, empty when it has none. It is not part of
|
|
494
|
+
// `bodyRevision`, so a typed page's push guard compares it separately.
|
|
495
|
+
displayDate: typeof summary.displayDate === "string" ? summary.displayDate : "",
|
|
493
496
|
};
|
|
494
497
|
}
|
|
495
498
|
|
|
@@ -526,14 +529,6 @@ export async function getPage(client, pageId, status) {
|
|
|
526
529
|
);
|
|
527
530
|
}
|
|
528
531
|
|
|
529
|
-
export function freeFormTemplate(body) {
|
|
530
|
-
return {
|
|
531
|
-
templateType: TEMPLATE_TYPE_FREE_FORM,
|
|
532
|
-
templateVersion: FREE_FORM_TEMPLATE_VERSION,
|
|
533
|
-
freeFormData: { body },
|
|
534
|
-
};
|
|
535
|
-
}
|
|
536
|
-
|
|
537
532
|
export async function createPage(client, body) {
|
|
538
533
|
return normalizePageSummary(await client.request("v1/pages", { method: "POST", body }));
|
|
539
534
|
}
|
|
@@ -1118,7 +1113,6 @@ export async function listDeployments(
|
|
|
1118
1113
|
`deployments${
|
|
1119
1114
|
query([
|
|
1120
1115
|
["pageSize", pageSize],
|
|
1121
|
-
["includeStaging", true],
|
|
1122
1116
|
["environment", environment],
|
|
1123
1117
|
])
|
|
1124
1118
|
}`,
|
package/src/cli.js
CHANGED
|
@@ -705,6 +705,7 @@ function parseReferenceArguments(arguments_) {
|
|
|
705
705
|
|| topic === "redirects"
|
|
706
706
|
|| topic === "walkthrough"
|
|
707
707
|
|| topic === "delivery"
|
|
708
|
+
|| topic === "import"
|
|
708
709
|
)
|
|
709
710
|
&& (subject !== undefined || extra.length > 0)
|
|
710
711
|
) {
|
|
@@ -774,6 +775,7 @@ function referenceResult(parsed) {
|
|
|
774
775
|
case "fixture":
|
|
775
776
|
case "walkthrough":
|
|
776
777
|
case "delivery":
|
|
778
|
+
case "import":
|
|
777
779
|
return {
|
|
778
780
|
...result,
|
|
779
781
|
topic: "workflow",
|
|
@@ -157,10 +157,6 @@ const CTA = Object.freeze({
|
|
|
157
157
|
}),
|
|
158
158
|
imagePosition: enumOf("top", "left", "right"),
|
|
159
159
|
mediaImage: image(),
|
|
160
|
-
// TR00890: "waitlist" replaces the link button with an email-capture form
|
|
161
|
-
// that posts to the platform's public waitlist endpoint. buttonUrl is
|
|
162
|
-
// ignored in that mode; buttonText becomes the submit button's label.
|
|
163
|
-
formMode: enumOf("none", "waitlist"),
|
|
164
160
|
});
|
|
165
161
|
|
|
166
162
|
/** `FeatureGridData`. */
|
|
@@ -363,12 +359,10 @@ const COMPONENT_DEFINITIONS = Object.freeze({
|
|
|
363
359
|
borderWidth: 0,
|
|
364
360
|
imagePosition: "top",
|
|
365
361
|
mediaImage: null,
|
|
366
|
-
formMode: "none",
|
|
367
362
|
},
|
|
368
363
|
[
|
|
369
364
|
"Button text must describe its action or destination.",
|
|
370
365
|
"Give meaningful media a descriptive alt value and decorative media an empty alt value.",
|
|
371
|
-
"formMode \"waitlist\" renders an email-capture form instead of the link button; leave buttonUrl empty and use buttonText for the submit label.",
|
|
372
366
|
],
|
|
373
367
|
{
|
|
374
368
|
heading: "Ready to begin?",
|
package/src/reference-help.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs";
|
|
2
2
|
|
|
3
|
-
import { TEMPLATE_TYPE_FREE_FORM } from "./api.js";
|
|
4
3
|
import { CLI_BINARY_NAME } from "./constants.js";
|
|
5
4
|
import {
|
|
6
5
|
canonicalizeComponentData,
|
|
@@ -35,6 +34,8 @@ import {
|
|
|
35
34
|
REDIRECTS_FILE_NAME,
|
|
36
35
|
} from "./redirects-contract.js";
|
|
37
36
|
import { SETTINGS_GROUPS } from "./settings-catalog.js";
|
|
37
|
+
import { formatTypedPageReference, getTypedPageReference, TYPED_PAGE_TYPES } from "./typed-page-reference.js";
|
|
38
|
+
import { PAGE_TEMPLATES } from "./typed-pages.js";
|
|
38
39
|
import {
|
|
39
40
|
INTERNAL_PAGE_BASELINE_DIRECTORY,
|
|
40
41
|
MANIFEST_VERSION,
|
|
@@ -49,8 +50,8 @@ import {
|
|
|
49
50
|
|
|
50
51
|
export { getAppearanceReference, getFooterReference, getThemeReference };
|
|
51
52
|
|
|
52
|
-
export const REFERENCE_VERSION =
|
|
53
|
-
export const PAGE_TYPES = Object.freeze(["free-form"]);
|
|
53
|
+
export const REFERENCE_VERSION = 28;
|
|
54
|
+
export const PAGE_TYPES = Object.freeze(["free-form", ...TYPED_PAGE_TYPES]);
|
|
54
55
|
|
|
55
56
|
function deepFreeze(value) {
|
|
56
57
|
if (value === null || typeof value !== "object" || Object.isFrozen(value)) return value;
|
|
@@ -334,7 +335,7 @@ export function getDesignBlueprint(type) {
|
|
|
334
335
|
return DESIGN_BLUEPRINTS.find((blueprint) => blueprint.type === type);
|
|
335
336
|
}
|
|
336
337
|
export const REFERENCE_TOPICS = Object.freeze([
|
|
337
|
-
Object.freeze({ name: "pages", usage: `${CLI_BINARY_NAME} help pages`, summary: "List authorable page types." }),
|
|
338
|
+
Object.freeze({ name: "pages", usage: `${CLI_BINARY_NAME} help pages`, summary: "List authorable page types: free-form, article, recipe, album, place-review." }),
|
|
338
339
|
Object.freeze({
|
|
339
340
|
name: "page",
|
|
340
341
|
usage: `${CLI_BINARY_NAME} help page <page-type>`,
|
|
@@ -397,6 +398,11 @@ export const REFERENCE_TOPICS = Object.freeze([
|
|
|
397
398
|
usage: `${CLI_BINARY_NAME} help footer`,
|
|
398
399
|
summary: "Author and concurrency-safely push the complete footer document.",
|
|
399
400
|
}),
|
|
401
|
+
Object.freeze({
|
|
402
|
+
name: "import",
|
|
403
|
+
usage: `${CLI_BINARY_NAME} help import`,
|
|
404
|
+
summary: "Import an existing WordPress site from its export: mapping rules, order of work, and what to report.",
|
|
405
|
+
}),
|
|
400
406
|
Object.freeze({
|
|
401
407
|
name: "fixture",
|
|
402
408
|
usage: `${CLI_BINARY_NAME} help fixture`,
|
|
@@ -405,6 +411,60 @@ export const REFERENCE_TOPICS = Object.freeze([
|
|
|
405
411
|
]);
|
|
406
412
|
|
|
407
413
|
const WORKFLOW_REFERENCES = Object.freeze({
|
|
414
|
+
import: Object.freeze({
|
|
415
|
+
title: "Importing a WordPress site",
|
|
416
|
+
summary: "Turn a WordPress export (WXR) into a published Taproot site with the templates, dates, media and redirects preserved, and a written account of everything that could not be mapped.",
|
|
417
|
+
usage: `${CLI_BINARY_NAME} pull | media upload <files...> | pages push | redirects push | nav push | deploy --staging`,
|
|
418
|
+
details: Object.freeze([
|
|
419
|
+
"Source: a WordPress export file (Tools > Export > All content, WXR 1.2). Read the live site only when the owner "
|
|
420
|
+
+ "has said you may; the export names every media URL but carries no image bytes, so downloading originals from "
|
|
421
|
+
+ "the old site also needs that permission. Work in a fresh workspace: 'use' the site, then 'pull'.",
|
|
422
|
+
"Order of work: 1 parse the export; 2 classify every item; 3 stage media; 4 write page sources; 5 check every "
|
|
423
|
+
+ "source; 6 pages push; 7 nav.json and nav push; 8 redirects.json and redirects push; 9 approve, deploy "
|
|
424
|
+
+ "--staging, review it; 10 redirects check and delivery check --staging; 11 deploy --production; 12 delivery "
|
|
425
|
+
+ "check --production --url <the origin 'sites' reports as primaryDomain>. Do not promote until the account of unmapped content has been read by the owner.",
|
|
426
|
+
"Classify. Published post: article (the default); recipe when it has an Ingredients list and a Steps list "
|
|
427
|
+
+ "(help page recipe); album when it is a gallery with little text (help page album). Published page: a free-form "
|
|
428
|
+
+ "page at its path, children under their parent's path. Never guess a place review: it needs a Taproot "
|
|
429
|
+
+ "placeId this CLI cannot look up, so import the post as an article and list it. Not imported, and always "
|
|
430
|
+
+ "listed: drafts, pending, private, scheduled and password-protected items; custom post types; comments; the WordPress front "
|
|
431
|
+
+ "page (Taproot seeds its own home page: ask the owner what belongs there).",
|
|
432
|
+
"Paths and dates. A post's path is '<first category slug, ignoring the default Uncategorized>/<post name>' ('journal/' with no category; a post or page whose slug has no ASCII letters gets a name like post-<id>, listed), a "
|
|
433
|
+
+ "page's is its post name under its parents. Keep the original publication date in displayDate (the date "
|
|
434
|
+
+ "part of wp:post_date). Categories beyond the first, and all tags, have no Taproot equivalent (tags are "
|
|
435
|
+
+ "generated per page type): list them. The featured image becomes coverImage on typed pages, and the same image must also appear in the page's own content (put it at the top of the body), or the site refuses the cover; a free-form page "
|
|
436
|
+
+ "has none, so list it.",
|
|
437
|
+
"Content. Convert the HTML to the Markdown subset. Keep headings from h2 to h4 (the title is the h1; deeper levels are refused, so make h5 and h6 an h4). Decode HTML entities (&, ’) in titles, excerpts, menu labels and term names; WordPress stores them encoded. An image "
|
|
438
|
+
+ "must stand alone in its own block, so split paragraphs around images and lift images out of links; a link "
|
|
439
|
+
+ "to the full-size file is dropped, so list it. Tables need a header row and 2 to 12 columns, else write "
|
|
440
|
+
+ "a list and report it. Remove Gutenberg block comments and the more-tag marker. In a recipe, times and servings "
|
|
441
|
+
+ "go to front matter, text after the steps moves into the introduction with its headings dropped to level 3, and the only "
|
|
442
|
+
+ "level-2 headings are Ingredients and Instructions; an album's only level-2 heading is Images.",
|
|
443
|
+
"Report, never silently drop. Shortcodes with no equivalent ([contact-form-7], [adsense], maps, recipe cards "
|
|
444
|
+
+ "whose data sits in separate records), iframes and other embeds (a YouTube, Vimeo, X/Twitter, Instagram or TikTok address becomes a plain "
|
|
445
|
+
+ "link), scripts, video and audio elements, inline styles, galleries (flattened to sequential images, or an "
|
|
446
|
+
+ "album when they are the post), links to content you did not import (keep the absolute old link), and images "
|
|
447
|
+
+ "on other hosts. Every item goes in the account with its page and what happened to it.",
|
|
448
|
+
"Media. Save each original in the workspace's media/ directory (PNG, JPEG, GIF or WebP up to 32 MiB each; list SVG, AVIF, HEIC and oversized files, which cannot be uploaded; use the original, not "
|
|
449
|
+
+ "the -300x200 thumbnail WordPress also stores), then upload in batches of at most 10 files (conservative: the limit depends on how long the delivery URLs are, and was measured only with short stand-in URLs): 'media upload' "
|
|
450
|
+
+ "prints every file's delivery URLs into a result capped at 64 KiB, and a longer batch uploads everything, "
|
|
451
|
+
+ "writes .taproot-site-media.json, then exits 1 with output.too_large. If that happens the files are already "
|
|
452
|
+
+ "uploaded; run the same paths again (they deduplicate) in smaller batches.",
|
|
453
|
+
"Check before sending. 'pages push' validates every source before it sends anything but stops at the first "
|
|
454
|
+
+ "problem. On a large import, validate the sources yourself first so you see every problem at once: run the "
|
|
455
|
+
+ "package's validators over each file, or push one page at a time ('pages push <path>').",
|
|
456
|
+
"Navigation. A site can have several menus (header, footer): ask which is primary and list the rest. Rebuild it as nav.json after 'pages push', at most three levels deep with canonical UUID ids, 1000 items and 200-character titles (help nav); a label-only dropdown heading (a WordPress '#' link) is a GROUP_HEADER, and a child of an item you cannot map is listed, not promoted (page items carry the resourceId from the "
|
|
457
|
+
+ "manifest; a custom link is an EXTERNAL_URL item; a category or tag item has no target, so list it).",
|
|
458
|
+
"Redirects. At most 2000 entries; never redirect to a page you did not push. Every imported page's old path (from its <link>) becomes a 301 to its new path, unless the two "
|
|
459
|
+
+ "are the same. A category archive redirects to its folder when it became one. Query-string URLs (?p=12), "
|
|
460
|
+
+ "feeds, tag archives, author archives and attachment pages cannot be redirect sources or have no "
|
|
461
|
+
+ "destination: list them and ask the owner whether each is 410 or goes to an index. A source that a live page "
|
|
462
|
+
+ "occupies is refused by the site.",
|
|
463
|
+
"Finish with the account: counts of pages by template, media staged, redirects written, then every unmapped "
|
|
464
|
+
+ "item grouped by kind with examples, the questions only the owner can answer, and the verification results "
|
|
465
|
+
+ "(redirects check, delivery check).",
|
|
466
|
+
]),
|
|
467
|
+
}),
|
|
408
468
|
delivery: Object.freeze({
|
|
409
469
|
title: "Delivery verification",
|
|
410
470
|
summary: "delivery check reads a completed deployment's target the way a visitor does and reports HTTP delivery, runtime compatibility and (optionally) browser behaviour as separate dimensions.",
|
|
@@ -683,8 +743,10 @@ const WORKFLOW_REFERENCES = Object.freeze({
|
|
|
683
743
|
`manifestVersion must be ${MANIFEST_VERSION}; fixture.contractVersion must be ${FIXTURE_CONTRACT_VERSION}.`,
|
|
684
744
|
"siteId, every pageId and resourceId, every settings entityId, and every fixture.imageIds entry is a canonical "
|
|
685
745
|
+ "lowercase UUID. They are deterministic fixture identities, not identities from a live site.",
|
|
686
|
-
`Each pages[] entry binds one editable
|
|
687
|
-
+
|
|
746
|
+
`Each pages[] entry binds one editable page source: pageId, resourceId, path, title, templateType `
|
|
747
|
+
+ `(${Object.values(PAGE_TEMPLATES).map((template) => template.wireType).join(", ")}; the source must be that template), file, `
|
|
748
|
+
+ `sourceFormat, and workspaceMode ${PAGE_WORKSPACE_MODE_EDITABLE}. Album images and the cover image must be `
|
|
749
|
+
+ "listed in fixture.imageIds. "
|
|
688
750
|
+ "status, hasDraft, isGenerated, and description are recorded by pull and not read: the page key set "
|
|
689
751
|
+ "stays open so a fixture from a newer pull is not refused. The homepage's path is the empty string.",
|
|
690
752
|
`file lives under '${PAGES_DIRECTORY}/' and ends in ${PAGE_SOURCE_EXTENSIONS.join(" or ")}; sourceFormat must be `
|
|
@@ -716,7 +778,7 @@ const WORKFLOW_REFERENCES = Object.freeze({
|
|
|
716
778
|
"validate reads the fixture and writes nothing to it. Copy the directory somewhere writable before editing it.",
|
|
717
779
|
"validate --init <new-directory> exports the current pulled workspace as a validated version-7 fixture with "
|
|
718
780
|
+ "appearance and footer metadata. Run from the workspace or its configured project; --config may select the source.",
|
|
719
|
-
"Initialization keeps editable
|
|
781
|
+
"Initialization keeps editable pages of every template as workspace documents and reports excluded metadata-only pages. "
|
|
720
782
|
+ "References to excluded pages must be resolved before the fixture can validate. It never copies credentials, "
|
|
721
783
|
+ "internal reconciliation state, or deployment receipts. UUIDs and HTTP(S) origins are replaced with deterministic "
|
|
722
784
|
+ "fixture identities and example.test origins; URL credentials, queries and fragments are removed. Authored prose is retained.",
|
|
@@ -1302,6 +1364,14 @@ const FREE_FORM_REFERENCE = Object.freeze({
|
|
|
1302
1364
|
inheritedForTracked: true,
|
|
1303
1365
|
defaultForNew: "",
|
|
1304
1366
|
}),
|
|
1367
|
+
Object.freeze({
|
|
1368
|
+
name: "template",
|
|
1369
|
+
type: "string",
|
|
1370
|
+
requiredForNew: false,
|
|
1371
|
+
inheritedForTracked: false,
|
|
1372
|
+
defaultForNew: "free-form",
|
|
1373
|
+
description: `Other templates: ${TYPED_PAGE_TYPES.join(", ")}; see '${CLI_BINARY_NAME} help pages'.`,
|
|
1374
|
+
}),
|
|
1305
1375
|
]),
|
|
1306
1376
|
}),
|
|
1307
1377
|
Object.freeze({
|
|
@@ -1387,17 +1457,20 @@ const FREE_FORM_REFERENCE = Object.freeze({
|
|
|
1387
1457
|
});
|
|
1388
1458
|
|
|
1389
1459
|
export function listPageTypeReferences() {
|
|
1390
|
-
return PAGE_TYPES.map((type) =>
|
|
1391
|
-
type
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1460
|
+
return PAGE_TYPES.map((type) => {
|
|
1461
|
+
const reference = type === "free-form" ? FREE_FORM_REFERENCE : getTypedPageReference(type);
|
|
1462
|
+
return {
|
|
1463
|
+
type,
|
|
1464
|
+
displayName: reference.displayName,
|
|
1465
|
+
summary: reference.summary,
|
|
1466
|
+
helpCommand: `${CLI_BINARY_NAME} help page ${type}`,
|
|
1467
|
+
};
|
|
1468
|
+
});
|
|
1396
1469
|
}
|
|
1397
1470
|
|
|
1398
1471
|
export function getPageTypeReference(type) {
|
|
1399
|
-
if (type
|
|
1400
|
-
return
|
|
1472
|
+
if (type === "free-form") return { ...FREE_FORM_REFERENCE, components: listComponentTypeReferences() };
|
|
1473
|
+
return getTypedPageReference(type);
|
|
1401
1474
|
}
|
|
1402
1475
|
|
|
1403
1476
|
function schemaLabel(schema) {
|
|
@@ -1557,6 +1630,7 @@ export function formatReferenceResult(result) {
|
|
|
1557
1630
|
}
|
|
1558
1631
|
case "page": {
|
|
1559
1632
|
const page = result.page;
|
|
1633
|
+
if (page.type !== "free-form") return formatTypedPageReference(page);
|
|
1560
1634
|
const markdownFormat = page.workspace.formats.find((format) => format.extension === ".md");
|
|
1561
1635
|
const sourceRule = page.workspace.sourceRule;
|
|
1562
1636
|
return `${page.displayName} (${page.type})\n${page.summary}\n\nWorkspace:\n${
|