@seamlet/astro 0.11.3 → 0.12.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/README.md +19 -7
- package/components/RichLine.astro +2 -5
- package/components/RichText.astro +2 -4
- package/dist/bridge.d.ts +2 -3
- package/dist/bridge.js +557 -393
- package/dist/chunk-QCVWHHJC.js +14 -0
- package/dist/{chunk-P2OQOZBC.js → chunk-QL4K4HR6.js} +8 -0
- package/dist/{chunk-2E7VDSUK.js → chunk-SSECNMAD.js} +34 -43
- package/dist/{chunk-3BS5CE26.js → chunk-W2LIOR2C.js} +6 -2
- package/dist/{chunk-DRDPKFHZ.js → chunk-ZS32CW5F.js} +396 -207
- package/dist/cli.js +96 -37
- package/dist/client.js +4 -1
- package/dist/index.js +52 -19
- package/dist/{messages-Dx0jNy3N.d.ts → messages-CvZhLRSE.d.ts} +29 -52
- package/dist/{content-6k-E7NWf.d.ts → parse-content-Eg8yRq_t.d.ts} +69 -1
- package/dist/preview-requests.d.ts +6 -1
- package/dist/preview-requests.js +3 -1
- package/dist/protocol.d.ts +31 -5
- package/dist/protocol.js +4 -3
- package/dist/routes/media.js +26 -5
- package/dist/routes/own-photos.js +2 -1
- package/dist/schema.d.ts +13 -8
- package/dist/schema.js +13 -1
- package/dist/site-description-Cg5KNhhG.d.ts +112 -0
- package/dist/site.d.ts +2 -2
- package/dist/site.js +83 -9
- package/package.json +1 -1
- package/routes/preview.astro +8 -11
- package/dist/parse-content-C9ldnmvj.d.ts +0 -69
- package/dist/site-description-4Jx6Nq6X.d.ts +0 -36
package/dist/routes/media.js
CHANGED
|
@@ -1,28 +1,49 @@
|
|
|
1
|
+
import {
|
|
2
|
+
uploadedPhotoPath
|
|
3
|
+
} from "../chunk-QCVWHHJC.js";
|
|
1
4
|
import {
|
|
2
5
|
isUploadedPhoto
|
|
3
|
-
} from "../chunk-
|
|
6
|
+
} from "../chunk-QL4K4HR6.js";
|
|
4
7
|
|
|
5
8
|
// src/routes/media.ts
|
|
6
9
|
import { cmsOrigin } from "virtual:seamlet/config";
|
|
7
10
|
import site from "virtual:seamlet/site";
|
|
8
11
|
var NOT_FOUND = 404;
|
|
12
|
+
var BAD_GATEWAY = 502;
|
|
13
|
+
var PHOTO_TIMEOUT_MS = 1e4;
|
|
9
14
|
var GET = async ({ params }) => {
|
|
10
15
|
const fileName = params["file"];
|
|
11
16
|
const isOurs = params["site"] === site.id && isUploadedPhoto(fileName);
|
|
12
17
|
if (!isOurs) {
|
|
13
18
|
return new Response(null, { status: NOT_FOUND });
|
|
14
19
|
}
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
)
|
|
20
|
+
const url = new URL(uploadedPhotoPath(site.id, fileName), cmsOrigin);
|
|
21
|
+
const response = await photoFromCms(url);
|
|
22
|
+
if (response === void 0) {
|
|
23
|
+
return new Response(null, { status: BAD_GATEWAY });
|
|
24
|
+
}
|
|
25
|
+
if (!response.ok) {
|
|
26
|
+
const status = response.status === NOT_FOUND ? NOT_FOUND : BAD_GATEWAY;
|
|
27
|
+
return new Response(null, { status });
|
|
28
|
+
}
|
|
18
29
|
return new Response(response.body, {
|
|
19
|
-
status: response.status,
|
|
20
30
|
headers: {
|
|
21
31
|
"Content-Type": response.headers.get("Content-Type") ?? "image/webp",
|
|
22
32
|
"Cache-Control": response.headers.get("Cache-Control") ?? "no-cache"
|
|
23
33
|
}
|
|
24
34
|
});
|
|
25
35
|
};
|
|
36
|
+
async function photoFromCms(url) {
|
|
37
|
+
try {
|
|
38
|
+
return await fetch(url, { signal: AbortSignal.timeout(PHOTO_TIMEOUT_MS) });
|
|
39
|
+
} catch (error) {
|
|
40
|
+
console.warn(
|
|
41
|
+
`@seamlet/astro: the photo ${url.href} could not be taken from the CMS`,
|
|
42
|
+
error
|
|
43
|
+
);
|
|
44
|
+
return void 0;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
26
47
|
export {
|
|
27
48
|
GET
|
|
28
49
|
};
|
package/dist/schema.d.ts
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
export {
|
|
3
|
-
|
|
4
|
-
export { C as ContentIssue, P as ParsedContent, c as countCharacters, f as formatFieldPath, p as parseContent } from './parse-content-C9ldnmvj.js';
|
|
5
|
-
export { a as SITE_DESCRIPTION_PATH, S as SiteDescription, b as SitePageDescription, i as isSearchTextsGroup, r as readSiteDescription } from './site-description-4Jx6Nq6X.js';
|
|
1
|
+
import { d as ImageValue, L as LinkValue, b as FieldPath, e as Field, f as CollectionField, F as Fields, a as Content, g as RecordFields, C as ContentOf, G as GroupField, h as LinkField, T as TextField, U as UrlField, i as ChoicesField, j as TimeField, I as ImageField } from './parse-content-Eg8yRq_t.js';
|
|
2
|
+
export { k as ChoiceField, l as ChoiceOption, c as ContentIssue, D as DateField, m as FieldKind, n as FieldValue, o as ImageFocus, N as NumberField, O as OptionValue, P as ParsedContent, p as RecordOf, q as ReferenceField, r as ReferencesField, s as RichBlock, t as RichLine, u as RichSpan, R as RichText, v as RichTextField, 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, S as image, W as linesOf, X as link, Y as normalizedLine, Z as number, _ as parseContent, $ as plainTextOf, a0 as readRichText, a1 as reference, a2 as references, a3 as richText, a4 as richTextOf, a5 as text, a6 as time, a7 as toggle, a8 as url } from './parse-content-Eg8yRq_t.js';
|
|
3
|
+
export { F as FieldsReading, H as HOME_PAGE_PATH, P as PageProblemReason, a as SITE_DESCRIPTION_PATH, b as SchemaProblem, c as SchemaProblemReason, S as SiteDescription, d as SiteDescriptionProblem, e as SiteDescriptionReading, f as SitePageDescription, g as checkFields, h as checkSiteDescription, i as isSearchTextsGroup, r as readFields, j as readSiteDescription } from './site-description-Cg5KNhhG.js';
|
|
6
4
|
|
|
7
5
|
declare const FIELD_PATH_ATTRIBUTE = "data-cms-path";
|
|
8
6
|
interface FieldMarker {
|
|
@@ -91,6 +89,12 @@ type LinkKind = LinkTarget["kind"];
|
|
|
91
89
|
* is checked exactly as it is stored and rendered: spaces around it make it no address at all.
|
|
92
90
|
*/
|
|
93
91
|
declare function linkTargetOf(href: string): LinkTarget | undefined;
|
|
92
|
+
/**
|
|
93
|
+
* The address as it is when a link of a site may lead there, or undefined when it may not: what a
|
|
94
|
+
* site puts into `href` of content it renders, since content that is not saved yet (the editor's
|
|
95
|
+
* draft in the preview) may hold any address.
|
|
96
|
+
*/
|
|
97
|
+
declare function safeHref(href: string): string | undefined;
|
|
94
98
|
/**
|
|
95
99
|
* The address of a link from what a person typed: a phone or an email without `tel:` or
|
|
96
100
|
* `mailto:`, a site without `https://`.
|
|
@@ -172,8 +176,9 @@ declare function openingHoursField(options?: {
|
|
|
172
176
|
note: TextField;
|
|
173
177
|
}>;
|
|
174
178
|
|
|
175
|
-
/**
|
|
176
|
-
|
|
179
|
+
/** An object of named values: not null, not a list. */
|
|
180
|
+
type JsonObject = Readonly<Record<string, unknown>>;
|
|
181
|
+
declare function isJsonObject(value: unknown): value is JsonObject;
|
|
177
182
|
|
|
178
183
|
/** The file at the root of a site's repository. */
|
|
179
184
|
declare const SITE_CONFIG_FILE = "seamlet.json";
|
|
@@ -198,4 +203,4 @@ declare function isSiteId(value: unknown): value is string;
|
|
|
198
203
|
/** The config the input gives (the parsed `seamlet.json`); fields it does not know are left out. */
|
|
199
204
|
declare function readSiteConfig(input: unknown): SiteConfigReading;
|
|
200
205
|
|
|
201
|
-
export { ChoicesField, CollectionField, type CollectionRecords, Content, ContentOf, type ContentPath, FIELD_PATH_ATTRIBUTE, Field, type FieldMarker, FieldPath, type FieldTarget, Fields, GroupField, ImageField, ImageValue, LinkField, type LinkKind, type LinkTarget, LinkValue, RECORD_ID_KEY, REVEALED_ATTRIBUTE, REVEAL_ATTRIBUTE, REVEAL_EVENT, RecordFields, SITE_CONFIG_FILE, type SiteConfig, type SiteConfigField, type SiteConfigReading, TextField, TimeField, UrlField, WEEKDAYS, collectionAt, contactsFields, editable, findRecord, hrefOf, imagePosition, isRecordId, isSiteId, linkTargetOf, newRecord, openingHoursField,
|
|
206
|
+
export { ChoicesField, CollectionField, type CollectionRecords, Content, ContentOf, type ContentPath, FIELD_PATH_ATTRIBUTE, Field, type FieldMarker, FieldPath, type FieldTarget, Fields, GroupField, ImageField, ImageValue, type JsonObject, LinkField, type LinkKind, type LinkTarget, LinkValue, RECORD_ID_KEY, REVEALED_ATTRIBUTE, REVEAL_ATTRIBUTE, REVEAL_EVENT, RecordFields, SITE_CONFIG_FILE, type SiteConfig, type SiteConfigField, type SiteConfigReading, TextField, TimeField, UrlField, WEEKDAYS, collectionAt, contactsFields, editable, findRecord, hrefOf, imagePosition, isJsonObject, isRecordId, isSiteId, linkTargetOf, newRecord, openingHoursField, readSiteConfig, resolveFieldPath, safeHref, seoFields, withRecordIds };
|
package/dist/schema.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
FIELD_PATH_ATTRIBUTE,
|
|
3
|
+
HOME_PAGE_PATH,
|
|
3
4
|
RECORD_ID_KEY,
|
|
4
5
|
REVEALED_ATTRIBUTE,
|
|
5
6
|
REVEAL_ATTRIBUTE,
|
|
@@ -8,6 +9,8 @@ import {
|
|
|
8
9
|
SITE_DESCRIPTION_PATH,
|
|
9
10
|
WEEKDAYS,
|
|
10
11
|
asOneParagraph,
|
|
12
|
+
checkFields,
|
|
13
|
+
checkSiteDescription,
|
|
11
14
|
choice,
|
|
12
15
|
choices,
|
|
13
16
|
collection,
|
|
@@ -25,9 +28,11 @@ import {
|
|
|
25
28
|
hrefOf,
|
|
26
29
|
image,
|
|
27
30
|
imagePosition,
|
|
31
|
+
isJsonObject,
|
|
28
32
|
isRecordId,
|
|
29
33
|
isSearchTextsGroup,
|
|
30
34
|
isSiteId,
|
|
35
|
+
linesOf,
|
|
31
36
|
link,
|
|
32
37
|
linkTargetOf,
|
|
33
38
|
newRecord,
|
|
@@ -45,15 +50,17 @@ import {
|
|
|
45
50
|
resolveFieldPath,
|
|
46
51
|
richText,
|
|
47
52
|
richTextOf,
|
|
53
|
+
safeHref,
|
|
48
54
|
seoFields,
|
|
49
55
|
text,
|
|
50
56
|
time,
|
|
51
57
|
toggle,
|
|
52
58
|
url,
|
|
53
59
|
withRecordIds
|
|
54
|
-
} from "./chunk-
|
|
60
|
+
} from "./chunk-ZS32CW5F.js";
|
|
55
61
|
export {
|
|
56
62
|
FIELD_PATH_ATTRIBUTE,
|
|
63
|
+
HOME_PAGE_PATH,
|
|
57
64
|
RECORD_ID_KEY,
|
|
58
65
|
REVEALED_ATTRIBUTE,
|
|
59
66
|
REVEAL_ATTRIBUTE,
|
|
@@ -62,6 +69,8 @@ export {
|
|
|
62
69
|
SITE_DESCRIPTION_PATH,
|
|
63
70
|
WEEKDAYS,
|
|
64
71
|
asOneParagraph,
|
|
72
|
+
checkFields,
|
|
73
|
+
checkSiteDescription,
|
|
65
74
|
choice,
|
|
66
75
|
choices,
|
|
67
76
|
collection,
|
|
@@ -79,9 +88,11 @@ export {
|
|
|
79
88
|
hrefOf,
|
|
80
89
|
image,
|
|
81
90
|
imagePosition,
|
|
91
|
+
isJsonObject,
|
|
82
92
|
isRecordId,
|
|
83
93
|
isSearchTextsGroup,
|
|
84
94
|
isSiteId,
|
|
95
|
+
linesOf,
|
|
85
96
|
link,
|
|
86
97
|
linkTargetOf,
|
|
87
98
|
newRecord,
|
|
@@ -99,6 +110,7 @@ export {
|
|
|
99
110
|
resolveFieldPath,
|
|
100
111
|
richText,
|
|
101
112
|
richTextOf,
|
|
113
|
+
safeHref,
|
|
102
114
|
seoFields,
|
|
103
115
|
text,
|
|
104
116
|
time,
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { F as Fields, a as Content, c as ContentIssue } from './parse-content-Eg8yRq_t.js';
|
|
2
|
+
|
|
3
|
+
/** Why a schema cannot be read. */
|
|
4
|
+
type SchemaProblemReason =
|
|
5
|
+
/** Not an object of fields, or a field that is not an object. */
|
|
6
|
+
"not-fields"
|
|
7
|
+
/** A name that is empty, holds the separator of paths (`.`) or is a number, which a path reads as a record. */
|
|
8
|
+
| "invalid-name" | "invalid-label" | "unknown-kind"
|
|
9
|
+
/** An option of the field (`maxLength`, `inline`…) is there but not of its kind. */
|
|
10
|
+
| "invalid-option" | "invalid-choice-options"
|
|
11
|
+
/** A record has a field named `id`, which is the record's own. */
|
|
12
|
+
| "record-id-field"
|
|
13
|
+
/** `itemTitle` names no text or link field of the record. */
|
|
14
|
+
| "invalid-item-title"
|
|
15
|
+
/** A reference names a collection the schema does not have. */
|
|
16
|
+
| "unknown-collection";
|
|
17
|
+
interface SchemaProblem {
|
|
18
|
+
/** The names of the fields down to the field with the problem; empty for the schema itself. */
|
|
19
|
+
readonly path: readonly string[];
|
|
20
|
+
readonly reason: SchemaProblemReason;
|
|
21
|
+
}
|
|
22
|
+
type FieldsReading = {
|
|
23
|
+
readonly status: "read";
|
|
24
|
+
readonly fields: Fields;
|
|
25
|
+
} | {
|
|
26
|
+
readonly status: "invalid";
|
|
27
|
+
readonly problem: SchemaProblem;
|
|
28
|
+
};
|
|
29
|
+
/** The schema the input describes, or undefined when it is not one. */
|
|
30
|
+
declare function readFields(input: unknown): Fields | undefined;
|
|
31
|
+
/** The schema the input describes, or the first problem that keeps it from being one. */
|
|
32
|
+
declare function checkFields(input: unknown): FieldsReading;
|
|
33
|
+
|
|
34
|
+
/** Where a site hands out its description, on the origin of its preview. */
|
|
35
|
+
declare const SITE_DESCRIPTION_PATH = "/seamlet/site.json";
|
|
36
|
+
/** A page of a site, which the site renders itself. */
|
|
37
|
+
interface SitePageDescription {
|
|
38
|
+
/** The address of the page on the site: `/` for the first page, `/menu` for others. */
|
|
39
|
+
readonly path: string;
|
|
40
|
+
/** How the editor names the page in its list of pages. */
|
|
41
|
+
readonly title: string;
|
|
42
|
+
/**
|
|
43
|
+
* Where the content holds what search engines and link previews show for the page
|
|
44
|
+
* (`seo.menu`): a group of a `title` and a `description` text.
|
|
45
|
+
*/
|
|
46
|
+
readonly searchTexts?: string;
|
|
47
|
+
}
|
|
48
|
+
interface SiteDescription {
|
|
49
|
+
/** The address of the site in the CMS (`/sites/<id>`). */
|
|
50
|
+
readonly id: string;
|
|
51
|
+
/** How the CMS names the site. */
|
|
52
|
+
readonly title: string;
|
|
53
|
+
readonly schema: Fields;
|
|
54
|
+
/** The content shown and edited until the first save. */
|
|
55
|
+
readonly initialContent: Content;
|
|
56
|
+
/** The pages of the site; the first is the home page at `/`. */
|
|
57
|
+
readonly pages: readonly [SitePageDescription, ...SitePageDescription[]];
|
|
58
|
+
}
|
|
59
|
+
/** Why a site's description cannot be read: what to fix in the site. */
|
|
60
|
+
type SiteDescriptionProblem = {
|
|
61
|
+
readonly code: "not-a-description";
|
|
62
|
+
} | {
|
|
63
|
+
readonly code: "invalid-id";
|
|
64
|
+
} | {
|
|
65
|
+
readonly code: "invalid-title";
|
|
66
|
+
} | {
|
|
67
|
+
readonly code: "invalid-schema";
|
|
68
|
+
readonly problem: SchemaProblem;
|
|
69
|
+
}
|
|
70
|
+
/** The initial content is not content the CMS could save. */
|
|
71
|
+
| {
|
|
72
|
+
readonly code: "invalid-initial-content";
|
|
73
|
+
readonly issues: readonly ContentIssue[];
|
|
74
|
+
} | {
|
|
75
|
+
readonly code: "no-pages";
|
|
76
|
+
}
|
|
77
|
+
/** The page at this place of the list is not a page, or its path or title is not one. */
|
|
78
|
+
| {
|
|
79
|
+
readonly code: "invalid-page";
|
|
80
|
+
readonly index: number;
|
|
81
|
+
readonly reason: PageProblemReason;
|
|
82
|
+
} | {
|
|
83
|
+
readonly code: "home-page-not-first";
|
|
84
|
+
} | {
|
|
85
|
+
readonly code: "repeated-page-path";
|
|
86
|
+
readonly path: string;
|
|
87
|
+
};
|
|
88
|
+
type PageProblemReason = "not-a-page"
|
|
89
|
+
/** Not `/`, or not lower-case segments without a trailing slash, such as `/menu`. */
|
|
90
|
+
| "invalid-path" | "invalid-title"
|
|
91
|
+
/** Not where the content holds a group of a `title` and a `description` text. */
|
|
92
|
+
| "invalid-search-texts";
|
|
93
|
+
type SiteDescriptionReading = {
|
|
94
|
+
readonly status: "read";
|
|
95
|
+
readonly description: SiteDescription;
|
|
96
|
+
} | {
|
|
97
|
+
readonly status: "invalid";
|
|
98
|
+
readonly problems: readonly SiteDescriptionProblem[];
|
|
99
|
+
};
|
|
100
|
+
/** The first page of every site. */
|
|
101
|
+
declare const HOME_PAGE_PATH = "/";
|
|
102
|
+
/**
|
|
103
|
+
* The description the input gives, or undefined when it is not a whole and sound one: a known
|
|
104
|
+
* schema, an initial content with nothing to fix, the home page first, each page once.
|
|
105
|
+
*/
|
|
106
|
+
declare function readSiteDescription(input: unknown): SiteDescription | undefined;
|
|
107
|
+
/** The description the input gives, or every problem that keeps it from being whole and sound. */
|
|
108
|
+
declare function checkSiteDescription(input: unknown): SiteDescriptionReading;
|
|
109
|
+
/** Whether the path names a group of a `title` and a `description` text. */
|
|
110
|
+
declare function isSearchTextsGroup(schema: Fields, path: string): boolean;
|
|
111
|
+
|
|
112
|
+
export { type FieldsReading as F, HOME_PAGE_PATH as H, type PageProblemReason as P, type SiteDescription as S, SITE_DESCRIPTION_PATH as a, type SchemaProblem as b, type SchemaProblemReason as c, type SiteDescriptionProblem as d, type SiteDescriptionReading as e, type SitePageDescription as f, checkFields as g, checkSiteDescription as h, isSearchTextsGroup as i, readSiteDescription as j, readFields as r };
|
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 { F as Fields, C as ContentOf } from './content-
|
|
4
|
-
import { S as SiteDescription } from './site-description-
|
|
3
|
+
import { F as Fields, C as ContentOf } from './parse-content-Eg8yRq_t.js';
|
|
4
|
+
import { S as SiteDescription } from './site-description-Cg5KNhhG.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 {
|
package/dist/site.js
CHANGED
|
@@ -1,13 +1,34 @@
|
|
|
1
1
|
import {
|
|
2
|
+
HOME_PAGE_PATH,
|
|
3
|
+
checkSiteDescription,
|
|
4
|
+
formatFieldPath,
|
|
2
5
|
parseContent
|
|
3
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-ZS32CW5F.js";
|
|
7
|
+
import {
|
|
8
|
+
SITE_MODULE_FILE
|
|
9
|
+
} from "./chunk-QCVWHHJC.js";
|
|
4
10
|
|
|
5
11
|
// src/site.ts
|
|
6
12
|
import { cmsOrigin, siteId, siteTitle } from "virtual:seamlet/config";
|
|
7
13
|
|
|
8
14
|
// src/published-answers.ts
|
|
9
15
|
var NOT_FOUND = 404;
|
|
16
|
+
var PUBLISHED_CONTENT_TIMEOUT_MS = 3e4;
|
|
10
17
|
var UNKNOWN_SITE = "unknown-site";
|
|
18
|
+
async function fetchPublishedAnswer(url) {
|
|
19
|
+
let response;
|
|
20
|
+
try {
|
|
21
|
+
response = await fetch(url, {
|
|
22
|
+
signal: AbortSignal.timeout(PUBLISHED_CONTENT_TIMEOUT_MS)
|
|
23
|
+
});
|
|
24
|
+
} catch (error) {
|
|
25
|
+
throw new Error(
|
|
26
|
+
`Loading the published content from ${url.href} failed: the CMS did not answer`,
|
|
27
|
+
{ cause: error }
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
return readPublishedAnswer(response, url);
|
|
31
|
+
}
|
|
11
32
|
async function readPublishedAnswer(response, url) {
|
|
12
33
|
if (response.ok)
|
|
13
34
|
return { status: "published", content: await response.json() };
|
|
@@ -35,21 +56,74 @@ function describeSite(identity, definition) {
|
|
|
35
56
|
...page.searchTexts === void 0 ? {} : { searchTexts: page.searchTexts }
|
|
36
57
|
})
|
|
37
58
|
);
|
|
38
|
-
const
|
|
39
|
-
if (home === void 0) {
|
|
40
|
-
throw new Error(`The site "${identity.id}" has no pages`);
|
|
41
|
-
}
|
|
42
|
-
return {
|
|
59
|
+
const description = {
|
|
43
60
|
id: identity.id,
|
|
44
61
|
title: identity.title,
|
|
45
62
|
schema: definition.schema,
|
|
46
63
|
initialContent: definition.initialContent,
|
|
47
|
-
pages
|
|
64
|
+
pages
|
|
48
65
|
};
|
|
66
|
+
const asSent = JSON.parse(JSON.stringify(description));
|
|
67
|
+
const reading = checkSiteDescription(asSent);
|
|
68
|
+
if (reading.status === "invalid") {
|
|
69
|
+
throw new Error(refusalMessage(identity.id, reading.problems));
|
|
70
|
+
}
|
|
71
|
+
return reading.description;
|
|
72
|
+
}
|
|
73
|
+
function refusalMessage(id, problems) {
|
|
74
|
+
const lines = problems.map((problem) => ` - ${problemText(problem)}`);
|
|
75
|
+
return [
|
|
76
|
+
`@seamlet/astro: the CMS would refuse the site "${id}" (${SITE_MODULE_FILE}):`,
|
|
77
|
+
...lines
|
|
78
|
+
].join("\n");
|
|
79
|
+
}
|
|
80
|
+
function problemText(problem) {
|
|
81
|
+
switch (problem.code) {
|
|
82
|
+
case "not-a-description":
|
|
83
|
+
return "the site describes nothing";
|
|
84
|
+
case "invalid-id":
|
|
85
|
+
return "its id in seamlet.json is not one";
|
|
86
|
+
case "invalid-title":
|
|
87
|
+
return "its title in seamlet.json is empty";
|
|
88
|
+
case "invalid-schema": {
|
|
89
|
+
const path = formatFieldPath(problem.problem.path);
|
|
90
|
+
const where = path === "" ? "the schema" : `the field "${path}"`;
|
|
91
|
+
return `${where}: ${SCHEMA_PROBLEM_TEXTS[problem.problem.reason]}`;
|
|
92
|
+
}
|
|
93
|
+
case "invalid-initial-content":
|
|
94
|
+
return `the initial content: ${problem.issues.map(issueText).join("; ")}`;
|
|
95
|
+
case "no-pages":
|
|
96
|
+
return "it has no pages";
|
|
97
|
+
case "invalid-page":
|
|
98
|
+
return `page ${String(problem.index + 1)} of pages: ${PAGE_PROBLEM_TEXTS[problem.reason]}`;
|
|
99
|
+
case "home-page-not-first":
|
|
100
|
+
return 'the first of the pages is not the home page "/"';
|
|
101
|
+
case "repeated-page-path":
|
|
102
|
+
return `the page "${problem.path}" is named twice`;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
var SCHEMA_PROBLEM_TEXTS = {
|
|
106
|
+
"not-fields": "not fields of the schema language",
|
|
107
|
+
"invalid-name": "a field's name must not be empty, hold a dot or be a number",
|
|
108
|
+
"invalid-label": "its label is not a text",
|
|
109
|
+
"unknown-kind": "a kind of field the schema language does not have",
|
|
110
|
+
"invalid-option": "an option of the field is not of its kind",
|
|
111
|
+
"invalid-choice-options": "its options are not a list of { value, label }",
|
|
112
|
+
"record-id-field": "a record has its own id: name the field otherwise",
|
|
113
|
+
"invalid-item-title": "itemTitle names no text or link field of the record",
|
|
114
|
+
"unknown-collection": "it refers to a collection the schema does not have"
|
|
115
|
+
};
|
|
116
|
+
var PAGE_PROBLEM_TEXTS = {
|
|
117
|
+
"not-a-page": "not a page",
|
|
118
|
+
"invalid-path": 'its address is not "/" or lower-case segments without a trailing slash, such as "/menu"',
|
|
119
|
+
"invalid-title": "its title is empty",
|
|
120
|
+
"invalid-search-texts": "searchTexts names no group of a title and a description text"
|
|
121
|
+
};
|
|
122
|
+
function issueText(issue) {
|
|
123
|
+
return `"${formatFieldPath(issue.path)}" is ${issue.code}`;
|
|
49
124
|
}
|
|
50
125
|
|
|
51
126
|
// src/site.ts
|
|
52
|
-
var HOME_PAGE_PATH = "/";
|
|
53
127
|
function defineSeamletSite(definition) {
|
|
54
128
|
const pages = new Map(Object.entries(definition.pages));
|
|
55
129
|
return {
|
|
@@ -63,7 +137,7 @@ function defineSeamletSite(definition) {
|
|
|
63
137
|
}
|
|
64
138
|
async function loadPublishedContent(schema) {
|
|
65
139
|
const url = new URL(`/api/sites/${siteId}/published`, cmsOrigin);
|
|
66
|
-
const answer = await
|
|
140
|
+
const answer = await fetchPublishedAnswer(url);
|
|
67
141
|
const content = answer.status === "published" ? answer.content : await initialContentOfSite();
|
|
68
142
|
return parseContent(schema, content).content;
|
|
69
143
|
}
|
package/package.json
CHANGED
package/routes/preview.astro
CHANGED
|
@@ -4,29 +4,26 @@
|
|
|
4
4
|
// content; POST shows the content in its JSON body, never saved: the bridge renders the editor's
|
|
5
5
|
// draft this way.
|
|
6
6
|
import { parseContent } from "@seamlet/astro/schema";
|
|
7
|
-
import { readDraft } from "@seamlet/astro/preview-requests";
|
|
7
|
+
import { previewHeadersOf, readDraft } from "@seamlet/astro/preview-requests";
|
|
8
8
|
import { cmsOrigin } from "virtual:seamlet/config";
|
|
9
9
|
import site from "virtual:seamlet/site";
|
|
10
10
|
|
|
11
11
|
const NOT_FOUND = 404;
|
|
12
12
|
|
|
13
|
+
const headers = previewHeadersOf(cmsOrigin);
|
|
14
|
+
for (const [name, value] of Object.entries(headers)) {
|
|
15
|
+
Astro.response.headers.set(name, value);
|
|
16
|
+
}
|
|
17
|
+
|
|
13
18
|
const Page = site.pageAt(Astro.params.page);
|
|
14
19
|
if (Page === undefined) {
|
|
15
|
-
return new Response(null, { status: NOT_FOUND });
|
|
20
|
+
return new Response(null, { status: NOT_FOUND, headers });
|
|
16
21
|
}
|
|
17
22
|
|
|
18
|
-
// Only the CMS's editor frames the preview, and nobody indexes or keeps it.
|
|
19
|
-
Astro.response.headers.set(
|
|
20
|
-
"Content-Security-Policy",
|
|
21
|
-
`frame-ancestors ${cmsOrigin}`,
|
|
22
|
-
);
|
|
23
|
-
Astro.response.headers.set("X-Robots-Tag", "noindex");
|
|
24
|
-
Astro.response.headers.set("Cache-Control", "no-store");
|
|
25
|
-
|
|
26
23
|
const reading =
|
|
27
24
|
Astro.request.method === "POST" ? await readDraft(Astro.request) : undefined;
|
|
28
25
|
if (reading !== undefined && !reading.isRead) {
|
|
29
|
-
return reading.response;
|
|
26
|
+
return new Response(null, { status: reading.response.status, headers });
|
|
30
27
|
}
|
|
31
28
|
const content =
|
|
32
29
|
reading === undefined
|
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
import { F as Fields, C as ContentOf } from './content-6k-E7NWf.js';
|
|
2
|
-
|
|
3
|
-
/** Where a value lives in the content: field names and, inside a collection, record indexes. */
|
|
4
|
-
type FieldPath = readonly (string | number)[];
|
|
5
|
-
type ContentIssue = {
|
|
6
|
-
readonly path: FieldPath;
|
|
7
|
-
readonly code: "invalid-type";
|
|
8
|
-
} | {
|
|
9
|
-
readonly path: FieldPath;
|
|
10
|
-
readonly code: "invalid-date";
|
|
11
|
-
} | {
|
|
12
|
-
readonly path: FieldPath;
|
|
13
|
-
readonly code: "invalid-time";
|
|
14
|
-
} | {
|
|
15
|
-
readonly path: FieldPath;
|
|
16
|
-
readonly code: "too-long";
|
|
17
|
-
readonly maxLength: number;
|
|
18
|
-
} | {
|
|
19
|
-
readonly path: FieldPath;
|
|
20
|
-
readonly code: "below-min";
|
|
21
|
-
readonly min: number;
|
|
22
|
-
} | {
|
|
23
|
-
readonly path: FieldPath;
|
|
24
|
-
readonly code: "above-max";
|
|
25
|
-
readonly max: number;
|
|
26
|
-
} | {
|
|
27
|
-
readonly path: FieldPath;
|
|
28
|
-
readonly code: "missing-image-src";
|
|
29
|
-
} | {
|
|
30
|
-
readonly path: FieldPath;
|
|
31
|
-
readonly code: "missing-link-href";
|
|
32
|
-
} | {
|
|
33
|
-
readonly path: FieldPath;
|
|
34
|
-
readonly code: "unsupported-link-href";
|
|
35
|
-
} | {
|
|
36
|
-
readonly path: FieldPath;
|
|
37
|
-
readonly code: "unsupported-url";
|
|
38
|
-
} | {
|
|
39
|
-
readonly path: FieldPath;
|
|
40
|
-
readonly code: "unknown-choice";
|
|
41
|
-
readonly value: string;
|
|
42
|
-
} | {
|
|
43
|
-
readonly path: FieldPath;
|
|
44
|
-
readonly code: "missing-record";
|
|
45
|
-
readonly id: string;
|
|
46
|
-
};
|
|
47
|
-
interface ParsedContent<C> {
|
|
48
|
-
readonly content: C;
|
|
49
|
-
readonly issues: readonly ContentIssue[];
|
|
50
|
-
}
|
|
51
|
-
declare function formatFieldPath(path: FieldPath): string;
|
|
52
|
-
/**
|
|
53
|
-
* The length that `maxLength` limits: characters as people see them (grapheme clusters), so
|
|
54
|
-
* "й" and "👍🏽" are one character each, whatever number of code units they take.
|
|
55
|
-
*/
|
|
56
|
-
declare function countCharacters(text: string): number;
|
|
57
|
-
/**
|
|
58
|
-
* Brings any input to the shape of the schema, so the result is always content of that schema:
|
|
59
|
-
* - a field missing from the input (the schema grew after the content was saved) gets its default;
|
|
60
|
-
* - a value of the wrong type or format gets the default too, and an issue;
|
|
61
|
-
* - a value of the right type that breaks a rule of its field (too long, below the minimum) is
|
|
62
|
-
* kept, with an issue: nothing a person typed is lost;
|
|
63
|
-
* - keys the schema does not know are dropped; a record keeps its id (see withRecordIds);
|
|
64
|
-
* - a reference to a record its collection does not have (removed since) is kept, with an issue.
|
|
65
|
-
* Content with issues is valid to show and edit, but not to save.
|
|
66
|
-
*/
|
|
67
|
-
declare function parseContent<F extends Fields>(schema: F, input: unknown): ParsedContent<ContentOf<F>>;
|
|
68
|
-
|
|
69
|
-
export { type ContentIssue as C, type FieldPath as F, type ParsedContent as P, countCharacters as c, formatFieldPath as f, parseContent as p };
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
import { F as Fields, a as Content } from './content-6k-E7NWf.js';
|
|
2
|
-
|
|
3
|
-
/** Where a site hands out its description, on the origin of its preview. */
|
|
4
|
-
declare const SITE_DESCRIPTION_PATH = "/seamlet/site.json";
|
|
5
|
-
/** A page of a site, which the site renders itself. */
|
|
6
|
-
interface SitePageDescription {
|
|
7
|
-
/** The address of the page on the site: `/` for the first page, `/menu` for others. */
|
|
8
|
-
readonly path: string;
|
|
9
|
-
/** How the editor names the page in its list of pages. */
|
|
10
|
-
readonly title: string;
|
|
11
|
-
/**
|
|
12
|
-
* Where the content holds what search engines and link previews show for the page
|
|
13
|
-
* (`seo.menu`): a group of a `title` and a `description` text.
|
|
14
|
-
*/
|
|
15
|
-
readonly searchTexts?: string;
|
|
16
|
-
}
|
|
17
|
-
interface SiteDescription {
|
|
18
|
-
/** The address of the site in the CMS (`/sites/<id>`). */
|
|
19
|
-
readonly id: string;
|
|
20
|
-
/** How the CMS names the site. */
|
|
21
|
-
readonly title: string;
|
|
22
|
-
readonly schema: Fields;
|
|
23
|
-
/** The content shown and edited until the first save. */
|
|
24
|
-
readonly initialContent: Content;
|
|
25
|
-
/** The pages of the site; the first is the home page at `/`. */
|
|
26
|
-
readonly pages: readonly [SitePageDescription, ...SitePageDescription[]];
|
|
27
|
-
}
|
|
28
|
-
/**
|
|
29
|
-
* The description the input gives, or undefined when it is not a whole and sound one: a known
|
|
30
|
-
* schema, an initial content with nothing to fix, the home page first, each page once.
|
|
31
|
-
*/
|
|
32
|
-
declare function readSiteDescription(input: unknown): SiteDescription | undefined;
|
|
33
|
-
/** Whether the path names a group of a `title` and a `description` text. */
|
|
34
|
-
declare function isSearchTextsGroup(schema: Fields, path: string): boolean;
|
|
35
|
-
|
|
36
|
-
export { type SiteDescription as S, SITE_DESCRIPTION_PATH as a, type SitePageDescription as b, isSearchTextsGroup as i, readSiteDescription as r };
|