@taprootio/site-authoring 0.27.0 → 0.28.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 +11 -12
- package/package.json +1 -1
- package/src/changelog.json +31 -0
- package/src/cli.js +3 -3
- package/src/reference-help.js +8 -5
- package/src/site-checks.js +25 -1
- package/src/typed-page-reference.js +7 -14
- package/src/typed-pages.js +11 -29
- package/src/verbs/pages-push.js +34 -11
- package/src/verbs/places.js +2 -2
- package/src/verbs/pull.js +1 -1
package/README.md
CHANGED
|
@@ -140,7 +140,7 @@ with no credential, no network, and no write: every page source (metadata,
|
|
|
140
140
|
Markdown conversion, media references, content vocabulary, section contexts),
|
|
141
141
|
page paths and templates against the pages the pull recorded, navigation,
|
|
142
142
|
redirects, forms, and the complete theme, appearance, header, brand, and
|
|
143
|
-
footer. Navigation to a generated page (a section
|
|
143
|
+
footer. Navigation to a generated page (a section or folder page) resolves
|
|
144
144
|
against the pages the pull recorded. It reports every problem at once in
|
|
145
145
|
`error.problems`, each with its file, code, and field, instead of one per run.
|
|
146
146
|
Warnings never refuse: `linkWarnings` names a link whose address is only
|
|
@@ -506,12 +506,11 @@ listings only while that person can still be named: a former member's page, or
|
|
|
506
506
|
a member who can no longer create pages, reports none, and a push that names
|
|
507
507
|
someone else on it is refused as a conflict.
|
|
508
508
|
|
|
509
|
-
Pages Taproot generates from the site's content —
|
|
510
|
-
|
|
511
|
-
`
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
things that are yours to author: `customTitle`, `breadcrumbTitle`,
|
|
509
|
+
Pages Taproot generates from the site's content — the published archive, a
|
|
510
|
+
folder — are update-only sources. `pull` writes each as
|
|
511
|
+
`pages/<name>.pm.json` with `"template": "generated"`: `data` holds what the
|
|
512
|
+
page is (`kind`, which you leave as pulled) and the four things
|
|
513
|
+
that are yours to author: `customTitle`, `breadcrumbTitle`,
|
|
515
514
|
`customDescription` and `introductionBody`. An empty string means "use the
|
|
516
515
|
generated default"; the description then falls back to the introduction's first
|
|
517
516
|
paragraph, else the system default. The manifest keeps the title and description
|
|
@@ -525,8 +524,8 @@ move one (`pages.generated_move`) or change its kind or identity
|
|
|
525
524
|
(`pages.generated_identity`, also when the manifest has no record of what the
|
|
526
525
|
file was pulled as: run `pull`); there is no Markdown form
|
|
527
526
|
(`pages.generated_markdown`). A generated page you did not edit never blocks a
|
|
528
|
-
push of the whole workspace, even when Taproot
|
|
529
|
-
|
|
527
|
+
push of the whole workspace, even when Taproot changed it since the pull. If the
|
|
528
|
+
site no longer has the page (its folder is gone, or Taproot retired its kind),
|
|
530
529
|
a whole-workspace push reports the file under `pages.staleGeneratedSources` and
|
|
531
530
|
sends nothing for it, and `pull` says so; delete the file. Naming that path in
|
|
532
531
|
`pages push <page-path>` is refused with `pages.generated_create`. A description over 160 characters is warned about,
|
|
@@ -857,8 +856,8 @@ taproot-site places category clear <placeId>
|
|
|
857
856
|
`list` prints the closed list `set` accepts; a name outside it is refused, and
|
|
858
857
|
capitalization is ignored. The category is the site's alone and applies to every
|
|
859
858
|
review of that place on the site: the place itself and other sites do not
|
|
860
|
-
change. Place-review bylines
|
|
861
|
-
|
|
859
|
+
change. Place-review bylines use it from the site's next deployment, including
|
|
860
|
+
reviews nobody has edited. `clear` removes it.
|
|
862
861
|
Both need the same page-editing permission as `places select`.
|
|
863
862
|
|
|
864
863
|
### Pages generated in 0.18.0
|
|
@@ -892,7 +891,7 @@ once).
|
|
|
892
891
|
page's title, description or author; `pull` keeps the edit until `pages push`
|
|
893
892
|
sends it. Descriptions are warned about over 160 characters and refused over
|
|
894
893
|
1000, here and by Taproot.
|
|
895
|
-
- Generated pages (
|
|
894
|
+
- Generated pages (the published archive, the map, folders) pull as update-only sources
|
|
896
895
|
whose custom title, breadcrumb, description and introduction you can edit.
|
|
897
896
|
- Pages are authorless by default. `authors list`, `authors add` and
|
|
898
897
|
`authors.json` name a member who can create pages or a site author without a
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@taprootio/site-authoring",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.28.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/changelog.json
CHANGED
|
@@ -1,5 +1,36 @@
|
|
|
1
1
|
{
|
|
2
2
|
"entries": [
|
|
3
|
+
{
|
|
4
|
+
"version": "0.28.0",
|
|
5
|
+
"note": "Taproot no longer generates /places, year and month, or tag pages, so a generated page's data drops those fields. One /published page lists every month, a folder page reaches search only with an introduction or three cards, and a review's location byline opens Google Maps.",
|
|
6
|
+
"behavior": [
|
|
7
|
+
"The /published/<year> and /published/<year>/<month> pages are no longer generated. One /published page carries a year and a month picker filled from JSON files the release writes (published/months.json and published/<year>/<month>.json), and a content byline links to /published?year=YYYY&month=M. Those file paths are refused as redirect sources with redirect.source_generated_file.",
|
|
8
|
+
"A generated page's data drops year and month beside the place fields: pull writes kind and the four authored fields, and a source that still carries year or month is refused by pages push with pages.document_shape as an unsupported field: delete the year and month lines from sources pulled before this release (every generated page pulled earlier carries year: 0 and month: 0).",
|
|
9
|
+
"The /places index, country, region, city and category pages are no longer generated, so a site has no generated page of those kinds to pull. On a published review, article or album with a place on a site with no map page, the location byline opens the place's Google Maps listing in a new tab; the review's category byline is plain text.",
|
|
10
|
+
"A generated page's data holds kind and the four authored fields. pull no longer writes countryCode, regionCode, citySlug or categorySlug, and a source that still carries one is refused by pages push with pages.document_shape as an unsupported field: delete the four lines from sources pulled before this release.",
|
|
11
|
+
"/places is no longer a reserved path, so a page may be authored there.",
|
|
12
|
+
"A workspace that pulled a /places page before this release keeps that file: pages push reports it as the source of a generated page Taproot no longer publishes and sends the rest, whether or not the site has synchronized the page away yet; naming its path is refused with pages.generated_create. Delete the file.",
|
|
13
|
+
"Taproot no longer generates tag pages (/tags and /tags/<slug>): tags stay page metadata with no public page, a workspace that pulled one keeps the file and pages push reports it as the source of a page Taproot no longer publishes, and /tags is no longer a reserved path. A generated page's data holds kind and the four authored fields; tagId leaves the identity, and a source that still carries it is refused by pages push with pages.document_shape as an unsupported field: delete the line from sources pulled before this release.",
|
|
14
|
+
"A folder index is a search destination when it has an introduction or renders at least three cards; otherwise it carries noindex and leaves the sitemap. Write the page's introductionBody in its pulled .pm.json source to make a small folder one.",
|
|
15
|
+
"Taproot generates a /map page for a site with a public post that has a location. It is a new generated kind in pulls, /map and every path under it are reserved, and /map/places.json cannot be a redirect source.",
|
|
16
|
+
"The help reference version is 47."
|
|
17
|
+
],
|
|
18
|
+
"help": [
|
|
19
|
+
"pages",
|
|
20
|
+
"page"
|
|
21
|
+
]
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
"version": "0.27.1",
|
|
25
|
+
"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.",
|
|
26
|
+
"behavior": [
|
|
27
|
+
"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.",
|
|
28
|
+
"The help reference version is 45."
|
|
29
|
+
],
|
|
30
|
+
"help": [
|
|
31
|
+
"redirects"
|
|
32
|
+
]
|
|
33
|
+
},
|
|
3
34
|
{
|
|
4
35
|
"version": "0.27.0",
|
|
5
36
|
"note": "Embedded forms inherit configurable background, border and shadow defaults from the site, with per-form overrides.",
|
package/src/cli.js
CHANGED
|
@@ -139,7 +139,7 @@ const VERBS = Object.freeze([
|
|
|
139
139
|
+ "appearance, header, brand, footer, navigation, redirects, and forms are checked with the validators the "
|
|
140
140
|
+ "pushes use, and every problem is reported at once in error.problems (each with its file, code, and field) "
|
|
141
141
|
+ "rather than one per run. A pulled workspace is checked offline against the pages (generated ones included, "
|
|
142
|
-
+ "so navigation to a
|
|
142
|
+
+ "so navigation to a folder index resolves), images, and baselines its manifests recorded at the pull. "
|
|
143
143
|
+ "A link whose address is only http(s):// plus its own text (left by older editor autolinking) is reported in "
|
|
144
144
|
+ "linkWarnings without refusing; pages push reports the same for the pages it sends. "
|
|
145
145
|
+ "The site checks that can run offline are reported in findings (a rule id, severity, file, and the value "
|
|
@@ -287,7 +287,7 @@ const VERBS = Object.freeze([
|
|
|
287
287
|
+ "and pull again. A locally edited '.pm.json' is kept rather than overwritten for the same reason. "
|
|
288
288
|
+ "First revisions adopted without a completed body comparison are counted in pages.revisionsRecordedWithoutBodyComparison "
|
|
289
289
|
+ "and announced on the human channel. New downloads and compared bodies are excluded. "
|
|
290
|
-
+ "A page Taproot generates (
|
|
290
|
+
+ "A page Taproot generates (the published archive, the map or a folder) is pulled as an update-only '.pm.json' "
|
|
291
291
|
+ "(template 'generated'): you edit its custom title, breadcrumb title, custom description and introduction, "
|
|
292
292
|
+ "and the manifest keeps the title and description the site reports. "
|
|
293
293
|
+ "A form a page places is written back by its key, the name 'forms/<key>.json' uses, though the site stores "
|
|
@@ -445,7 +445,7 @@ const VERBS = Object.freeze([
|
|
|
445
445
|
+ "'places category list', which prints the closed list; a name outside it is refused, and capitalization is "
|
|
446
446
|
+ "ignored. Use it when Google's own category for a place is not the one this site wants, such as a park filed "
|
|
447
447
|
+ "under Other. The category is this site's alone and applies to every review of that place here: the place "
|
|
448
|
-
+ "itself and other sites do not change. Place-review bylines
|
|
448
|
+
+ "itself and other sites do not change. Place-review bylines use it from the "
|
|
449
449
|
+ "next deployment, which a changed category alone is enough to start. Undo it with 'places category clear'.",
|
|
450
450
|
},
|
|
451
451
|
{
|
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 = 47;
|
|
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({
|
|
@@ -755,7 +755,7 @@ const WORKFLOW_REFERENCES = Object.freeze({
|
|
|
755
755
|
+ "those away, so '/x/../visit' would be checked as itself and served as '/visit' — past the occupancy, "
|
|
756
756
|
+ "chain, and loop rules that were asked about a different path. Write the resolved path instead.",
|
|
757
757
|
"origin is reported by the site and ignored on push: 'authored' is what you declared, 'path_history' is "
|
|
758
|
-
+ "what a page
|
|
758
|
+
+ "what a page rename recorded. Pull the map, edit it, push the whole thing back — an entry you "
|
|
759
759
|
+ "did not change keeps its origin.",
|
|
760
760
|
"Refused, offline and again at the site, each naming the entry index: a chain (a target that is itself a "
|
|
761
761
|
+ "source in the same map); a loop (an entry targeting itself); a duplicate path; a gone entry carrying a "
|
|
@@ -786,7 +786,10 @@ 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',
|
|
789
|
+
+ "'/sitemap.xml', '/robots.txt', '/favicon.ico', the published archive's data files ('/published/months.json' "
|
|
790
|
+
+ "and '/published/<year>/<month>.json'), the map page's data file ('/map/places.json'), a path under any of those names, or the "
|
|
791
|
+
+ "search-engine key file Taproot publishes when a site tells search engines about its releases (32 "
|
|
792
|
+
+ "lowercase hexadecimal characters followed by '.txt', in any capitals). Nothing under "
|
|
790
793
|
+ "'/taproot/<version>/' (for example '/taproot/5.0.62/manifest.json') can be a source either: the platform "
|
|
791
794
|
+ "serves the published-site runtime there before any redirect. The platform "
|
|
792
795
|
+ "already redirects every '…/index.html' address to its directory, so no entry is needed for one. A source "
|
|
@@ -868,8 +871,8 @@ const WORKFLOW_REFERENCES = Object.freeze({
|
|
|
868
871
|
+ "refused as forms.page_not_found, so push the page first. It need not be published to save the form, but "
|
|
869
872
|
+ "a deploy whose release does not publish that page is refused, naming the form and the page. The path in "
|
|
870
873
|
+ "each release is the one the page has in that release, so the published form never asks the site where "
|
|
871
|
-
+ "to go. The page must be one you created, the home page included: a generated page (folder
|
|
872
|
-
+ "archive
|
|
874
|
+
+ "to go. The page must be one you created, the home page included: a generated page (a folder index or "
|
|
875
|
+
+ "the published archive), the 404 page, a legal page (help page legal), a profile home and an integration-managed page are refused as forms.page_not_allowed. The button text and the confirmation belong to the form, not to the page that places it: "
|
|
873
876
|
+ "the 'form' component takes only formKey.",
|
|
874
877
|
"A form's contact address is the value of one email field, stored with each submission. That is what lets the "
|
|
875
878
|
+ "owner find and delete every row for a visitor who asks. With exactly one email field, that field is the "
|
package/src/site-checks.js
CHANGED
|
@@ -280,6 +280,15 @@ 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;
|
|
285
|
+
// The /published archive's data files (TR01309): months.json and yyyy/mm.json, with anything beneath either;
|
|
286
|
+
// a marker at one of those names fails every publish in the generator's occupancy check. The API mirror is
|
|
287
|
+
// SiteRedirectMap.IsPublishedArchiveDataPath.
|
|
288
|
+
const PUBLISHED_ARCHIVE_ROOT = "published";
|
|
289
|
+
const ARCHIVE_MONTHS_FILE = "months.json";
|
|
290
|
+
const ARCHIVE_YEAR_SEGMENT = /^\d{4}$/u;
|
|
291
|
+
const ARCHIVE_MONTH_FILE = /^\d{2}\.json$/iu;
|
|
283
292
|
const INTEGRATION_DATA_ROOT = "integrations";
|
|
284
293
|
const MAX_MARKER_SEGMENT_BYTES = 200;
|
|
285
294
|
const MAX_OUTPUT_PATH_LENGTH = 1_024;
|
|
@@ -288,6 +297,18 @@ function isWindowsDeviceSegment(segment) {
|
|
|
288
297
|
const stem = segment.split(".", 1)[0].toLowerCase();
|
|
289
298
|
return WINDOWS_DEVICE_STEMS.has(stem) || /^(?:com|lpt)[1-9]$/u.test(stem);
|
|
290
299
|
}
|
|
300
|
+
function isPublishedArchiveDataPath(segments) {
|
|
301
|
+
if (segments.length < 2 || segments[0].toLowerCase() !== PUBLISHED_ARCHIVE_ROOT)
|
|
302
|
+
return false;
|
|
303
|
+
if (segments[1].toLowerCase() === ARCHIVE_MONTHS_FILE)
|
|
304
|
+
return true;
|
|
305
|
+
return segments.length >= 3 && ARCHIVE_YEAR_SEGMENT.test(segments[1]) && ARCHIVE_MONTH_FILE.test(segments[2]);
|
|
306
|
+
}
|
|
307
|
+
// The /map page's data file (TR01321), with anything beneath it, for the same reason.
|
|
308
|
+
// The API mirrors this in SiteRedirectMap.IsPlaceMapDataPath.
|
|
309
|
+
function isPlaceMapDataPath(segments) {
|
|
310
|
+
return segments.length >= 2 && segments[0].toLowerCase() === "map" && segments[1].toLowerCase() === "places.json";
|
|
311
|
+
}
|
|
291
312
|
function isStorable(decoded) {
|
|
292
313
|
const output = `${decoded}/index.html`;
|
|
293
314
|
if (output !== output.normalize("NFC") || output.length > MAX_OUTPUT_PATH_LENGTH || output.startsWith("/")
|
|
@@ -317,8 +338,11 @@ export function redirectSourceRule(edgePath) {
|
|
|
317
338
|
return refuse(RULE_REDIRECT_SOURCE_INTEGRATION_DATA);
|
|
318
339
|
if (isRuntimeMirrorPath(decoded))
|
|
319
340
|
return refuse(RULE_REDIRECT_SOURCE_RUNTIME_MIRROR);
|
|
320
|
-
if (GENERATED_ROOT_FILES.includes(first.toLowerCase()))
|
|
341
|
+
if (GENERATED_ROOT_FILES.includes(first.toLowerCase()) || INDEX_NOW_KEY_FILE.test(first.toLowerCase())
|
|
342
|
+
|| isPublishedArchiveDataPath(segments)
|
|
343
|
+
|| isPlaceMapDataPath(segments)) {
|
|
321
344
|
return refuse(RULE_REDIRECT_SOURCE_GENERATED_FILE);
|
|
345
|
+
}
|
|
322
346
|
if (!isStorable(decoded))
|
|
323
347
|
return refuse(RULE_REDIRECT_SOURCE_NOT_STORABLE);
|
|
324
348
|
return undefined;
|
|
@@ -77,7 +77,7 @@ const FIELD_HELP = Object.freeze({
|
|
|
77
77
|
description:
|
|
78
78
|
"The Taproot place being reviewed. The site fills in its name, address and coordinates from this record. Find "
|
|
79
79
|
+ "it with 'places search <name and city>' and 'places select <googlePlaceId> <sessionToken>'. The site publishes the "
|
|
80
|
-
+ "place's category in the byline
|
|
80
|
+
+ "place's category in the byline; change it for this site with "
|
|
81
81
|
+ "'places category set <placeId> <category>'.",
|
|
82
82
|
},
|
|
83
83
|
place: {
|
|
@@ -263,7 +263,7 @@ const TYPE_DETAILS = Object.freeze({
|
|
|
263
263
|
},
|
|
264
264
|
[TEMPLATE_PLACE_REVIEW]: {
|
|
265
265
|
displayName: "Place review",
|
|
266
|
-
summary: "A review of a place with a three-step rating
|
|
266
|
+
summary: "A review of a place with a three-step rating; its address card opens the place on Google Maps, and its location byline opens the site's map page when it has one.",
|
|
267
267
|
body: "Everything after the front matter is the review body.",
|
|
268
268
|
markdownExample: PLACE_REVIEW_EXAMPLE,
|
|
269
269
|
documentExample: documentExample(
|
|
@@ -284,20 +284,14 @@ const TYPE_DETAILS = Object.freeze({
|
|
|
284
284
|
[TEMPLATE_GENERATED]: {
|
|
285
285
|
displayName: "Generated page",
|
|
286
286
|
summary:
|
|
287
|
-
"A listing Taproot generates from the site's content:
|
|
288
|
-
+ "
|
|
287
|
+
"A listing Taproot generates from the site's content: the published archive, the map or a folder. You cannot create "
|
|
288
|
+
+ "one; you customize its title, breadcrumb, description and introduction. A folder index is a search "
|
|
289
|
+
+ "destination when it has an introduction or renders at least three cards; otherwise it carries noindex.",
|
|
289
290
|
documentExample: documentExample(
|
|
290
291
|
TEMPLATE_GENERATED,
|
|
291
292
|
{},
|
|
292
293
|
{
|
|
293
|
-
kind: "
|
|
294
|
-
tagId: "0198a3f2-7c4e-4a10-9b2d-3f6e5d4c3b2a",
|
|
295
|
-
year: 0,
|
|
296
|
-
month: 0,
|
|
297
|
-
countryCode: "",
|
|
298
|
-
regionCode: "",
|
|
299
|
-
citySlug: "",
|
|
300
|
-
categorySlug: "",
|
|
294
|
+
kind: "GENERATED_PAGE_KIND_FOLDER_INDEX",
|
|
301
295
|
customTitle: "Trail notes",
|
|
302
296
|
breadcrumbTitle: "",
|
|
303
297
|
customDescription: "Field notes from the trails I walk most often.",
|
|
@@ -305,8 +299,7 @@ const TYPE_DETAILS = Object.freeze({
|
|
|
305
299
|
},
|
|
306
300
|
),
|
|
307
301
|
dataFields: [
|
|
308
|
-
"kind
|
|
309
|
-
+ "page is, written by pull. Leave them as pulled; a push that changes them is refused (pages.generated_identity)",
|
|
302
|
+
"kind: what the page is, written by pull. Leave it as pulled; a push that changes it is refused (pages.generated_identity)",
|
|
310
303
|
"customTitle: string; empty uses the generated title (the page's reader-facing title)",
|
|
311
304
|
"breadcrumbTitle: string; empty uses the generated label (used on pages beneath this one)",
|
|
312
305
|
"customDescription: string, at most 1000 characters, about 160 shown in search results; empty uses the "
|
package/src/typed-pages.js
CHANGED
|
@@ -32,7 +32,7 @@ export const TEMPLATE_ALBUM = "album";
|
|
|
32
32
|
export const TEMPLATE_PLACE_REVIEW = "place-review";
|
|
33
33
|
export const TEMPLATE_LEGAL = "legal";
|
|
34
34
|
/**
|
|
35
|
-
* A page Taproot generates (
|
|
35
|
+
* A page Taproot generates (the published archive, the map or a folder). It is update-only: the
|
|
36
36
|
* owner's title, breadcrumb, description and introduction are authored, and
|
|
37
37
|
* pages push refuses to create one or to change what it is (TR01195).
|
|
38
38
|
*/
|
|
@@ -111,9 +111,18 @@ const RECIPE_HEADINGS = Object.freeze(["ingredients", "instructions"]);
|
|
|
111
111
|
|
|
112
112
|
/** The generated page kinds the site reports, as the API names them (never the unknown kind). */
|
|
113
113
|
export const GENERATED_PAGE_KINDS = Object.freeze([
|
|
114
|
+
"GENERATED_PAGE_KIND_PUBLISHED_INDEX",
|
|
115
|
+
"GENERATED_PAGE_KIND_FOLDER_INDEX",
|
|
116
|
+
"GENERATED_PAGE_KIND_MAP_INDEX",
|
|
117
|
+
]);
|
|
118
|
+
/**
|
|
119
|
+
* The kinds Taproot no longer generates (TR01308, TR01309, TR01310), which an older
|
|
120
|
+
* pull may have written. Named, so a typo or the unknown kind is refused as a bad
|
|
121
|
+
* document and is never mistaken for a page Taproot retired.
|
|
122
|
+
*/
|
|
123
|
+
export const RETIRED_GENERATED_PAGE_KINDS = Object.freeze([
|
|
114
124
|
"GENERATED_PAGE_KIND_TAG",
|
|
115
125
|
"GENERATED_PAGE_KIND_TAGS_INDEX",
|
|
116
|
-
"GENERATED_PAGE_KIND_PUBLISHED_INDEX",
|
|
117
126
|
"GENERATED_PAGE_KIND_PUBLISHED_YEAR",
|
|
118
127
|
"GENERATED_PAGE_KIND_PUBLISHED_MONTH",
|
|
119
128
|
"GENERATED_PAGE_KIND_PLACES_INDEX",
|
|
@@ -121,18 +130,10 @@ export const GENERATED_PAGE_KINDS = Object.freeze([
|
|
|
121
130
|
"GENERATED_PAGE_KIND_PLACES_STATE",
|
|
122
131
|
"GENERATED_PAGE_KIND_PLACES_CITY",
|
|
123
132
|
"GENERATED_PAGE_KIND_PLACES_CATEGORY",
|
|
124
|
-
"GENERATED_PAGE_KIND_FOLDER_INDEX",
|
|
125
133
|
]);
|
|
126
134
|
/** What a generated page is: system-managed, so a push must send it back unchanged. */
|
|
127
135
|
export const GENERATED_IDENTITY_KEYS = Object.freeze([
|
|
128
136
|
"kind",
|
|
129
|
-
"tagId",
|
|
130
|
-
"year",
|
|
131
|
-
"month",
|
|
132
|
-
"countryCode",
|
|
133
|
-
"regionCode",
|
|
134
|
-
"citySlug",
|
|
135
|
-
"categorySlug",
|
|
136
137
|
]);
|
|
137
138
|
const GENERATED_KEYS = [
|
|
138
139
|
...GENERATED_IDENTITY_KEYS,
|
|
@@ -142,17 +143,10 @@ const GENERATED_KEYS = [
|
|
|
142
143
|
"introductionBody",
|
|
143
144
|
];
|
|
144
145
|
const GENERATED_TEXT_KEYS = [
|
|
145
|
-
"tagId",
|
|
146
|
-
"countryCode",
|
|
147
|
-
"regionCode",
|
|
148
|
-
"citySlug",
|
|
149
|
-
"categorySlug",
|
|
150
146
|
"customTitle",
|
|
151
147
|
"breadcrumbTitle",
|
|
152
148
|
"customDescription",
|
|
153
149
|
];
|
|
154
|
-
const GENERATED_MAXIMUM_YEAR = 9999;
|
|
155
|
-
const GENERATED_MAXIMUM_MONTH = 12;
|
|
156
150
|
const EMPTY_DOCUMENT = Object.freeze({ type: "doc", content: Object.freeze([]) });
|
|
157
151
|
|
|
158
152
|
function fail(code, message, field) {
|
|
@@ -368,13 +362,6 @@ const DATA_BUILDERS = Object.freeze({
|
|
|
368
362
|
[TEMPLATE_GENERATED]: (raw) =>
|
|
369
363
|
definedEntries([
|
|
370
364
|
["kind", raw.kind],
|
|
371
|
-
["tagId", raw.tagId ?? ""],
|
|
372
|
-
["year", raw.year ?? 0],
|
|
373
|
-
["month", raw.month ?? 0],
|
|
374
|
-
["countryCode", raw.countryCode ?? ""],
|
|
375
|
-
["regionCode", raw.regionCode ?? ""],
|
|
376
|
-
["citySlug", raw.citySlug ?? ""],
|
|
377
|
-
["categorySlug", raw.categorySlug ?? ""],
|
|
378
365
|
["customTitle", raw.customTitle ?? ""],
|
|
379
366
|
["breadcrumbTitle", raw.breadcrumbTitle ?? ""],
|
|
380
367
|
["customDescription", raw.customDescription ?? ""],
|
|
@@ -479,9 +466,6 @@ function canonicalSourceData(template, data, file) {
|
|
|
479
466
|
for (const key of GENERATED_TEXT_KEYS) {
|
|
480
467
|
if (data[key] !== undefined) requireString(data[key], file, `${path}.${key}`);
|
|
481
468
|
}
|
|
482
|
-
if (data.tagId !== undefined && data.tagId !== "") requireIdentifier(data.tagId, file, `${path}.tagId`);
|
|
483
|
-
if (data.year !== undefined) requireCount(data.year, file, `${path}.year`, GENERATED_MAXIMUM_YEAR);
|
|
484
|
-
if (data.month !== undefined) requireCount(data.month, file, `${path}.month`, GENERATED_MAXIMUM_MONTH);
|
|
485
469
|
// The API trims a description, so the length that counts is the trimmed one.
|
|
486
470
|
const customDescription = data.customDescription?.trim();
|
|
487
471
|
requireDescriptionLength(customDescription, `${file}:${path}.customDescription`);
|
|
@@ -658,8 +642,6 @@ const PAGE_PROJECTIONS = Object.freeze({
|
|
|
658
642
|
? DATA_BUILDERS[TEMPLATE_GENERATED]({
|
|
659
643
|
kind: raw.kind,
|
|
660
644
|
...Object.fromEntries(GENERATED_TEXT_KEYS.map((key) => [key, typeof raw[key] === "string" ? raw[key] : ""])),
|
|
661
|
-
year: Number.isSafeInteger(raw.year) ? raw.year : 0,
|
|
662
|
-
month: Number.isSafeInteger(raw.month) ? raw.month : 0,
|
|
663
645
|
introductionBody: presentIntroduction(raw.introductionBody),
|
|
664
646
|
})
|
|
665
647
|
: undefined,
|
package/src/verbs/pages-push.js
CHANGED
|
@@ -51,6 +51,7 @@ import {
|
|
|
51
51
|
FRONT_MATTER_KEYS,
|
|
52
52
|
PAGE_TEMPLATES,
|
|
53
53
|
requireTemplateName,
|
|
54
|
+
RETIRED_GENERATED_PAGE_KINDS,
|
|
54
55
|
sentDisplayDate,
|
|
55
56
|
TEMPLATE_FREE_FORM,
|
|
56
57
|
TEMPLATE_GENERATED,
|
|
@@ -486,6 +487,20 @@ function peekJsonTemplate(text) {
|
|
|
486
487
|
}
|
|
487
488
|
}
|
|
488
489
|
|
|
490
|
+
/**
|
|
491
|
+
* Whether a generated-page source names a kind Taproot retired (TR01308). The
|
|
492
|
+
* site deletes such a page on its next synchronization, but until then it is still listed,
|
|
493
|
+
* so a pulled source of that kind is judged here, before its document is read.
|
|
494
|
+
*/
|
|
495
|
+
function isRetiredGeneratedSource(source) {
|
|
496
|
+
if (source.template !== TEMPLATE_GENERATED || typeof source.text !== "string") return false;
|
|
497
|
+
try {
|
|
498
|
+
return RETIRED_GENERATED_PAGE_KINDS.includes(JSON.parse(source.text)?.data?.kind);
|
|
499
|
+
} catch {
|
|
500
|
+
return false;
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
|
|
489
504
|
/**
|
|
490
505
|
* Reads one editable page source and resolves the metadata that says *which
|
|
491
506
|
* page it is* — no Markdown conversion, no media resolution, no document
|
|
@@ -1293,7 +1308,6 @@ export async function planPages({
|
|
|
1293
1308
|
let observedRecord;
|
|
1294
1309
|
let legalRevisionGuard;
|
|
1295
1310
|
requireCompletePageMetadata(source);
|
|
1296
|
-
const document_ = await convertWorkspacePageSource({ source, mediaManifest, content });
|
|
1297
1311
|
|
|
1298
1312
|
const target = manifestEntry?.pageId !== undefined
|
|
1299
1313
|
? liveById.get(manifestEntry.pageId)
|
|
@@ -1307,21 +1321,26 @@ export async function planPages({
|
|
|
1307
1321
|
file,
|
|
1308
1322
|
);
|
|
1309
1323
|
}
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1324
|
+
// A generated source the site no longer has, or whose kind Taproot retired
|
|
1325
|
+
// (TR01308; the site deletes the page on its next synchronization), is
|
|
1326
|
+
// judged before its document is read: it is reported stale like any
|
|
1327
|
+
// other and never refused as an unknown kind that blocks the push.
|
|
1328
|
+
const generated = source.template === TEMPLATE_GENERATED;
|
|
1329
|
+
const retiredKind = generated && isRetiredGeneratedSource(source);
|
|
1330
|
+
if (generated && (target === undefined || retiredKind)) {
|
|
1315
1331
|
if (targeted) {
|
|
1316
1332
|
throw documentError(
|
|
1317
1333
|
"pages.generated_create",
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1334
|
+
retiredKind
|
|
1335
|
+
? `'${file}' is the source of a generated page of a kind Taproot no longer publishes, and a push `
|
|
1336
|
+
+ "cannot send one. Delete the file."
|
|
1337
|
+
: `'${file}' is a generated-page source, but the site has no generated page for it. Taproot creates `
|
|
1338
|
+
+ "generated pages from the site's content; a push cannot create one. If the page no longer exists, "
|
|
1339
|
+
+ "delete this file and run 'taproot-site pull'.",
|
|
1321
1340
|
file,
|
|
1322
1341
|
);
|
|
1323
1342
|
}
|
|
1324
|
-
// The site retired this page (its
|
|
1343
|
+
// The site retired this page (its folder or its kind is gone),
|
|
1325
1344
|
// which is Taproot's doing, not a mistake in the workspace: it cannot
|
|
1326
1345
|
// block the pages that do exist.
|
|
1327
1346
|
staleGenerated.push({ file, path: displayPagePath(pagePath) });
|
|
@@ -1331,6 +1350,10 @@ export async function planPages({
|
|
|
1331
1350
|
);
|
|
1332
1351
|
return;
|
|
1333
1352
|
}
|
|
1353
|
+
const document_ = await convertWorkspacePageSource({ source, mediaManifest, content });
|
|
1354
|
+
if (documentTemplate(document_) === TEMPLATE_LEGAL && target === undefined && manifestEntry?.pageId !== undefined) {
|
|
1355
|
+
throw documentError("pages.legal_missing", `'${file}' tracks a legal page that the site no longer lists. Pull and reconcile it before creating another page.`, file);
|
|
1356
|
+
}
|
|
1334
1357
|
// The file-keyed entry is gone whenever the file was renamed or replaced —
|
|
1335
1358
|
// which is exactly what the pull, delete the `.pm.json`, author a `.md`
|
|
1336
1359
|
// flow does. Falling back through the page's own identity keeps the
|
|
@@ -1371,7 +1394,7 @@ export async function planPages({
|
|
|
1371
1394
|
&& targetEntry.file === file
|
|
1372
1395
|
&& targetEntry.sourceFormat === source.sourceFormat;
|
|
1373
1396
|
// An untouched generated page is not this push's to judge. Taproot
|
|
1374
|
-
// moves its revision and its path on its own (a
|
|
1397
|
+
// moves its revision and its path on its own (a folder that
|
|
1375
1398
|
// gained a post), and refusing the whole push for a page nothing here
|
|
1376
1399
|
// sends would make one renamed page block every other edit.
|
|
1377
1400
|
let untouchedGenerated = false;
|
package/src/verbs/places.js
CHANGED
|
@@ -83,8 +83,8 @@ export async function placesSelect(invocation) {
|
|
|
83
83
|
*
|
|
84
84
|
* A place's category is shared by every site, and Google's types do not always
|
|
85
85
|
* give the one an owner wants. The override is the site's alone: its place
|
|
86
|
-
* reviews
|
|
87
|
-
*
|
|
86
|
+
* reviews use it from the next deployment, and the platform-wide place is
|
|
87
|
+
* never changed. The server owns the closed list, so
|
|
88
88
|
* the CLI sends the name as given and lets the server check it.
|
|
89
89
|
*/
|
|
90
90
|
export async function placesCategorySet(invocation) {
|
package/src/verbs/pull.js
CHANGED
|
@@ -728,7 +728,7 @@ export async function pull(invocation) {
|
|
|
728
728
|
registeredFiles.add(record.file);
|
|
729
729
|
}
|
|
730
730
|
const tracked = await resolveTrackedSources(config.workspaceDir, authorable, registry, onProgress);
|
|
731
|
-
// A generated page the site retired (its
|
|
731
|
+
// A generated page the site retired (its folder or its kind is
|
|
732
732
|
// gone) leaves its pulled file behind: authored content, so pull will not
|
|
733
733
|
// delete it, and settles it before naming any page's file so that a page
|
|
734
734
|
// created since at the same path never writes over it. It is recorded
|