@taprootio/site-authoring 0.14.1 → 0.16.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 +181 -16
- package/examples/riverbend-wellness/.taproot-site-media.json +23 -0
- package/examples/riverbend-wellness/pages/index.md +1 -17
- package/package.json +2 -2
- package/src/api.js +123 -1
- package/src/authors-contract.js +170 -0
- package/src/cli.js +355 -30
- package/src/constants.js +9 -0
- package/src/content/components.js +64 -6
- package/src/content/free-form-sections.js +21 -4
- package/src/content/link-warnings.js +109 -0
- package/src/content/markdown.js +161 -26
- package/src/content/tiptap-prosemirror.ts +66 -8
- package/src/content/validate-document.js +30 -6
- package/src/content/vocabulary.js +26 -0
- package/src/errors.js +22 -0
- package/src/espalier-shared.js +11 -0
- package/src/fixture-init.js +55 -3
- package/src/font-catalog.js +87 -0
- package/src/footer-contract.js +1 -1
- package/src/output.js +19 -1
- package/src/page-metadata.js +92 -0
- package/src/presentation-reference.js +21 -7
- package/src/problems.js +116 -0
- package/src/reference-help.js +157 -41
- package/src/session.js +13 -0
- package/src/staging-check.js +62 -0
- package/src/theme-proposals.js +393 -0
- package/src/theme-validation.js +31 -10
- package/src/typed-page-reference.js +97 -21
- package/src/typed-pages.js +196 -13
- package/src/verbs/approve.js +63 -14
- package/src/verbs/authors.js +167 -0
- package/src/verbs/deploy.js +5 -28
- package/src/verbs/index.js +22 -0
- package/src/verbs/media-upload.js +127 -30
- package/src/verbs/nav-push.js +7 -0
- package/src/verbs/pages-meta-set.js +303 -0
- package/src/verbs/pages-push.js +814 -224
- package/src/verbs/places.js +61 -0
- package/src/verbs/plan.js +595 -0
- package/src/verbs/pull.js +255 -20
- package/src/verbs/redirects-check.js +4 -10
- package/src/verbs/theme-push.js +68 -36
- package/src/verbs/validate.js +309 -217
- package/src/video-prepare.js +48 -12
- package/src/workspace-check.js +199 -0
- package/src/workspace-ledger.js +173 -0
- package/src/workspace.js +105 -10
package/README.md
CHANGED
|
@@ -119,8 +119,20 @@ dark logo on the rendered dark header at desktop and compact/mobile widths;
|
|
|
119
119
|
transparent canvas pixels do not count as contrast. Supply separate
|
|
120
120
|
scheme-specific assets when one visible mark cannot satisfy both surfaces.
|
|
121
121
|
|
|
122
|
+
`taproot-site validate` checks the pulled workspace `taproot-site.json` names,
|
|
123
|
+
with no credential, no network, and no write: every page source (metadata,
|
|
124
|
+
Markdown conversion, media references, content vocabulary, section contexts),
|
|
125
|
+
page paths and templates against the pages the pull recorded, navigation,
|
|
126
|
+
redirects, forms, and the complete theme, appearance, header, brand, and
|
|
127
|
+
footer. Navigation to a generated page (a section, tag or place page) resolves
|
|
128
|
+
against the pages the pull recorded. It reports every problem at once in
|
|
129
|
+
`error.problems`, each with its file, code, and field, instead of one per run.
|
|
130
|
+
Warnings never refuse: `linkWarnings` names a link whose address is only
|
|
131
|
+
`http(s)://` plus its own text (`content.link_autolinked`, left by older editor
|
|
132
|
+
autolinking), and `pages push` reports the same for the pages it sends.
|
|
133
|
+
|
|
122
134
|
`taproot-site validate <fixture-directory>` checks a complete offline fixture
|
|
123
|
-
|
|
135
|
+
the same way: a directory laid out like a
|
|
124
136
|
pulled workspace — `pages/`, `nav.json`, `redirects.json`, and the four
|
|
125
137
|
`settings/` documents —
|
|
126
138
|
whose `manifest.fixture.json` binds deterministic page, resource, image, and
|
|
@@ -143,9 +155,10 @@ taproot-site validate "$(npm root --global)/@taprootio/site-authoring/examples/r
|
|
|
143
155
|
The fixture is a fictional wellness studio — `example.test` hostnames, a
|
|
144
156
|
reserved `555-01xx` telephone number, an invented street and town — with two
|
|
145
157
|
free-form pages and one each of the article, recipe, album and place-review
|
|
146
|
-
templates, a three-item navigation tree,
|
|
147
|
-
|
|
148
|
-
it
|
|
158
|
+
templates, a three-item navigation tree, all four settings documents, and a
|
|
159
|
+
`.taproot-site-media.json` that maps its one media path to a fixture image id.
|
|
160
|
+
Nothing in it describes a real business. Copy the directory somewhere writable
|
|
161
|
+
before you edit it; `validate` never writes to the fixture it reads.
|
|
149
162
|
|
|
150
163
|
## Authorize
|
|
151
164
|
|
|
@@ -285,14 +298,15 @@ each step names the reference it relies on.
|
|
|
285
298
|
3. **Edit both schemes and the appearance.** `help theme` lists every field,
|
|
286
299
|
the role slots, how anchor references resolve and what wins when a pin and
|
|
287
300
|
a role disagree; `help appearance` covers the default scheme, header, logos
|
|
288
|
-
and favicon
|
|
301
|
+
and favicon; `help fonts` lists the font families, by category, that the
|
|
302
|
+
font fields can name.
|
|
289
303
|
4. **Prepare media locally.** Raster files only (PNG, JPEG, GIF, WebP),
|
|
290
304
|
relative to the workspace root; make the transparent logo, its `@2x` copy,
|
|
291
305
|
a dark-scheme logo when needed and a square favicon with your own tools,
|
|
292
306
|
upload them with `media upload`, then assign `lightLogoId`, `darkLogoId`
|
|
293
307
|
and `brand.faviconId` from the returned ids. There is no recolor, crop or
|
|
294
308
|
SVG upload command.
|
|
295
|
-
5. **Validate, preview, then push.** `validate`
|
|
309
|
+
5. **Validate, preview, then push.** `validate` checks the whole workspace offline;
|
|
296
310
|
`theme push --dry-run` reads the site and lists the JSON paths at which
|
|
297
311
|
each settings file differs from it, and whether the baseline `pull`
|
|
298
312
|
recorded is still current; `theme push` saves both themes, the appearance
|
|
@@ -350,13 +364,127 @@ it is created: a source for another template is refused with
|
|
|
350
364
|
`pages.template_immutable`. `taproot-site help page <type>` gives each
|
|
351
365
|
template's fields and one worked example.
|
|
352
366
|
|
|
367
|
+
A `.pm.json` page's title, path, description and author live in the workspace
|
|
368
|
+
manifest rather than the file. `pages meta set <path> --title <text>
|
|
369
|
+
--description <text>` changes the title or description there (a Markdown page
|
|
370
|
+
keeps them in its front matter; `--author` is described under *Page authors*); `pull` keeps that edit until `pages push`
|
|
371
|
+
sends it, and a site edit to the same page is a `pages.pull_conflict`. A
|
|
372
|
+
description over 160 characters is a warning (`descriptionWarnings`), because
|
|
373
|
+
search results cut it off; over 1000 is refused with
|
|
374
|
+
`pages.description_too_long`, by the CLI and by Taproot.
|
|
375
|
+
|
|
376
|
+
### Page authors
|
|
377
|
+
|
|
378
|
+
Pages are authorless by default: a page you create with the CLI publishes with
|
|
379
|
+
no byline unless you name an author, and `pages push` lists the pages it created
|
|
380
|
+
that way in `pages.authorless`, with how to credit them. An author is one of two
|
|
381
|
+
kinds of person, named by one string:
|
|
382
|
+
|
|
383
|
+
- a **site author without a Taproot account** — one of an imported blog's
|
|
384
|
+
authors, say — named by handle (`jane-doe`: lowercase letters, digits and
|
|
385
|
+
single hyphens, at most 64 characters, fixed when the author is created). A
|
|
386
|
+
site author's name appears on the byline and in the page's structured data;
|
|
387
|
+
there is no profile link or follow button, because there is no account;
|
|
388
|
+
- a **site member who can create pages**, named by email address.
|
|
389
|
+
|
|
390
|
+
`authors list` prints both and rewrites `authors.json` (`pull` writes it too).
|
|
391
|
+
`authors add jane-doe --name "Jane Doe" [--email jane@example.com]` creates a
|
|
392
|
+
site author; `--email` is kept private (never published, never listed) so the
|
|
393
|
+
person can be linked to a Taproot account later. Adding a handle the site
|
|
394
|
+
already has under the same name succeeds and reports `existing: true`, so a
|
|
395
|
+
script can repeat itself; a different name for a taken handle is refused
|
|
396
|
+
(`authors.handle_taken`), naming the one the site has. `authors.json` holds the
|
|
397
|
+
members' email addresses, so keep it out of a public repository.
|
|
398
|
+
|
|
399
|
+
A page names its author with `author: jane-doe` in its front matter (any
|
|
400
|
+
template), or, for a `.pm.json` page, `pages meta set <path> --author jane-doe`.
|
|
401
|
+
Only the CLI assigns authors, and only to a page that has none; people editing
|
|
402
|
+
in the app are never asked to name one. `authors.json` is as old as the last
|
|
403
|
+
`pull` or `authors list`, so a name missing from it is not proof the person does
|
|
404
|
+
not exist. `validate` and `pages meta set` run offline: they refuse text that
|
|
405
|
+
is neither a handle nor an address (`pages.author_invalid`) and otherwise only
|
|
406
|
+
warn (`pages.author_unverified`, in `authorWarnings`) about a name `authors.json`
|
|
407
|
+
lacks. `pages push` and `plan` settle it online: on a miss they read the site's
|
|
408
|
+
current list once (`pages push` rewrites `authors.json` with it) and refuse
|
|
409
|
+
(`pages.author_unknown`) only a name the site really lacks. A member who cannot
|
|
410
|
+
create pages is `pages.author_unknown` as well, with the same wording.
|
|
411
|
+
|
|
412
|
+
The CLI only fills a page that has no author: a page that already has a
|
|
413
|
+
different one is refused (`pages.author_conflict`), by the CLI from what the
|
|
414
|
+
last pull recorded and by Taproot, and naming the author a page already has
|
|
415
|
+
changes nothing. The refusal says how to settle it: for a `.pm.json` page,
|
|
416
|
+
`pages meta set <path> --author <the site's author>` matches the site, and
|
|
417
|
+
`pages meta set <path> --author ""` drops an author that was never sent (it
|
|
418
|
+
never asks the site to remove one, and is refused once the site holds an
|
|
419
|
+
author); for a Markdown page, fix the `author:` line. `pull` keeps a pending
|
|
420
|
+
author the site now disagrees with and says so. The author is recorded on the
|
|
421
|
+
page's manifest entry (`author`, and `baseline.author` for what the site
|
|
422
|
+
held), so a re-push never resends it. The site names a page's author in
|
|
423
|
+
listings only while that person can still be named: a former member's page, or
|
|
424
|
+
a member who can no longer create pages, reports none, and a push that names
|
|
425
|
+
someone else on it is refused as a conflict.
|
|
426
|
+
|
|
427
|
+
Pages Taproot generates from the site's content — a tag, the tags index, a
|
|
428
|
+
published year or month, a place archive, a folder — are update-only sources.
|
|
429
|
+
`pull` writes each as `pages/<name>.pm.json` with `"template": "generated"`:
|
|
430
|
+
`data` holds what the page is (`kind`, `tagId`, `year`, `month`, `countryCode`,
|
|
431
|
+
`regionCode`, `citySlug`, `categorySlug`, which you leave as pulled) and the four
|
|
432
|
+
things that are yours to author: `customTitle`, `breadcrumbTitle`,
|
|
433
|
+
`customDescription` and `introductionBody`. An empty string means "use the
|
|
434
|
+
generated default"; the description then falls back to the introduction's first
|
|
435
|
+
paragraph, else the system default. The manifest keeps the title and description
|
|
436
|
+
the site reports, for reference. `pages meta set <path> --title/--description`
|
|
437
|
+
edits `customTitle` and `customDescription` in the source (an empty value
|
|
438
|
+
clears it), and `pages push` sends the page back as an unapproved draft. The
|
|
439
|
+
title and description the site stores are derived from `customTitle`,
|
|
440
|
+
`customDescription` and `introductionBody`, so what a push sends for them is
|
|
441
|
+
ignored. A push refuses to create a generated page (`pages.generated_create`),
|
|
442
|
+
move one (`pages.generated_move`) or change its kind or identity
|
|
443
|
+
(`pages.generated_identity`, also when the manifest has no record of what the
|
|
444
|
+
file was pulled as: run `pull`); there is no Markdown form
|
|
445
|
+
(`pages.generated_markdown`). A generated page you did not edit never blocks a
|
|
446
|
+
push of the whole workspace, even when Taproot moved it since the pull (a tag was
|
|
447
|
+
renamed). If the site no longer has the page (its tag, month or folder is gone),
|
|
448
|
+
a whole-workspace push reports the file under `pages.staleGeneratedSources` and
|
|
449
|
+
sends nothing for it, and `pull` says so; delete the file. Naming that path in
|
|
450
|
+
`pages push <page-path>` is refused with `pages.generated_create`. A description over 160 characters is warned about,
|
|
451
|
+
since search results cut it off, and one over 1000 is refused.
|
|
452
|
+
|
|
353
453
|
`pages push [page-path...]` validates and sends the selected pages, or every
|
|
354
|
-
workspace page when none is named; the homepage is addressed as `/`.
|
|
454
|
+
workspace page when none is named; the homepage is addressed as `/`. Every
|
|
455
|
+
selected page is checked before anything is sent, and a refusal lists every
|
|
456
|
+
problem in `error.problems`. `pages push --dry-run` runs the same checks
|
|
457
|
+
against the live site, lists which pages would be created, updated, or left
|
|
458
|
+
unchanged, and sends nothing. It fails
|
|
355
459
|
closed with `pages.push_conflict` when a page's stored-state revision has
|
|
356
460
|
moved since this workspace last reconciled with it, naming both revisions;
|
|
357
461
|
`pull` first, or push after a refused pull has shown you the site's version.
|
|
358
|
-
|
|
359
|
-
|
|
462
|
+
|
|
463
|
+
`places search <name and city>` asks Google Places for the place a review
|
|
464
|
+
names and reports predictions with a session token; `places select
|
|
465
|
+
<googlePlaceId> <sessionToken>` records the match as a Taproot place and
|
|
466
|
+
reports the `placeId` a place review's front matter needs. An article or an
|
|
467
|
+
album can name its place the same way with `place:`.
|
|
468
|
+
|
|
469
|
+
`plan` checks the whole workspace against the live site — pages, the media
|
|
470
|
+
they reference and the images the settings name, the footer and the pages it
|
|
471
|
+
links, the theme, navigation — and orders what is left
|
|
472
|
+
to send: media upload, pages push, footer push (theme push refuses unsaved
|
|
473
|
+
footer content), theme push, nav push, each marked ready, blocked, or nothing
|
|
474
|
+
to do. It writes nothing and reports a `planHash`. `apply --plan <planHash>`
|
|
475
|
+
plans again, refuses with `apply.plan_stale` if the workspace or the site
|
|
476
|
+
moved, and otherwise runs the steps through the ordinary push verbs. While
|
|
477
|
+
they run, a workspace file read differently from the plan (or not read by it)
|
|
478
|
+
stops the step before it writes, naming the file, and so does a page revision
|
|
479
|
+
or the site's navigation that moved. Steps are
|
|
480
|
+
not atomic together: a failure reports in `error.completedWrites` which steps
|
|
481
|
+
completed, which failed, and which did not run, and the next `plan` shows
|
|
482
|
+
finished steps as nothing to do. Redirects, forms, approval, and deployment
|
|
483
|
+
stay separate commands.
|
|
484
|
+
`media upload [path...]` uploads raster files and records each one in
|
|
485
|
+
`.taproot-site-media.json`; Markdown then names an image by its path (such as
|
|
486
|
+
`media/photo.webp`) everywhere an image goes, from body images to component and
|
|
487
|
+
section image fields. MP4, MOV, and WebM files are uploaded as videos. Taproot does not
|
|
360
488
|
encode: the command reads each file with mediabunny, rewrites an H.264 and AAC
|
|
361
489
|
file that is not an MP4 with its index first as one (copying the tracks, never
|
|
362
490
|
re-encoding), declares its codecs, and prints the server's one-line refusal as
|
|
@@ -372,7 +500,9 @@ settings, and the closed footer document. `pull` writes each scheme's
|
|
|
372
500
|
complete effective theme — the stored theme resolved over the same defaults
|
|
373
501
|
every consumer renders — so a fresh workspace validates as pulled, and it
|
|
374
502
|
keeps only authored `semanticMappings` pins (listed in
|
|
375
|
-
`explicitMappingTokens`
|
|
503
|
+
`explicitMappingTokens` at the root; a pin inside a context needs no marker)
|
|
504
|
+
because a pinned token shadows its role. Contrast and status-color warnings
|
|
505
|
+
name the nearest passing value the CLI found and checked.
|
|
376
506
|
`theme push` — and `theme push --dry-run`, which writes nothing — also runs
|
|
377
507
|
Espalier's fit report over the root and every context in both schemes and
|
|
378
508
|
prints each lint as a warning, such as a filled action that renders the
|
|
@@ -505,6 +635,37 @@ Other C0/C1 controls, U+2028/U+2029, U+FEFF, and literal `<br>` tags are rejecte
|
|
|
505
635
|
with the exact fact field path. A label is optional and stays outside the value's link. See
|
|
506
636
|
`taproot-site help page free-form` and `taproot-site help component cta`.
|
|
507
637
|
|
|
638
|
+
### Link rel in 0.15.0
|
|
639
|
+
|
|
640
|
+
A link publishes followed unless its author chooses otherwise. A link mark's
|
|
641
|
+
`rel` takes space-separated link types (`nofollow`, `sponsored`, `ugc`, `me`,
|
|
642
|
+
and the other standard types except `opener`), on any link; `target: "_blank"`
|
|
643
|
+
opens a new tab with `noopener noreferrer` added. In Markdown, write
|
|
644
|
+
`[Shop](https://shop.example){rel="sponsored nofollow" target="_blank"}`
|
|
645
|
+
(either attribute alone works). Other tokens and
|
|
646
|
+
targets are refused (`content.link_rel`, `content.link_target`). The editor's
|
|
647
|
+
old default, `noopener noreferrer nofollow`, reads as no choice. See
|
|
648
|
+
`taproot-site help page free-form`.
|
|
649
|
+
|
|
650
|
+
### Authoring feedback in 0.16.0
|
|
651
|
+
|
|
652
|
+
- `pages meta set <path> --title/--description/--author` edits a `.pm.json`
|
|
653
|
+
page's title, description or author; `pull` keeps the edit until `pages push`
|
|
654
|
+
sends it. Descriptions are warned about over 160 characters and refused over
|
|
655
|
+
1000, here and by Taproot.
|
|
656
|
+
- Generated pages (tags, archives, places, folders) pull as update-only sources
|
|
657
|
+
whose custom title, breadcrumb, description and introduction you can edit.
|
|
658
|
+
- Pages are authorless by default. `authors list`, `authors add` and
|
|
659
|
+
`authors.json` name a member who can create pages or a site author without a
|
|
660
|
+
Taproot account; `author:` front matter or `pages meta set --author` credits
|
|
661
|
+
one on a page that has none, and an author is never replaced from the CLI.
|
|
662
|
+
- `redirects check` and `deploy --staging` report `routeCheck`, `redirects` and
|
|
663
|
+
`stagingPreview` at the top level; result paths spell the home page `/`;
|
|
664
|
+
`approve` skips a page with nothing to approve instead of failing the batch.
|
|
665
|
+
- `validate` checks a real pulled workspace offline (`validate --init` keeps the
|
|
666
|
+
generated pages navigation links to), and `validate` and `pages push` warn on
|
|
667
|
+
links that look made by autolinking (`content.link_autolinked`).
|
|
668
|
+
|
|
508
669
|
### Staging verification and deployment evidence
|
|
509
670
|
|
|
510
671
|
`deploy --staging` selects the same changed pages, settings groups and navigation
|
|
@@ -527,13 +688,15 @@ only error class and known page/node/attribute labels, never renderer messages.
|
|
|
527
688
|
Staging deploys check the current redirect map through the real staging edge.
|
|
528
689
|
Run `taproot-site redirects check` to repeat once the deployment has completed; a redirect is a file inside the release, so it is live exactly when its release is served.
|
|
529
690
|
The Node CLI consumes a separate handoff, retains its short staging cookie in
|
|
530
|
-
memory, and requests each map path without following redirects.
|
|
531
|
-
|
|
691
|
+
memory, and requests each map path without following redirects. Both
|
|
692
|
+
`redirects check` and `deploy --staging` report `routeCheck` and `redirects` at
|
|
693
|
+
the top level of their result: `redirects.items` lists each path, HTTP status
|
|
694
|
+
and Location, and `redirects.verified` requires all entries to match and the
|
|
532
695
|
map revision to remain unchanged. A mismatch is evidence to investigate, not a
|
|
533
696
|
failed deployment. Checks describe the current map, not a historical release.
|
|
534
697
|
Requests have a 90-second deadline with eight concurrent requests; large JSON
|
|
535
|
-
lists have explicit truncation, prioritizing failures.
|
|
536
|
-
entry. A gate/transport failure never counts as verified.
|
|
698
|
+
lists have explicit truncation, prioritizing failures. Progress output reports
|
|
699
|
+
each entry. A gate/transport failure never counts as verified.
|
|
537
700
|
|
|
538
701
|
`stagingPreview.url` is a fresh single-use two-minute handoff minted **after**
|
|
539
702
|
checks. Opening it consumes it and establishes a five-minute staging cookie.
|
|
@@ -588,8 +751,10 @@ The destination must be new, outside the source workspace, and its parent must e
|
|
|
588
751
|
to select another source configuration. Initialization needs no credential or
|
|
589
752
|
network. It exports editable pages of every template as workspace documents with navigation,
|
|
590
753
|
redirects, settings, and version-7 appearance/footer metadata, validates the
|
|
591
|
-
result, and leaves the source unchanged.
|
|
592
|
-
|
|
754
|
+
result, and leaves the source unchanged. A metadata-only page that navigation,
|
|
755
|
+
the footer or a page links to, such as a generated section page, is kept as a
|
|
756
|
+
metadata-only entry (`initialized.linkedPages`); other metadata-only pages are
|
|
757
|
+
left out (`initialized.skippedPages`).
|
|
593
758
|
|
|
594
759
|
Exported UUIDs and HTTP(S) origins are deterministic fixture values under
|
|
595
760
|
`example.test`; URL credentials, query strings and fragments are removed.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"mediaManifestVersion": 2,
|
|
3
|
+
"siteId": "a0000000-0000-4000-8000-000000000001",
|
|
4
|
+
"media": {
|
|
5
|
+
"media/riverbend-studio.webp": {
|
|
6
|
+
"imageId": "a0000000-0000-4000-8000-0000000000a2",
|
|
7
|
+
"src": "https://static.example.test/site/img/riverbend-studio-1280.webp",
|
|
8
|
+
"urls": [
|
|
9
|
+
{
|
|
10
|
+
"minWidth": 640,
|
|
11
|
+
"url": "https://static.example.test/site/img/riverbend-studio-640.webp"
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
"minWidth": 1280,
|
|
15
|
+
"url": "https://static.example.test/site/img/riverbend-studio-1280.webp"
|
|
16
|
+
}
|
|
17
|
+
],
|
|
18
|
+
"width": 1600,
|
|
19
|
+
"height": 1067,
|
|
20
|
+
"alt": "An example photograph placeholder for the fixture studio"
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -19,23 +19,7 @@ description: An example home page for the offline authoring fixture.
|
|
|
19
19
|
"url": "https://schedule.example.test/riverbend"
|
|
20
20
|
},
|
|
21
21
|
"alignment": "start",
|
|
22
|
-
"media":
|
|
23
|
-
"imageId": "a0000000-0000-4000-8000-0000000000a2",
|
|
24
|
-
"src": "https://static.example.test/site/img/riverbend-studio-1280.webp",
|
|
25
|
-
"urls": [
|
|
26
|
-
{
|
|
27
|
-
"minWidth": 640,
|
|
28
|
-
"url": "https://static.example.test/site/img/riverbend-studio-640.webp"
|
|
29
|
-
},
|
|
30
|
-
{
|
|
31
|
-
"minWidth": 1280,
|
|
32
|
-
"url": "https://static.example.test/site/img/riverbend-studio-1280.webp"
|
|
33
|
-
}
|
|
34
|
-
],
|
|
35
|
-
"width": 1600,
|
|
36
|
-
"height": 1067,
|
|
37
|
-
"alt": "An example photograph placeholder for the fixture studio"
|
|
38
|
-
},
|
|
22
|
+
"media": "media/riverbend-studio.webp",
|
|
39
23
|
"mediaArrangement": "split",
|
|
40
24
|
"mediaPosition": "after",
|
|
41
25
|
"mediaWidth": "equal"
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@taprootio/site-authoring",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.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",
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"README.md"
|
|
29
29
|
],
|
|
30
30
|
"dependencies": {
|
|
31
|
-
"@taprootio/espalier": "
|
|
31
|
+
"@taprootio/espalier": "6.2.0",
|
|
32
32
|
"mediabunny": "1.61.1"
|
|
33
33
|
},
|
|
34
34
|
"scripts": {
|
package/src/api.js
CHANGED
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
// would turn a successful mint into an orphaned key.
|
|
24
24
|
import { KEY_PREFIX } from "./credentials.js";
|
|
25
25
|
import { isCanonicalUuid, normalizePreviewDiagnostic, sanitizeDiagnostic, SiteAuthoringError } from "./errors.js";
|
|
26
|
+
import { normalizeAuthorListing, parseAuthorReference } from "./authors-contract.js";
|
|
26
27
|
import { docsPublicationModeFromWire, siteKindFromWire, surfaceFromWire } from "./surface.js";
|
|
27
28
|
import {
|
|
28
29
|
DEFAULT_REDIRECT_STATUS,
|
|
@@ -178,7 +179,8 @@ const AUTHORING_PREVIEW_FIELD_ERRORS = Object.freeze({
|
|
|
178
179
|
],
|
|
179
180
|
[AUTHORING_PREVIEW_FIELDS.staging]: [
|
|
180
181
|
"preview.staging_unavailable",
|
|
181
|
-
"The site
|
|
182
|
+
"The site's staging address is not ready for previews. If the site has never deployed to staging, run "
|
|
183
|
+
+ "'deploy --staging' once; otherwise check its staging host with 'taproot-site status'.",
|
|
182
184
|
],
|
|
183
185
|
[AUTHORING_PREVIEW_FIELDS.expiry]: ["preview.expired", "The authoring preview has expired."],
|
|
184
186
|
[AUTHORING_PREVIEW_FIELDS.readiness]: ["preview.not_ready", "The authoring preview is not ready for a handoff."],
|
|
@@ -233,6 +235,11 @@ function requireIdentifier(value, code, field) {
|
|
|
233
235
|
return value;
|
|
234
236
|
}
|
|
235
237
|
|
|
238
|
+
/** A Google place id as Taproot stores it: the server's characters and its 256-character column. */
|
|
239
|
+
export function isGooglePlaceId(value) {
|
|
240
|
+
return typeof value === "string" && value.length <= 256 && /^[A-Za-z0-9_-]+$/u.test(value);
|
|
241
|
+
}
|
|
242
|
+
|
|
236
243
|
function requireCanonicalUuid(value, code, field) {
|
|
237
244
|
if (!isCanonicalUuid(value)) {
|
|
238
245
|
throw new SiteAuthoringError(code, `Taproot returned an invalid ${field}.`, { field });
|
|
@@ -493,6 +500,14 @@ export function normalizePageSummary(value) {
|
|
|
493
500
|
// The page's calendar date, empty when it has none. It is not part of
|
|
494
501
|
// `bodyRevision`, so a typed page's push guard compares it separately.
|
|
495
502
|
displayDate: typeof summary.displayDate === "string" ? summary.displayDate : "",
|
|
503
|
+
// The name the page's author goes by in a write: a site author's handle or
|
|
504
|
+
// a member's email address, folded. Empty for a page with no author, for a
|
|
505
|
+
// former member's page, and against a Taproot that predates authors
|
|
506
|
+
// (TR01196). Never the author's display name, which is not a reference.
|
|
507
|
+
authorRef: typeof summary.authorRef === "string" ? parseAuthorReference(summary.authorRef)?.value ?? "" : "",
|
|
508
|
+
// Whether the page credits anyone at all, which an empty authorRef cannot
|
|
509
|
+
// say: it is also empty for an author this credential may not name.
|
|
510
|
+
hasAuthor: typeof summary.authorDisplayName === "string" && summary.authorDisplayName.trim() !== "",
|
|
496
511
|
};
|
|
497
512
|
}
|
|
498
513
|
|
|
@@ -539,6 +554,62 @@ export async function updatePage(client, pageId, body) {
|
|
|
539
554
|
);
|
|
540
555
|
}
|
|
541
556
|
|
|
557
|
+
/**
|
|
558
|
+
* Gives the site's two author refusals the stable codes the CLI documents
|
|
559
|
+
* (TR01196). The site answers an unknown name and one that cannot be named with
|
|
560
|
+
* the same field and wording on purpose; this keeps them one code too.
|
|
561
|
+
*/
|
|
562
|
+
export function translateAuthorRefusal(error, file) {
|
|
563
|
+
if (!(error instanceof ApiError)) return error;
|
|
564
|
+
for (const [field, code, fallback] of [
|
|
565
|
+
["AuthorUnknown", "pages.author_unknown", "No site author or member who can create pages has that name."],
|
|
566
|
+
["AuthorConflict", "pages.author_conflict", "The page already has a different author, which is never replaced."],
|
|
567
|
+
]) {
|
|
568
|
+
if (error.hasField(field)) {
|
|
569
|
+
const detail = sanitizeDiagnostic(error.descriptionFor(field) ?? fallback, fallback);
|
|
570
|
+
// The workspace only learns who the site credits from a pull, and a
|
|
571
|
+
// conflict means someone gave the page an author after the last one.
|
|
572
|
+
const hint = field === "AuthorConflict"
|
|
573
|
+
? " Run 'taproot-site pull' to learn who the site credits, then match it with 'pages meta set <path> --author "
|
|
574
|
+
+ "<author>' (a .pm.json page) or the author: front matter line (Markdown)."
|
|
575
|
+
: "";
|
|
576
|
+
return new SiteAuthoringError(code, `'${file}': ${detail}${hint}`, { field: file });
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
return error;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
/**
|
|
583
|
+
* Reads who a page can be credited to: the site's authors without an account
|
|
584
|
+
* and the members who can create pages (TR01196).
|
|
585
|
+
*/
|
|
586
|
+
export async function listSiteAuthors(client, siteId) {
|
|
587
|
+
return normalizeAuthorListing(
|
|
588
|
+
requireObject(await client.request(sitePath(siteId, "authors")), "api.authors_contract", "site authors"),
|
|
589
|
+
);
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
/** Creates a site author. The site folds the handle and the address. */
|
|
593
|
+
export async function createSiteAuthor(client, siteId, { handle, displayName, email }) {
|
|
594
|
+
const created = requireObject(
|
|
595
|
+
await client.request(sitePath(siteId, "authors"), {
|
|
596
|
+
method: "POST",
|
|
597
|
+
body: { handle, displayName, ...(email === undefined ? {} : { email }) },
|
|
598
|
+
}),
|
|
599
|
+
"api.authors_contract",
|
|
600
|
+
"site author",
|
|
601
|
+
);
|
|
602
|
+
const listing = normalizeAuthorListing({ authors: [created], members: [] });
|
|
603
|
+
if (listing.authors.length !== 1) {
|
|
604
|
+
throw new SiteAuthoringError(
|
|
605
|
+
"api.authors_contract",
|
|
606
|
+
"Taproot returned a site author this CLI could not read.",
|
|
607
|
+
{ field: "handle" },
|
|
608
|
+
);
|
|
609
|
+
}
|
|
610
|
+
return listing.authors[0];
|
|
611
|
+
}
|
|
612
|
+
|
|
542
613
|
export async function publishDrafts(client, pageIds) {
|
|
543
614
|
const response = requireObject(
|
|
544
615
|
await client.request("v1/pages/publish_drafts", { method: "POST", body: { pageIds } }),
|
|
@@ -874,6 +945,57 @@ export async function listVideos(client, siteId, requestOptions = {}) {
|
|
|
874
945
|
return { videos, truncated: true };
|
|
875
946
|
}
|
|
876
947
|
|
|
948
|
+
/**
|
|
949
|
+
* Google Places autocomplete for a place review (TR01003). A key searches for
|
|
950
|
+
* the site it was issued for; every search and the select that follows share
|
|
951
|
+
* `sessionToken`, which is what groups them into one billed session.
|
|
952
|
+
*/
|
|
953
|
+
export async function searchPlaces(client, siteId, { text: searchText, sessionToken }) {
|
|
954
|
+
const search = query([["query", searchText], ["siteId", siteId], ["sessionToken", sessionToken]]);
|
|
955
|
+
const response = requireObject(
|
|
956
|
+
await client.request(`v1/places/search${search}`),
|
|
957
|
+
"api.place_contract",
|
|
958
|
+
"place search",
|
|
959
|
+
);
|
|
960
|
+
return (Array.isArray(response.predictions) ? response.predictions : []).map((value) => {
|
|
961
|
+
const prediction = requireObject(value, "api.place_contract", "place prediction");
|
|
962
|
+
return {
|
|
963
|
+
googlePlaceId: requireGooglePlaceId(prediction.googlePlaceId),
|
|
964
|
+
name: text(prediction.name),
|
|
965
|
+
formattedAddress: text(prediction.formattedAddress),
|
|
966
|
+
};
|
|
967
|
+
});
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
function requireGooglePlaceId(value) {
|
|
971
|
+
if (!isGooglePlaceId(value)) {
|
|
972
|
+
throw new SiteAuthoringError("api.place_contract", "Taproot returned an invalid googlePlaceId.", { field: "googlePlaceId" });
|
|
973
|
+
}
|
|
974
|
+
return value;
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
/** Records a prediction as a Taproot place, ending the search's billed session, and returns its id. */
|
|
978
|
+
export async function selectPlace(client, siteId, { googlePlaceId, sessionToken }) {
|
|
979
|
+
const place = requireObject(
|
|
980
|
+
await client.request("v1/places/select", {
|
|
981
|
+
method: "POST",
|
|
982
|
+
body: { siteId, googlePlaceId, sessionToken },
|
|
983
|
+
}),
|
|
984
|
+
"api.place_contract",
|
|
985
|
+
"selected place",
|
|
986
|
+
);
|
|
987
|
+
return {
|
|
988
|
+
// Any UUID version: the server mints place ids.
|
|
989
|
+
placeId: requireIdentifier(place.id, "api.place_contract", "id"),
|
|
990
|
+
name: text(place.name),
|
|
991
|
+
category: text(place.category),
|
|
992
|
+
address: text(place.address),
|
|
993
|
+
city: text(place.city),
|
|
994
|
+
region: text(place.region),
|
|
995
|
+
country: text(place.country),
|
|
996
|
+
};
|
|
997
|
+
}
|
|
998
|
+
|
|
877
999
|
/**
|
|
878
1000
|
* Processing is observed through `ListSiteImages`; `GetImageById` is
|
|
879
1001
|
* session-only under TR00602's read list, so a key-authorized client watches
|