@seamlet/astro 1.1.0 → 1.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,14 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.2.1
4
+
5
+ - `site.staticPaths(content)` is what Astro's `GetStaticPaths` takes: a static site's `[...page]` route may write `satisfies GetStaticPaths` and read its props with `InferGetStaticPropsType`. `StaticPage` and `RenderedPage` became type aliases, and `staticPaths` returns a mutable array.
6
+
7
+ ## 1.2.0
8
+
9
+ - `productFields()`: the fields of a product of a catalog, for the record of a collection, with its address, photos, a short and a whole description, the price with a word about it, characteristics as pairs of a name and a value, the switch that hides it and what search engines show.
10
+ - `recordPages(…, { shows })` leaves the pages of some records off the site: `site.pageOf` and `site.staticPaths` leave them out, `pathOf` gives them no address, and the preview shows them (`site.previewPageOf`), so the owner prepares a hidden record in the editor.
11
+
3
12
  ## 1.1.0
4
13
 
5
14
  - Pages of records: `recordPages(schema, { collection, path: "/menu/[record]" })` gives every record of a collection a page, rendered by one component with `content`, `record` and `index`; `defineSeamletSite` takes them as `recordPages`, the site's description tells the CMS about them, and the preview shows them, a record the editor has just added too.
package/README.md CHANGED
@@ -230,6 +230,33 @@ Sets of fields almost every site has, edited the same way everywhere; they are p
230
230
 
231
231
  Each takes `{ label }` to name it otherwise.
232
232
 
233
+ `productFields()` is the fields of a product of a catalog, for the record of a collection: `name`, `slug` (the address of its page), `photos` (records of one `photo`, the first is the main one), `summary` (one line for a card), `description`, `price` with `priceNote` («от», «за м²»), `characteristics` (records of a `name` and a `value`, so the owner adds a property without code: «Площадь» — «36 м²»), `isHidden` and `seo`. A site adds its own fields beside them, a category for one:
234
+
235
+ ```ts
236
+ export const schema = defineSchema({
237
+ categories: collection({
238
+ label: "Категории",
239
+ itemTitle: "name",
240
+ item: { name: text({ label: "Название" }) },
241
+ }),
242
+ products: collection({
243
+ label: "Товары",
244
+ itemTitle: "name",
245
+ item: {
246
+ ...productFields(),
247
+ category: reference({ label: "Категория", collection: "categories" }),
248
+ },
249
+ }),
250
+ });
251
+
252
+ export const productPages = recordPages(schema, {
253
+ collection: "products",
254
+ path: "/catalog/[record]",
255
+ searchTexts: "seo",
256
+ shows: (product) => !product.isHidden,
257
+ });
258
+ ```
259
+
233
260
  ## Records and references
234
261
 
235
262
  Every record of a collection has an `id` of its own, which never changes when records move, so a record has no field named `id`. The CMS gives ids to records that have none (`withRecordIds`), so a site's initial content may leave them out, unless something refers to those records.
@@ -317,6 +344,8 @@ const edit = editable<Content>();
317
344
  <h1 {...edit("menu", index, "name")}>{record.name}</h1>
318
345
  ```
319
346
 
347
+ `shows` leaves the pages of some records off the site, a hidden product's for one: `site.pageOf` and `site.staticPaths` leave them out and `pathOf` gives them no address, while the preview shows them, so the owner prepares a product before showing it.
348
+
320
349
  `drinkPages.pathOf(drink)` gives the address of a record's page for a link to it: `/menu/latte`. The address is the record's `slug` when it has one, a field the owner fills in (lower-case Latin letters, digits and hyphens; the CMS spells it from the record's name), and its id otherwise, so a record has its page from the moment it is added, and keeps it when it is renamed or moved. A slug another record of the collection has, as its slug or as its id, is the issue `taken-slug`; a slug with other characters, `invalid-slug`. A collection is `catalog.products` through groups, its pages have one segment more than a page of the site (`/catalog/[record]`, never `/[record]`), a record has one page, and no page of the site may stand among the pages of records (`/menu/specials` beside `/menu/[record]`). `searchTexts` names a group of the record with a `title` and a `description` text for its page.
321
350
 
322
351
  Every record of the initial content needs an address of its own, a slug or an id, since its page is built before the CMS saves anything. The preview shows a page of a record the owner has just added, so the editor opens it before it is saved or published: under the pages of records, an address that could be an id and names no record shows a blank record, which the editor's draft replaces at once.
@@ -335,7 +364,7 @@ What was opened stays open while the page is rendered anew after every edit: a p
335
364
 
336
365
  ## What the integration adds
337
366
 
338
- - `/preview` and `/preview/<page>`: the preview the CMS's editor frames, the pages of records too (at their address or at the record's id). `GET` shows the published content; `POST` with the content as JSON shows that content without saving it, which is how the editor shows every edit at once. Only the CMS may frame it (`Content-Security-Policy: frame-ancestors`), and it is never indexed or cached; its refusals (an address of no page, a record a draft does not have, a draft that is not JSON or is over 1 MB) say the same.
367
+ - `/preview` and `/preview/<page>`: the preview the CMS's editor frames, the pages of records too (at their address or at the record's id, a page the site does not show too). `GET` shows the published content; `POST` with the content as JSON shows that content without saving it, which is how the editor shows every edit at once. Only the CMS may frame it (`Content-Security-Policy: frame-ancestors`), and it is never indexed or cached; its refusals (an address of no page, a record a draft does not have, a draft that is not JSON or is over 1 MB) say the same.
339
368
  - The bridge on the preview: it tells the editor where the marked elements are and what a person does on the page, and types texts in place, formatted ones too.
340
369
  - `/seamlet/site.json`: what the site tells the CMS about itself (`SITE_DESCRIPTION_PATH`): its id and name from `seamlet.json`, its schema, the content it starts with and its pages. The CMS reads it with `readSiteDescription` whenever the site is opened in its editor.
341
370
  - `/seamlet-photos/<address>`: the site's own photo at an address its content gives it, found by `photos` of `defineSeamletSite`, for the CMS's editor.
package/dist/bridge.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { F as Fields } from './parse-content-IOJxgKLB.js';
2
- import { P as PageMessage, E as EditorMessage } from './messages-Bqgs_mic.js';
1
+ import { F as Fields } from './parse-content-Bnn6Wa-l.js';
2
+ import { P as PageMessage, E as EditorMessage } from './messages-C2OjqFmy.js';
3
3
 
4
4
  interface BridgeOptions {
5
5
  /** The origin of the CMS whose editor may frame the page, such as `https://cms.example.com`. */
package/dist/bridge.js CHANGED
@@ -18,7 +18,7 @@ import {
18
18
  shortcutOf,
19
19
  toggleList,
20
20
  toggleMark
21
- } from "./chunk-PLNPM2D3.js";
21
+ } from "./chunk-Y4R4YV5O.js";
22
22
  import {
23
23
  FIELD_PATH_ATTRIBUTE,
24
24
  REVEALED_ATTRIBUTE,
@@ -28,7 +28,7 @@ import {
28
28
  readRichText,
29
29
  resolveFieldPath,
30
30
  safeHref
31
- } from "./chunk-I3HOJE3S.js";
31
+ } from "./chunk-UL2C2IB2.js";
32
32
 
33
33
  // src/bridge/typed-fields.ts
34
34
  function typedKindAt(schema, path) {
@@ -6,7 +6,7 @@ import {
6
6
  recordAt,
7
7
  recordPageAt,
8
8
  recordPagePath
9
- } from "./chunk-I3HOJE3S.js";
9
+ } from "./chunk-UL2C2IB2.js";
10
10
 
11
11
  // src/site-pages.ts
12
12
  function pagePathOf(rest) {
@@ -15,12 +15,13 @@ function pagePathOf(rest) {
15
15
  function pageParameterOf(path) {
16
16
  return path === HOME_PAGE_PATH ? void 0 : path.slice(PAGE_PATH_SEPARATOR.length);
17
17
  }
18
+ var SHOWN_ON_SITE = (recordPage, record) => recordPage.pages.isShown?.(record) ?? true;
19
+ var EVERY_RECORD = () => true;
18
20
  function renderedPageAt(site, path, content) {
19
- const page = Object.hasOwn(site.pages, path) ? site.pages[path] : void 0;
20
- if (page !== void 0) {
21
- return { component: page.component, props: { content } };
22
- }
23
- return renderedRecordPageAt(site, path, content);
21
+ return ownPageAt(site, path, content) ?? renderedRecordPageAt(site, { path, content, isShown: SHOWN_ON_SITE });
22
+ }
23
+ function previewedPageAt(site, path, content) {
24
+ return ownPageAt(site, path, content) ?? renderedRecordPageAt(site, { path, content, isShown: EVERY_RECORD });
24
25
  }
25
26
  function staticPagesOf(site, content) {
26
27
  const ownPages = Object.entries(site.pages).map(
@@ -31,26 +32,35 @@ function staticPagesOf(site, content) {
31
32
  );
32
33
  return [...ownPages, ...recordPages];
33
34
  }
34
- function renderedRecordPageAt(site, path, content) {
35
- const described = site.recordPages.map((recordPage) => ({
36
- ...recordPage.pages,
37
- component: recordPage.component
35
+ function ownPageAt(site, path, content) {
36
+ const page = Object.hasOwn(site.pages, path) ? site.pages[path] : void 0;
37
+ return page === void 0 ? void 0 : { component: page.component, props: { content } };
38
+ }
39
+ function renderedRecordPageAt(site, request) {
40
+ const { path, content, isShown } = request;
41
+ const described = site.recordPages.map((recordPage2) => ({
42
+ ...recordPage2.pages,
43
+ recordPage: recordPage2
38
44
  }));
39
45
  const found = recordPageAt(described, path);
40
46
  if (found === void 0) return void 0;
41
47
  const records = collectionAt(site.schema, content, found.page.collection);
42
48
  if (records === void 0) return void 0;
43
49
  const placed = recordAt(records.collection, records.records, found.address);
44
- if (placed === void 0) return void 0;
50
+ const { recordPage } = found.page;
51
+ if (placed === void 0 || !isShown(recordPage, placed.record)) {
52
+ return void 0;
53
+ }
45
54
  const props = { content, record: placed.record, index: placed.index };
46
- return { component: found.page.component, props };
55
+ return { component: recordPage.component, props };
47
56
  }
48
57
  function staticRecordPagesOf(schema, recordPage, content) {
49
58
  const found = collectionAt(schema, content, recordPage.pages.collection);
50
59
  if (found === void 0) return [];
51
60
  return found.records.flatMap((record, index) => {
52
61
  const address = recordAddressOf(found.collection, record);
53
- if (address === void 0) return [];
62
+ const hasPage = address !== void 0 && SHOWN_ON_SITE(recordPage, record);
63
+ if (!hasPage) return [];
54
64
  const path = recordPagePath(recordPage.pages.path, address);
55
65
  const props = { content, record, index };
56
66
  return [staticPage(path, { component: recordPage.component, props })];
@@ -63,5 +73,6 @@ function staticPage(path, page) {
63
73
  export {
64
74
  pagePathOf,
65
75
  renderedPageAt,
76
+ previewedPageAt,
66
77
  staticPagesOf
67
78
  };
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  pagePathOf
3
- } from "./chunk-G6Z44LGA.js";
3
+ } from "./chunk-HFPZVDST.js";
4
4
  import {
5
5
  FIELD_PATH_SEPARATOR,
6
6
  collectionAt,
@@ -9,7 +9,7 @@ import {
9
9
  newRecord,
10
10
  recordAt,
11
11
  recordPageAt
12
- } from "./chunk-I3HOJE3S.js";
12
+ } from "./chunk-UL2C2IB2.js";
13
13
 
14
14
  // src/preview-requests.ts
15
15
  var MAX_DRAFT_BYTES = 1024 * 1024;
@@ -1026,6 +1026,30 @@ function openingHoursField(options = {}) {
1026
1026
  }
1027
1027
  });
1028
1028
  }
1029
+ function productFields() {
1030
+ return {
1031
+ name: text({ label: "\u041D\u0430\u0437\u0432\u0430\u043D\u0438\u0435", maxLength: 120 }),
1032
+ slug: slug({ label: "\u0410\u0434\u0440\u0435\u0441 \u0441\u0442\u0440\u0430\u043D\u0438\u0446\u044B" }),
1033
+ photos: collection({
1034
+ label: "\u0424\u043E\u0442\u043E",
1035
+ item: { photo: image({ label: "\u0424\u043E\u0442\u043E" }) }
1036
+ }),
1037
+ summary: richText({ label: "\u041A\u0440\u0430\u0442\u043A\u043E", inline: true, maxLength: 200 }),
1038
+ description: richText({ label: "\u041E\u043F\u0438\u0441\u0430\u043D\u0438\u0435" }),
1039
+ price: number({ label: "\u0426\u0435\u043D\u0430, \u20BD", min: 0 }),
1040
+ priceNote: text({ label: "\u0423\u0442\u043E\u0447\u043D\u0435\u043D\u0438\u0435 \u0446\u0435\u043D\u044B", maxLength: 40 }),
1041
+ characteristics: collection({
1042
+ label: "\u0425\u0430\u0440\u0430\u043A\u0442\u0435\u0440\u0438\u0441\u0442\u0438\u043A\u0438",
1043
+ itemTitle: "name",
1044
+ item: {
1045
+ name: text({ label: "\u041D\u0430\u0437\u0432\u0430\u043D\u0438\u0435", maxLength: 60 }),
1046
+ value: text({ label: "\u0417\u043D\u0430\u0447\u0435\u043D\u0438\u0435", maxLength: 120 })
1047
+ }
1048
+ }),
1049
+ isHidden: toggle({ label: "\u0421\u043A\u0440\u044B\u0442\u044C \u0441 \u0441\u0430\u0439\u0442\u0430" }),
1050
+ seo: seoFields()
1051
+ };
1052
+ }
1029
1053
 
1030
1054
  // src/schema/read-fields.ts
1031
1055
  var TITLE_KINDS = /* @__PURE__ */ new Set([
@@ -1310,17 +1334,19 @@ var RECORD_PAGE_PATH = new RegExp(
1310
1334
  var PAGE_PATH_SEPARATOR = "/";
1311
1335
  var NOT_FOUND = -1;
1312
1336
  function recordPages(schema, options) {
1313
- const collection2 = collectionFieldAt(schema, options.collection);
1337
+ const { shows, ...described } = options;
1338
+ const collection2 = collectionFieldAt(schema, described.collection);
1314
1339
  if (collection2 === void 0) {
1315
1340
  throw new Error(
1316
- `@seamlet/astro: the schema has no collection "${options.collection}" for the pages at "${options.path}"`
1341
+ `@seamlet/astro: the schema has no collection "${described.collection}" for the pages at "${described.path}"`
1317
1342
  );
1318
1343
  }
1344
+ const isShown = (record) => shows === void 0 || shows(record);
1319
1345
  const pathOf = (record) => {
1320
- const address = recordAddressOf(collection2, record);
1321
- return address === void 0 ? void 0 : recordPagePath(options.path, address);
1346
+ const address = isShown(record) ? recordAddressOf(collection2, record) : void 0;
1347
+ return address === void 0 ? void 0 : recordPagePath(described.path, address);
1322
1348
  };
1323
- return { ...options, pathOf };
1349
+ return { ...described, isShown, pathOf };
1324
1350
  }
1325
1351
  function isRecordPagePath(path) {
1326
1352
  return typeof path === "string" && RECORD_PAGE_PATH.test(path);
@@ -1586,6 +1612,7 @@ export {
1586
1612
  contactsFields,
1587
1613
  WEEKDAYS,
1588
1614
  openingHoursField,
1615
+ productFields,
1589
1616
  readFields,
1590
1617
  checkFields,
1591
1618
  SITE_CONFIG_FILE,
@@ -6,7 +6,7 @@ import {
6
6
  readRichText,
7
7
  resolveFieldPath,
8
8
  safeHref
9
- } from "./chunk-I3HOJE3S.js";
9
+ } from "./chunk-UL2C2IB2.js";
10
10
 
11
11
  // src/bridge/editor-shortcuts.ts
12
12
  function shortcutOf(event) {
package/dist/cli.js CHANGED
@@ -6,7 +6,7 @@ import {
6
6
  SITE_CONFIG_FILE,
7
7
  isSiteId,
8
8
  readSiteConfig
9
- } from "./chunk-I3HOJE3S.js";
9
+ } from "./chunk-UL2C2IB2.js";
10
10
 
11
11
  // src/cli.ts
12
12
  import process from "process";
package/dist/index.js CHANGED
@@ -8,14 +8,14 @@ import {
8
8
  } from "./chunk-QCVWHHJC.js";
9
9
  import {
10
10
  isUploadedPhoto
11
- } from "./chunk-QXPXIMIM.js";
12
- import "./chunk-G6Z44LGA.js";
11
+ } from "./chunk-OCA2WYK3.js";
12
+ import "./chunk-HFPZVDST.js";
13
13
  import {
14
14
  SITE_CONFIG_FILE,
15
15
  SITE_DESCRIPTION_PATH,
16
16
  isJsonObject,
17
17
  readSiteConfig
18
- } from "./chunk-I3HOJE3S.js";
18
+ } from "./chunk-UL2C2IB2.js";
19
19
 
20
20
  // src/index.ts
21
21
  import process from "process";
@@ -1,4 +1,4 @@
1
- import { R as RichText, F as Fields, C as Content, a as FieldPath } from './parse-content-IOJxgKLB.js';
1
+ import { R as RichText, F as Fields, C as Content, a as FieldPath } from './parse-content-Bnn6Wa-l.js';
2
2
 
3
3
  interface Box {
4
4
  readonly top: number;
@@ -300,4 +300,4 @@ declare function countCharacters(text: string): number;
300
300
  */
301
301
  declare function parseContent<F extends Fields>(schema: F, input: unknown): ParsedContent<ContentOf<F>>;
302
302
 
303
- export { parseContent as $, collection as A, countCharacters as B, type Content as C, type DateField as D, date as E, type Fields as F, type GroupField as G, defaultContent as H, type ImageField as I, defaultValue as J, defineSchema as K, type LinkValue as L, formatFieldPath as M, type NumberField as N, type OptionValue as O, type ParsedContent as P, group as Q, type RichText as R, type SlugField as S, type TextField as T, type UrlField as U, type ValueOf as V, image as W, linesOf as X, link as Y, normalizedLine as Z, number as _, type FieldPath as a, plainTextOf as a0, readRichText as a1, reference as a2, references as a3, richText as a4, richTextOf as a5, slug as a6, text as a7, time as a8, toggle as a9, url as aa, type ContentOf as b, type Field as c, type CollectionField as d, type RecordFields as e, type RecordOf as f, type ContentIssue as g, type ImageValue as h, type LinkField as i, type ChoicesField as j, type TimeField as k, type ChoiceField as l, type ChoiceOption as m, type FieldKind as n, type FieldValue as o, type ImageFocus as p, type ReferenceField as q, type ReferencesField as r, type RichBlock as s, type RichLine as t, type RichSpan as u, type RichTextField as v, type ToggleField as w, asOneParagraph as x, choice as y, choices as z };
303
+ export { parseContent as $, collection as A, countCharacters as B, type Content as C, type DateField as D, date as E, type Fields as F, type GroupField as G, defaultContent as H, type ImageField as I, defaultValue as J, defineSchema as K, type LinkValue as L, formatFieldPath as M, type NumberField as N, type OptionValue as O, type ParsedContent as P, group as Q, type RichText as R, type SlugField as S, type TextField as T, type UrlField as U, type ValueOf as V, image as W, linesOf as X, link as Y, normalizedLine as Z, number as _, type FieldPath as a, plainTextOf as a0, readRichText as a1, reference as a2, references as a3, richText as a4, richTextOf as a5, slug as a6, text as a7, time as a8, toggle as a9, url as aa, type ContentOf as b, type Field as c, type CollectionField as d, type RecordFields as e, type RecordOf as f, type ContentIssue as g, type ImageValue as h, type LinkField as i, type ChoicesField as j, type TimeField as k, type RichTextField as l, type ToggleField as m, type ChoiceField as n, type ChoiceOption as o, type FieldKind as p, type FieldValue as q, type ImageFocus as r, type ReferenceField as s, type ReferencesField as t, type RichBlock as u, type RichLine as v, type RichSpan as w, asOneParagraph as x, choice as y, choices as z };
@@ -1,5 +1,5 @@
1
- import { C as Content } from './parse-content-IOJxgKLB.js';
2
- import { S as SiteDescription } from './site-description-BuEpga8p.js';
1
+ import { C as Content } from './parse-content-Bnn6Wa-l.js';
2
+ import { S as SiteDescription } from './site-description-BnHrODQX.js';
3
3
 
4
4
  /** Far more than the content of a site ever takes; a bigger body is not a draft. */
5
5
  declare const MAX_DRAFT_BYTES: number;
@@ -5,9 +5,9 @@ import {
5
5
  previewContentAt,
6
6
  previewHeadersOf,
7
7
  readDraft
8
- } from "./chunk-QXPXIMIM.js";
9
- import "./chunk-G6Z44LGA.js";
10
- import "./chunk-I3HOJE3S.js";
8
+ } from "./chunk-OCA2WYK3.js";
9
+ import "./chunk-HFPZVDST.js";
10
+ import "./chunk-UL2C2IB2.js";
11
11
  export {
12
12
  MAX_DRAFT_BYTES,
13
13
  isPageAddress,
@@ -1,6 +1,6 @@
1
- import { S as Shortcut, T as TextFormats } from './messages-Bqgs_mic.js';
2
- export { B as Box, a as EditorKey, E as EditorMessage, I as InlineEditingOutcome, L as LinkBox, M as MarkerBox, b as PROTOCOL_VERSION, c as PageLayout, P as PageMessage, d as Point, e as TextFormat, f as TypedSelection, V as Viewport, r as readEditorMessage, g as readPageMessage } from './messages-Bqgs_mic.js';
3
- import { F as Fields, a as FieldPath, I as ImageField, R as RichText } from './parse-content-IOJxgKLB.js';
1
+ import { S as Shortcut, T as TextFormats } from './messages-C2OjqFmy.js';
2
+ export { B as Box, a as EditorKey, E as EditorMessage, I as InlineEditingOutcome, L as LinkBox, M as MarkerBox, b as PROTOCOL_VERSION, c as PageLayout, P as PageMessage, d as Point, e as TextFormat, f as TypedSelection, V as Viewport, r as readEditorMessage, g as readPageMessage } from './messages-C2OjqFmy.js';
3
+ import { F as Fields, a as FieldPath, I as ImageField, R as RichText } from './parse-content-Bnn6Wa-l.js';
4
4
 
5
5
  declare function shortcutOf(event: KeyboardEvent): Shortcut | undefined;
6
6
 
package/dist/protocol.js CHANGED
@@ -22,9 +22,9 @@ import {
22
22
  shortcutOf,
23
23
  toggleList,
24
24
  toggleMark
25
- } from "./chunk-PLNPM2D3.js";
25
+ } from "./chunk-Y4R4YV5O.js";
26
26
  import "./chunk-QCVWHHJC.js";
27
- import "./chunk-I3HOJE3S.js";
27
+ import "./chunk-UL2C2IB2.js";
28
28
  export {
29
29
  OWN_PHOTOS_PATH,
30
30
  PROTOCOL_VERSION,
@@ -3,9 +3,9 @@ import {
3
3
  } from "../chunk-QCVWHHJC.js";
4
4
  import {
5
5
  isUploadedPhoto
6
- } from "../chunk-QXPXIMIM.js";
7
- import "../chunk-G6Z44LGA.js";
8
- import "../chunk-I3HOJE3S.js";
6
+ } from "../chunk-OCA2WYK3.js";
7
+ import "../chunk-HFPZVDST.js";
8
+ import "../chunk-UL2C2IB2.js";
9
9
 
10
10
  // src/routes/media.ts
11
11
  import { cmsOrigin } from "virtual:seamlet/config";
package/dist/schema.d.ts CHANGED
@@ -1,6 +1,6 @@
1
- import { h as ImageValue, L as LinkValue, d as CollectionField, C as Content, F as Fields, e as RecordFields, b as ContentOf, G as GroupField, i as LinkField, T as TextField, U as UrlField, j as ChoicesField, k as TimeField, I as ImageField } from './parse-content-IOJxgKLB.js';
2
- export { l as ChoiceField, m as ChoiceOption, g as ContentIssue, D as DateField, c as Field, n as FieldKind, a as FieldPath, o as FieldValue, p as ImageFocus, N as NumberField, O as OptionValue, P as ParsedContent, f as RecordOf, q as ReferenceField, r as ReferencesField, s as RichBlock, t as RichLine, u as RichSpan, R as RichText, v as RichTextField, S as SlugField, w as ToggleField, V as ValueOf, x as asOneParagraph, y as choice, z as choices, A as collection, B as countCharacters, E as date, H as defaultContent, J as defaultValue, K as defineSchema, M as formatFieldPath, Q as group, W as image, X as linesOf, Y as link, Z as normalizedLine, _ as number, $ as parseContent, a0 as plainTextOf, a1 as readRichText, a2 as reference, a3 as references, a4 as richText, a5 as richTextOf, a6 as slug, a7 as text, a8 as time, a9 as toggle, aa as url } from './parse-content-IOJxgKLB.js';
3
- export { C as CollectionPath, F as FIELD_PATH_SEPARATOR, b as FieldTarget, c as FieldsReading, H as HOME_PAGE_PATH, P as PAGE_PATH_SEPARATOR, d as PageProblemReason, e as PlacedRecord, R as RECORD_PLACEHOLDER, f as RecordFieldsAt, g as RecordPagePath, h as RecordPageProblemReason, i as RecordPages, j as SITE_DESCRIPTION_PATH, k as SchemaProblem, l as SchemaProblemReason, S as SiteDescription, m as SiteDescriptionProblem, n as SiteDescriptionReading, o as SitePageDescription, a as SiteRecordPageDescription, p as checkFields, q as checkSiteDescription, r as isRecordPagePath, s as isSearchTextsGroup, t as readFields, u as readSiteDescription, v as recordAddressOf, w as recordAt, x as recordPageAt, y as recordPagePath, z as recordPages, A as resolveFieldPath } from './site-description-BuEpga8p.js';
1
+ import { h as ImageValue, L as LinkValue, d as CollectionField, C as Content, F as Fields, e as RecordFields, b as ContentOf, G as GroupField, i as LinkField, T as TextField, U as UrlField, j as ChoicesField, k as TimeField, S as SlugField, I as ImageField, l as RichTextField, N as NumberField, m as ToggleField } from './parse-content-Bnn6Wa-l.js';
2
+ export { n as ChoiceField, o as ChoiceOption, g as ContentIssue, D as DateField, c as Field, p as FieldKind, a as FieldPath, q as FieldValue, r as ImageFocus, O as OptionValue, P as ParsedContent, f as RecordOf, s as ReferenceField, t as ReferencesField, u as RichBlock, v as RichLine, w as RichSpan, R as RichText, V as ValueOf, x as asOneParagraph, y as choice, z as choices, A as collection, B as countCharacters, E as date, H as defaultContent, J as defaultValue, K as defineSchema, M as formatFieldPath, Q as group, W as image, X as linesOf, Y as link, Z as normalizedLine, _ as number, $ as parseContent, a0 as plainTextOf, a1 as readRichText, a2 as reference, a3 as references, a4 as richText, a5 as richTextOf, a6 as slug, a7 as text, a8 as time, a9 as toggle, aa as url } from './parse-content-Bnn6Wa-l.js';
3
+ export { C as CollectionPath, F as FIELD_PATH_SEPARATOR, b as FieldTarget, c as FieldsReading, H as HOME_PAGE_PATH, P as PAGE_PATH_SEPARATOR, d as PageProblemReason, e as PlacedRecord, R as RECORD_PLACEHOLDER, f as RecordFieldsAt, g as RecordPagePath, h as RecordPageProblemReason, i as RecordPages, j as SITE_DESCRIPTION_PATH, k as SchemaProblem, l as SchemaProblemReason, S as SiteDescription, m as SiteDescriptionProblem, n as SiteDescriptionReading, o as SitePageDescription, p as SiteRecordPageDescription, a as SiteRecordPages, q as checkFields, r as checkSiteDescription, s as isRecordPagePath, t as isSearchTextsGroup, u as readFields, v as readSiteDescription, w as recordAddressOf, x as recordAt, y as recordPageAt, z as recordPagePath, A as recordPages, B as resolveFieldPath } from './site-description-BnHrODQX.js';
4
4
 
5
5
  declare const FIELD_PATH_ATTRIBUTE = "data-cms-path";
6
6
  interface FieldMarker {
@@ -159,6 +159,35 @@ declare function openingHoursField(options?: {
159
159
  closes: TimeField;
160
160
  note: TextField;
161
161
  }>;
162
+ /**
163
+ * The fields of a product of a catalog: its name and the address of its page, its photos (the first
164
+ * is the main one), a short line for a card and a whole description for its page, the price and a
165
+ * word about it («от», «за м²»), characteristics as pairs of a name and a value, so the owner adds a
166
+ * property without code, the switch that hides it, and what search engines show for its page. A
167
+ * site puts them into its collection and adds its own fields beside them:
168
+ * `item: { ...productFields(), category: reference({ label: "Категория", collection: "categories" }) }`.
169
+ */
170
+ declare function productFields(): {
171
+ name: TextField;
172
+ slug: SlugField;
173
+ photos: CollectionField<{
174
+ photo: ImageField;
175
+ }>;
176
+ summary: RichTextField;
177
+ description: RichTextField;
178
+ price: NumberField;
179
+ priceNote: TextField;
180
+ characteristics: CollectionField<{
181
+ name: TextField;
182
+ value: TextField;
183
+ }>;
184
+ isHidden: ToggleField;
185
+ seo: GroupField<{
186
+ title: TextField;
187
+ description: TextField;
188
+ image: ImageField;
189
+ }>;
190
+ };
162
191
 
163
192
  /** An object of named values: not null, not a list. */
164
193
  type JsonObject = Readonly<Record<string, unknown>>;
@@ -205,4 +234,4 @@ declare function slugOf(words: string): string;
205
234
  /** The name of the slug of a record of the collection, if its records have one. */
206
235
  declare function slugFieldOf(item: RecordFields): string | undefined;
207
236
 
208
- export { ChoicesField, CollectionField, type CollectionRecords, Content, ContentOf, type ContentPath, FIELD_PATH_ATTRIBUTE, type FieldMarker, Fields, GroupField, ImageField, ImageValue, type JsonObject, LinkField, type LinkKind, type LinkTarget, LinkValue, MAX_SLUG_LENGTH, RECORD_ID_KEY, REVEALED_ATTRIBUTE, REVEAL_ATTRIBUTE, REVEAL_EVENT, RecordFields, SITE_CONFIG_FILE, type SiteConfig, type SiteConfigField, type SiteConfigReading, TextField, TimeField, UrlField, WEEKDAYS, collectionAt, collectionFieldAt, contactsFields, editable, findRecord, hrefOf, imagePosition, isContentObject, isJsonObject, isRecordId, isSiteId, isSlug, linkTargetOf, newRecord, openingHoursField, readSiteConfig, safeHref, seoFields, slugFieldOf, slugOf, withRecordIds };
237
+ export { ChoicesField, CollectionField, type CollectionRecords, Content, ContentOf, type ContentPath, FIELD_PATH_ATTRIBUTE, type FieldMarker, Fields, GroupField, ImageField, ImageValue, type JsonObject, LinkField, type LinkKind, type LinkTarget, LinkValue, MAX_SLUG_LENGTH, NumberField, RECORD_ID_KEY, REVEALED_ATTRIBUTE, REVEAL_ATTRIBUTE, REVEAL_EVENT, RecordFields, RichTextField, SITE_CONFIG_FILE, type SiteConfig, type SiteConfigField, type SiteConfigReading, SlugField, TextField, TimeField, ToggleField, UrlField, WEEKDAYS, collectionAt, collectionFieldAt, contactsFields, editable, findRecord, hrefOf, imagePosition, isContentObject, isJsonObject, isRecordId, isSiteId, isSlug, linkTargetOf, newRecord, openingHoursField, productFields, readSiteConfig, safeHref, seoFields, slugFieldOf, slugOf, withRecordIds };
package/dist/schema.js CHANGED
@@ -49,6 +49,7 @@ import {
49
49
  openingHoursField,
50
50
  parseContent,
51
51
  plainTextOf,
52
+ productFields,
52
53
  readFields,
53
54
  readRichText,
54
55
  readSiteConfig,
@@ -73,7 +74,7 @@ import {
73
74
  toggle,
74
75
  url,
75
76
  withRecordIds
76
- } from "./chunk-I3HOJE3S.js";
77
+ } from "./chunk-UL2C2IB2.js";
77
78
  export {
78
79
  FIELD_PATH_ATTRIBUTE,
79
80
  FIELD_PATH_SEPARATOR,
@@ -125,6 +126,7 @@ export {
125
126
  openingHoursField,
126
127
  parseContent,
127
128
  plainTextOf,
129
+ productFields,
128
130
  readFields,
129
131
  readRichText,
130
132
  readSiteConfig,
@@ -1,4 +1,4 @@
1
- import { a as FieldPath, c as Field, d as CollectionField, F as Fields, G as GroupField, C as Content, e as RecordFields, f as RecordOf, g as ContentIssue } from './parse-content-IOJxgKLB.js';
1
+ import { a as FieldPath, c as Field, d as CollectionField, F as Fields, G as GroupField, C as Content, e as RecordFields, f as RecordOf, g as ContentIssue } from './parse-content-Bnn6Wa-l.js';
2
2
 
3
3
  /** What a path written on a page points at: a field, or a whole record of a collection. */
4
4
  type FieldTarget = {
@@ -75,9 +75,20 @@ interface PlacedRecord {
75
75
  readonly record: Content;
76
76
  readonly index: number;
77
77
  }
78
+ /** The pages of the records of a collection as the site renders them. */
79
+ interface SiteRecordPages extends SiteRecordPageDescription {
80
+ /**
81
+ * Whether the site shows the record's page; every record's when left out. The preview shows the
82
+ * page of every record, a hidden one too, so the owner prepares it before showing it.
83
+ */
84
+ readonly isShown?: (record: Content) => boolean;
85
+ }
78
86
  /** The pages of the records of a collection, with the address of each record's page. */
79
- interface RecordPages<R> extends SiteRecordPageDescription {
80
- /** The address of the record's page: `/menu/latte`; none for a record with neither slug nor id. */
87
+ interface RecordPages<R> extends SiteRecordPages {
88
+ /**
89
+ * The address of the record's page: `/menu/latte`; none for a record whose page the site does
90
+ * not show, or with neither slug nor id.
91
+ */
81
92
  readonly pathOf: (record: R) => string | undefined;
82
93
  }
83
94
  /** The address of a record's page with `[record]` in place of the record. */
@@ -96,12 +107,14 @@ declare const PAGE_PATH_SEPARATOR = "/";
96
107
  * A page of every record of a collection of the schema at the address with `[record]`:
97
108
  * `recordPages(schema, { collection: "menu", path: "/menu/[record]" })`. A site gives it to
98
109
  * `defineSeamletSite` with the component that renders a record, and links to a record's page with
99
- * its `pathOf`.
110
+ * its `pathOf`. `shows` leaves the pages of some records off the site, such as hidden ones:
111
+ * `shows: (product) => !product.isHidden`.
100
112
  */
101
113
  declare function recordPages<F extends Fields, const P extends CollectionPath<F>>(schema: F, options: {
102
114
  readonly collection: P;
103
115
  readonly path: RecordPagePath;
104
116
  readonly searchTexts?: string;
117
+ readonly shows?: (record: RecordOf<RecordFieldsAt<F, P>>) => boolean;
105
118
  }): RecordPages<RecordOf<RecordFieldsAt<F, P>>>;
106
119
  /** Whether the path can be the address of the pages of records: `/menu/[record]`. */
107
120
  declare function isRecordPagePath(path: unknown): path is string;
@@ -242,4 +255,4 @@ declare function checkSiteDescription(input: unknown): SiteDescriptionReading;
242
255
  /** Whether the path names a group of a `title` and a `description` text. */
243
256
  declare function isSearchTextsGroup(schema: Fields, path: string): boolean;
244
257
 
245
- export { resolveFieldPath as A, type CollectionPath as C, FIELD_PATH_SEPARATOR as F, HOME_PAGE_PATH as H, PAGE_PATH_SEPARATOR as P, RECORD_PLACEHOLDER as R, type SiteDescription as S, type SiteRecordPageDescription as a, type FieldTarget as b, type FieldsReading as c, type PageProblemReason as d, type PlacedRecord as e, type RecordFieldsAt as f, type RecordPagePath as g, type RecordPageProblemReason as h, type RecordPages as i, SITE_DESCRIPTION_PATH as j, type SchemaProblem as k, type SchemaProblemReason as l, type SiteDescriptionProblem as m, type SiteDescriptionReading as n, type SitePageDescription as o, checkFields as p, checkSiteDescription as q, isRecordPagePath as r, isSearchTextsGroup as s, readFields as t, readSiteDescription as u, recordAddressOf as v, recordAt as w, recordPageAt as x, recordPagePath as y, recordPages as z };
258
+ export { recordPages as A, resolveFieldPath as B, type CollectionPath as C, FIELD_PATH_SEPARATOR as F, HOME_PAGE_PATH as H, PAGE_PATH_SEPARATOR as P, RECORD_PLACEHOLDER as R, type SiteDescription as S, type SiteRecordPages as a, type FieldTarget as b, type FieldsReading as c, type PageProblemReason as d, type PlacedRecord as e, type RecordFieldsAt as f, type RecordPagePath as g, type RecordPageProblemReason as h, type RecordPages as i, SITE_DESCRIPTION_PATH as j, type SchemaProblem as k, type SchemaProblemReason as l, type SiteDescriptionProblem as m, type SiteDescriptionReading as n, type SitePageDescription as o, type SiteRecordPageDescription as p, checkFields as q, checkSiteDescription as r, isRecordPagePath as s, isSearchTextsGroup as t, readFields as u, readSiteDescription as v, recordAddressOf as w, recordAt as x, recordPageAt as y, recordPagePath as z };
package/dist/site.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { ImageMetadata } from 'astro';
2
2
  import { AstroComponentFactory } from 'astro/runtime/server/index.js';
3
- import { C as Content, F as Fields, b as ContentOf } from './parse-content-IOJxgKLB.js';
4
- import { a as SiteRecordPageDescription, S as SiteDescription } from './site-description-BuEpga8p.js';
3
+ import { C as Content, F as Fields, b as ContentOf } from './parse-content-Bnn6Wa-l.js';
4
+ import { a as SiteRecordPages, S as SiteDescription } from './site-description-BnHrODQX.js';
5
5
 
6
6
  /** A page of the site: how the editor names it, and the component that renders it. */
7
7
  interface SeamletPage {
@@ -14,7 +14,7 @@ interface SeamletPage {
14
14
  /** The pages of the records of a collection, and the component that renders each of them. */
15
15
  interface SeamletRecordPage {
16
16
  /** Which records have pages and where: `recordPages(schema, { collection, path })`. */
17
- readonly pages: SiteRecordPageDescription;
17
+ readonly pages: SiteRecordPages;
18
18
  /**
19
19
  * A component that takes the site's content, the record and its place in its collection as its
20
20
  * `content`, `record` and `index` props.
@@ -22,10 +22,10 @@ interface SeamletRecordPage {
22
22
  readonly component: AstroComponentFactory;
23
23
  }
24
24
  /** What renders a page: its component, and the props to give it. */
25
- interface RenderedPage {
25
+ type RenderedPage = {
26
26
  readonly component: AstroComponentFactory;
27
27
  readonly props: PageProps;
28
- }
28
+ };
29
29
  type PageProps = {
30
30
  readonly content: Content;
31
31
  } | {
@@ -34,12 +34,12 @@ type PageProps = {
34
34
  readonly index: number;
35
35
  };
36
36
  /** A page that `getStaticPaths` of a site's `[...page]` route builds. */
37
- interface StaticPage {
37
+ type StaticPage = {
38
38
  readonly params: {
39
39
  readonly page: string | undefined;
40
40
  };
41
41
  readonly props: RenderedPage;
42
- }
42
+ };
43
43
 
44
44
  /** The site's content and pages; its id and title come from `seamlet.json`. */
45
45
  interface SeamletSiteDefinition<F extends Fields> {
@@ -72,8 +72,13 @@ interface SeamletSite<F extends Fields = Fields> {
72
72
  * first page, `menu/latte` the page of the record at /menu/latte. Undefined when there is none.
73
73
  */
74
74
  readonly pageOf: (rest: string | undefined, content: ContentOf<F>) => RenderedPage | undefined;
75
+ /**
76
+ * What the preview renders at the rest of an address with the content: as `pageOf`, and the page
77
+ * of a record the site does not show, so the owner prepares it in the editor.
78
+ */
79
+ readonly previewPageOf: (rest: string | undefined, content: ContentOf<F>) => RenderedPage | undefined;
75
80
  /** Every page of the site with the content, for `getStaticPaths` of a `[...page]` route. */
76
- readonly staticPaths: (content: ContentOf<F>) => readonly StaticPage[];
81
+ readonly staticPaths: (content: ContentOf<F>) => StaticPage[];
77
82
  /** What the CMS published: the site's initial content until the first publication. */
78
83
  readonly loadPublishedContent: () => Promise<ContentOf<F>>;
79
84
  /** The site's own photo at an address its content gives it, if it has one there. */
package/dist/site.js CHANGED
@@ -3,14 +3,15 @@ import {
3
3
  } from "./chunk-QCVWHHJC.js";
4
4
  import {
5
5
  pagePathOf,
6
+ previewedPageAt,
6
7
  renderedPageAt,
7
8
  staticPagesOf
8
- } from "./chunk-G6Z44LGA.js";
9
+ } from "./chunk-HFPZVDST.js";
9
10
  import {
10
11
  checkSiteDescription,
11
12
  formatFieldPath,
12
13
  parseContent
13
- } from "./chunk-I3HOJE3S.js";
14
+ } from "./chunk-UL2C2IB2.js";
14
15
 
15
16
  // src/site.ts
16
17
  import { cmsOrigin, siteId, siteTitle } from "virtual:seamlet/config";
@@ -173,6 +174,7 @@ function defineSeamletSite(definition) {
173
174
  return page?.component;
174
175
  },
175
176
  pageOf: (rest, content) => renderedPageAt(sitePages, pagePathOf(rest), contentOf(content)),
177
+ previewPageOf: (rest, content) => previewedPageAt(sitePages, pagePathOf(rest), contentOf(content)),
176
178
  staticPaths: (content) => staticPagesOf(sitePages, contentOf(content)),
177
179
  loadPublishedContent: () => loadPublishedContent(definition.schema),
178
180
  ownPhotoAt: (src) => definition.photos?.(src),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@seamlet/astro",
3
- "version": "1.1.0",
3
+ "version": "1.2.1",
4
4
  "description": "Connects an Astro site to the Seamlet CMS: the schema of its content, the preview the editor frames, and its published content.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -38,7 +38,7 @@ const content =
38
38
  )
39
39
  : parseContent(site.schema, reading.draft).content;
40
40
 
41
- const page = site.pageOf(rest, content);
41
+ const page = site.previewPageOf(rest, content);
42
42
  if (page === undefined) {
43
43
  return new Response(null, { status: NOT_FOUND, headers });
44
44
  }