@seamlet/astro 0.0.0-stage → 0.1.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/dist/index.js ADDED
@@ -0,0 +1,50 @@
1
+ // src/index.ts
2
+ import { fileURLToPath } from "url";
3
+ var CONFIG_MODULE = "virtual:seamlet/config";
4
+ var SITE_MODULE = "virtual:seamlet/site";
5
+ var RESOLVED_CONFIG_MODULE = `\0${CONFIG_MODULE}`;
6
+ function seamlet(options) {
7
+ const cmsOrigin = new URL(options.cms).origin;
8
+ return {
9
+ name: "@seamlet/astro",
10
+ hooks: {
11
+ "astro:config:setup": ({
12
+ config,
13
+ injectRoute,
14
+ injectScript,
15
+ updateConfig
16
+ }) => {
17
+ const siteModule = fileURLToPath(new URL(options.site, config.root));
18
+ updateConfig({
19
+ vite: {
20
+ plugins: [
21
+ {
22
+ name: "seamlet-modules",
23
+ resolveId: (id) => {
24
+ if (id === CONFIG_MODULE) return RESOLVED_CONFIG_MODULE;
25
+ if (id === SITE_MODULE) return siteModule;
26
+ return void 0;
27
+ },
28
+ load: (id) => id === RESOLVED_CONFIG_MODULE ? `export const cmsOrigin = ${JSON.stringify(cmsOrigin)};` : void 0
29
+ }
30
+ ]
31
+ }
32
+ });
33
+ injectScript("page", `import "@seamlet/astro/client";`);
34
+ injectRoute({
35
+ pattern: "/preview/[...page]",
36
+ entrypoint: "@seamlet/astro/routes/preview.astro",
37
+ prerender: false
38
+ });
39
+ injectRoute({
40
+ pattern: "/media/[site]/[file]",
41
+ entrypoint: "@seamlet/astro/routes/media",
42
+ prerender: false
43
+ });
44
+ }
45
+ }
46
+ };
47
+ }
48
+ export {
49
+ seamlet as default
50
+ };
@@ -0,0 +1,143 @@
1
+ import { F as Fields, d as Content } from './content-DKVM1CUQ.js';
2
+ import { F as FieldPath } from './parse-content-DF2dL2FG.js';
3
+
4
+ interface Box {
5
+ readonly top: number;
6
+ readonly left: number;
7
+ readonly width: number;
8
+ readonly height: number;
9
+ }
10
+ interface Viewport {
11
+ readonly width: number;
12
+ readonly height: number;
13
+ }
14
+
15
+ type Shortcut = "save" | "undo" | "redo";
16
+ declare function shortcutOf(event: KeyboardEvent): Shortcut | undefined;
17
+
18
+ type InlineEditingOutcome = "kept" | "reverted";
19
+
20
+ /**
21
+ * The version of what the two say to each other. A site updates @seamlet/astro when it likes, so the
22
+ * editor may meet a bridge of another version: it tells by this number, said with `ready`, instead
23
+ * of misreading the page. Raised whenever a message changes in a way the other side cannot read.
24
+ */
25
+ declare const PROTOCOL_VERSION = 1;
26
+ interface Point {
27
+ readonly x: number;
28
+ readonly y: number;
29
+ }
30
+ /** A marked element of the page and where it is. */
31
+ interface MarkerBox {
32
+ readonly path: string;
33
+ readonly box: Box;
34
+ }
35
+ /** A link on the page that is not a field. */
36
+ interface LinkBox {
37
+ /** The address as the site wrote it. */
38
+ readonly href: string;
39
+ readonly box: Box;
40
+ }
41
+ /** Where everything the editor draws over is on the page now. */
42
+ interface PageLayout {
43
+ /** Every marked element that takes space, in the order of the page. */
44
+ readonly markers: readonly MarkerBox[];
45
+ readonly viewport: Viewport;
46
+ /** The link under the pointer, if it is not a field. */
47
+ readonly link: LinkBox | undefined;
48
+ }
49
+ /** A key the editor acts on: its shortcuts, and Escape that closes what is open. */
50
+ type EditorKey = Shortcut | "escape";
51
+ /** What the page tells the editor. */
52
+ type PageMessage =
53
+ /** The bridge runs and waits for the editor to connect; it speaks this version of the protocol. */
54
+ {
55
+ readonly type: "ready";
56
+ readonly protocol: number;
57
+ } | {
58
+ readonly type: "layout";
59
+ readonly layout: PageLayout;
60
+ }
61
+ /** The pointer moved onto a marked element, or off all of them. */
62
+ | {
63
+ readonly type: "hover";
64
+ readonly path: string | undefined;
65
+ }
66
+ /** A click on a marked element, or outside all of them. */
67
+ | {
68
+ readonly type: "click";
69
+ readonly target: {
70
+ readonly path: string;
71
+ readonly point: Point;
72
+ } | undefined;
73
+ } | {
74
+ readonly type: "link-click";
75
+ readonly href: string;
76
+ } | {
77
+ readonly type: "key";
78
+ readonly key: EditorKey;
79
+ }
80
+ /** A file is dragged over the page: over a photo that takes it, or elsewhere. */
81
+ | {
82
+ readonly type: "file-drag";
83
+ readonly path: string | undefined;
84
+ } | {
85
+ readonly type: "file-drag-end";
86
+ } | {
87
+ readonly type: "file-drop";
88
+ readonly path: string;
89
+ readonly file: File;
90
+ } | {
91
+ readonly type: "typing-started";
92
+ readonly path: string;
93
+ }
94
+ /** The element does not show the text as it is: the field is edited in a popover. */
95
+ | {
96
+ readonly type: "typing-refused";
97
+ readonly path: string;
98
+ } | {
99
+ readonly type: "typed";
100
+ readonly path: string;
101
+ readonly text: string;
102
+ } | {
103
+ readonly type: "typing-ended";
104
+ readonly path: string;
105
+ readonly outcome: InlineEditingOutcome;
106
+ } | {
107
+ readonly type: "rendered";
108
+ } | {
109
+ readonly type: "render-failed";
110
+ };
111
+ /** What the editor tells the page. */
112
+ type EditorMessage =
113
+ /** The editor took the page; the schema tells the bridge which fields take a photo. */
114
+ {
115
+ readonly type: "connect";
116
+ readonly schema: Fields;
117
+ }
118
+ /** Show the page with this content. */
119
+ | {
120
+ readonly type: "render";
121
+ readonly content: Content;
122
+ }
123
+ /** Type the text of the field right in its element, if the element shows it as it is. */
124
+ | {
125
+ readonly type: "start-typing";
126
+ readonly path: string;
127
+ readonly text: string;
128
+ readonly isSingleLine: boolean;
129
+ readonly point: Point;
130
+ } | {
131
+ readonly type: "stop-typing";
132
+ }
133
+ /** Bring the field, or the closest marked element around it, into view. */
134
+ | {
135
+ readonly type: "scroll-to";
136
+ readonly path: FieldPath;
137
+ };
138
+ /** The message the page sent, or undefined for anything that is not one. */
139
+ declare function readPageMessage(data: unknown): PageMessage | undefined;
140
+ /** The message the editor sent, or undefined for anything that is not one. */
141
+ declare function readEditorMessage(data: unknown): EditorMessage | undefined;
142
+
143
+ export { type Box as B, type EditorMessage as E, type InlineEditingOutcome as I, type LinkBox as L, type MarkerBox as M, type PageMessage as P, type Shortcut as S, type Viewport as V, type EditorKey as a, PROTOCOL_VERSION as b, type PageLayout as c, type Point as d, readPageMessage as e, readEditorMessage as r, shortcutOf as s };
@@ -0,0 +1,54 @@
1
+ import { F as Fields, C as ContentOf } from './content-DKVM1CUQ.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: "too-long";
14
+ readonly maxLength: number;
15
+ } | {
16
+ readonly path: FieldPath;
17
+ readonly code: "below-min";
18
+ readonly min: number;
19
+ } | {
20
+ readonly path: FieldPath;
21
+ readonly code: "above-max";
22
+ readonly max: number;
23
+ } | {
24
+ readonly path: FieldPath;
25
+ readonly code: "missing-image-src";
26
+ } | {
27
+ readonly path: FieldPath;
28
+ readonly code: "missing-link-href";
29
+ } | {
30
+ readonly path: FieldPath;
31
+ readonly code: "unsupported-link-href";
32
+ };
33
+ interface ParsedContent<C> {
34
+ readonly content: C;
35
+ readonly issues: readonly ContentIssue[];
36
+ }
37
+ declare function formatFieldPath(path: FieldPath): string;
38
+ /**
39
+ * The length that `maxLength` limits: characters as people see them (grapheme clusters), so
40
+ * "й" and "👍🏽" are one character each, whatever number of code units they take.
41
+ */
42
+ declare function countCharacters(text: string): number;
43
+ /**
44
+ * Brings any input to the shape of the schema, so the result is always content of that schema:
45
+ * - a field missing from the input (the schema grew after the content was saved) gets its default;
46
+ * - a value of the wrong type or format gets the default too, and an issue;
47
+ * - a value of the right type that breaks a rule of its field (too long, below the minimum) is
48
+ * kept, with an issue: nothing a person typed is lost;
49
+ * - keys the schema does not know are dropped.
50
+ * Content with issues is valid to show and edit, but not to save.
51
+ */
52
+ declare function parseContent<F extends Fields>(schema: F, input: unknown): ParsedContent<ContentOf<F>>;
53
+
54
+ export { type ContentIssue as C, type FieldPath as F, type ParsedContent as P, countCharacters as c, formatFieldPath as f, parseContent as p };
@@ -0,0 +1,18 @@
1
+ /** Far more than the content of a site ever takes; a bigger body is not a draft. */
2
+ declare const MAX_DRAFT_BYTES: number;
3
+ type DraftReading = {
4
+ readonly isRead: true;
5
+ readonly draft: unknown;
6
+ } | {
7
+ readonly isRead: false;
8
+ readonly response: Response;
9
+ };
10
+ /**
11
+ * The draft in the JSON body of a request. A form of another site cannot send JSON, so only the
12
+ * page's own bridge renders a draft.
13
+ */
14
+ declare function readDraft(request: Request): Promise<DraftReading>;
15
+ /** Whether a name can be a photo uploaded to the CMS: the hash of its bytes, as WebP. */
16
+ declare function isUploadedPhoto(fileName: string | undefined): fileName is string;
17
+
18
+ export { type DraftReading, MAX_DRAFT_BYTES, isUploadedPhoto, readDraft };
@@ -0,0 +1,10 @@
1
+ import {
2
+ MAX_DRAFT_BYTES,
3
+ isUploadedPhoto,
4
+ readDraft
5
+ } from "./chunk-P2OQOZBC.js";
6
+ export {
7
+ MAX_DRAFT_BYTES,
8
+ isUploadedPhoto,
9
+ readDraft
10
+ };
@@ -0,0 +1,19 @@
1
+ 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, S as Shortcut, V as Viewport, r as readEditorMessage, e as readPageMessage, s as shortcutOf } from './messages-DIMwt3Kn.js';
2
+ import { F as Fields, I as ImageField } from './content-DKVM1CUQ.js';
3
+ import { F as FieldPath } from './parse-content-DF2dL2FG.js';
4
+
5
+ declare function carriesFiles(event: DragEvent): boolean;
6
+
7
+ /** The image field a marker points at, or undefined when it points at anything else. */
8
+ declare function imageFieldAt(schema: Fields, path: string | undefined): {
9
+ readonly path: FieldPath;
10
+ readonly field: ImageField;
11
+ } | undefined;
12
+
13
+ /**
14
+ * The path a site marked for a field: the path itself or, when the site did not mark it, the
15
+ * closest path around it (the record of a price the page shows only as part of the record).
16
+ */
17
+ declare function closestMarkedPath(path: FieldPath, isMarked: (path: string) => boolean): string | undefined;
18
+
19
+ export { carriesFiles, closestMarkedPath, imageFieldAt };
@@ -0,0 +1,19 @@
1
+ import {
2
+ PROTOCOL_VERSION,
3
+ carriesFiles,
4
+ closestMarkedPath,
5
+ imageFieldAt,
6
+ readEditorMessage,
7
+ readPageMessage,
8
+ shortcutOf
9
+ } from "./chunk-HRAL4X6W.js";
10
+ import "./chunk-EC42AIQC.js";
11
+ export {
12
+ PROTOCOL_VERSION,
13
+ carriesFiles,
14
+ closestMarkedPath,
15
+ imageFieldAt,
16
+ readEditorMessage,
17
+ readPageMessage,
18
+ shortcutOf
19
+ };
@@ -0,0 +1,5 @@
1
+ import { APIRoute } from 'astro';
2
+
3
+ declare const GET: APIRoute;
4
+
5
+ export { GET };
@@ -0,0 +1,28 @@
1
+ import {
2
+ isUploadedPhoto
3
+ } from "../chunk-P2OQOZBC.js";
4
+
5
+ // src/routes/media.ts
6
+ import { cmsOrigin } from "virtual:seamlet/config";
7
+ import site from "virtual:seamlet/site";
8
+ var NOT_FOUND = 404;
9
+ var GET = async ({ params }) => {
10
+ const fileName = params["file"];
11
+ const isOurs = params["site"] === site.id && isUploadedPhoto(fileName);
12
+ if (!isOurs) {
13
+ return new Response(null, { status: NOT_FOUND });
14
+ }
15
+ const response = await fetch(
16
+ new URL(`/media/${site.id}/${fileName}`, cmsOrigin)
17
+ );
18
+ return new Response(response.body, {
19
+ status: response.status,
20
+ headers: {
21
+ "Content-Type": response.headers.get("Content-Type") ?? "image/webp",
22
+ "Cache-Control": response.headers.get("Cache-Control") ?? "no-cache"
23
+ }
24
+ });
25
+ };
26
+ export {
27
+ GET
28
+ };
@@ -0,0 +1,68 @@
1
+ import { a as ImageValue, L as LinkValue, b as Field, c as CollectionField, F as Fields } from './content-DKVM1CUQ.js';
2
+ export { d as Content, C as ContentOf, D as DateField, e as FieldKind, f as FieldValue, G as GroupField, I as ImageField, g as LinkField, N as NumberField, T as TextField, V as ValueOf, h as collection, i as date, j as defaultContent, k as defaultValue, l as defineSchema, m as group, n as image, o as link, p as number, t as text } from './content-DKVM1CUQ.js';
3
+ import { F as FieldPath } from './parse-content-DF2dL2FG.js';
4
+ export { C as ContentIssue, P as ParsedContent, c as countCharacters, f as formatFieldPath, p as parseContent } from './parse-content-DF2dL2FG.js';
5
+
6
+ declare const FIELD_PATH_ATTRIBUTE = "data-cms-path";
7
+ interface FieldMarker {
8
+ readonly [FIELD_PATH_ATTRIBUTE]: string;
9
+ }
10
+ /**
11
+ * Every path in content of type C, as a tuple: ["hero"], ["hero", "title"], ["menu", 2],
12
+ * ["menu", 2, "price"]. Scalars, images and links are leaves: an image is one field, not two.
13
+ */
14
+ type ContentPath<C> = C extends readonly (infer Record)[] ? [number] | [number, ...ContentPath<Record>] : C extends string | number | null | ImageValue | LinkValue ? never : {
15
+ [Name in keyof C & string]: [Name] | [Name, ...ContentPath<C[Name]>];
16
+ }[keyof C & string];
17
+ /**
18
+ * The marker of a site's content type. On its page a site spreads it on the element that shows
19
+ * a field or a record of a collection: `<h1 {...edit("hero", "title")}>`,
20
+ * `<li {...edit("menu", index)}>`. A path that is not in the content does not compile.
21
+ */
22
+ declare function editable<C>(): (...path: ContentPath<C>) => FieldMarker;
23
+
24
+ /** What a path written on a page points at: a field, or a whole record of a collection. */
25
+ type FieldTarget = {
26
+ readonly kind: "field";
27
+ readonly path: FieldPath;
28
+ readonly field: Field;
29
+ } | {
30
+ readonly kind: "record";
31
+ readonly path: FieldPath;
32
+ readonly collection: CollectionField;
33
+ /** The place of the record in its collection, the last segment of its path. */
34
+ readonly index: number;
35
+ };
36
+ /**
37
+ * Reads a path from a field marker (`menu.2.price`) by the schema: names inside fields and
38
+ * groups, indexes inside collections. A path the schema does not have gives undefined.
39
+ */
40
+ declare function resolveFieldPath(schema: Fields, text: string): FieldTarget | undefined;
41
+
42
+ /** Where a link leads, as the editor offers it. */
43
+ type LinkTarget = {
44
+ readonly kind: "page";
45
+ readonly path: string;
46
+ } | {
47
+ readonly kind: "web";
48
+ readonly url: string;
49
+ } | {
50
+ readonly kind: "phone";
51
+ readonly number: string;
52
+ } | {
53
+ readonly kind: "email";
54
+ readonly address: string;
55
+ };
56
+ type LinkKind = LinkTarget["kind"];
57
+ /**
58
+ * Where the address leads, or undefined for an address a link of a site may not have. The address
59
+ * is checked exactly as it is stored and rendered: spaces around it make it no address at all.
60
+ */
61
+ declare function linkTargetOf(href: string): LinkTarget | undefined;
62
+ /**
63
+ * The address of a link from what a person typed: a phone or an email without `tel:` or
64
+ * `mailto:`, a site without `https://`.
65
+ */
66
+ declare function hrefOf(target: LinkTarget): string;
67
+
68
+ export { CollectionField, type ContentPath, FIELD_PATH_ATTRIBUTE, Field, type FieldMarker, FieldPath, type FieldTarget, Fields, ImageValue, type LinkKind, type LinkTarget, LinkValue, editable, hrefOf, linkTargetOf, resolveFieldPath };
package/dist/schema.js ADDED
@@ -0,0 +1,40 @@
1
+ import {
2
+ FIELD_PATH_ATTRIBUTE,
3
+ collection,
4
+ countCharacters,
5
+ date,
6
+ defaultContent,
7
+ defaultValue,
8
+ defineSchema,
9
+ editable,
10
+ formatFieldPath,
11
+ group,
12
+ hrefOf,
13
+ image,
14
+ link,
15
+ linkTargetOf,
16
+ number,
17
+ parseContent,
18
+ resolveFieldPath,
19
+ text
20
+ } from "./chunk-EC42AIQC.js";
21
+ export {
22
+ FIELD_PATH_ATTRIBUTE,
23
+ collection,
24
+ countCharacters,
25
+ date,
26
+ defaultContent,
27
+ defaultValue,
28
+ defineSchema,
29
+ editable,
30
+ formatFieldPath,
31
+ group,
32
+ hrefOf,
33
+ image,
34
+ link,
35
+ linkTargetOf,
36
+ number,
37
+ parseContent,
38
+ resolveFieldPath,
39
+ text
40
+ };
package/dist/site.d.ts ADDED
@@ -0,0 +1,25 @@
1
+ import { AstroComponentFactory } from 'astro/runtime/server/index.js';
2
+ import { F as Fields, C as ContentOf } from './content-DKVM1CUQ.js';
3
+
4
+ interface SeamletSiteDefinition<F extends Fields> {
5
+ /** The site's address in the CMS: `/sites/<id>`. */
6
+ readonly id: string;
7
+ readonly schema: F;
8
+ /**
9
+ * The pages of the site by their address (`/`, `/menu`): components that take the site's content
10
+ * as their `content` prop. The CMS lists the same addresses for its editor.
11
+ */
12
+ readonly pages: Readonly<Record<string, AstroComponentFactory>>;
13
+ }
14
+ interface SeamletSite<F extends Fields = Fields> {
15
+ readonly id: string;
16
+ readonly schema: F;
17
+ /** The page at the rest of an address: nothing is the first page, `menu` the page at /menu. */
18
+ readonly pageAt: (rest: string | undefined) => AstroComponentFactory | undefined;
19
+ /** What the CMS published: the site's initial content until the first publication. */
20
+ readonly loadPublishedContent: () => Promise<ContentOf<F>>;
21
+ }
22
+ /** Describes the site for the integration: `src/seamlet.ts` exports it as its default. */
23
+ declare function defineSeamletSite<F extends Fields>(definition: SeamletSiteDefinition<F>): SeamletSite<F>;
24
+
25
+ export { type SeamletSite, type SeamletSiteDefinition, defineSeamletSite };
package/dist/site.js ADDED
@@ -0,0 +1,32 @@
1
+ import {
2
+ parseContent
3
+ } from "./chunk-EC42AIQC.js";
4
+
5
+ // src/site.ts
6
+ import { cmsOrigin } from "virtual:seamlet/config";
7
+ var HOME_PAGE_PATH = "/";
8
+ function defineSeamletSite(definition) {
9
+ const pages = new Map(Object.entries(definition.pages));
10
+ return {
11
+ id: definition.id,
12
+ schema: definition.schema,
13
+ pageAt: (rest) => pages.get(rest === void 0 ? HOME_PAGE_PATH : `/${rest}`),
14
+ loadPublishedContent: async () => {
15
+ const published = await fetchPublished(definition.id);
16
+ return parseContent(definition.schema, published).content;
17
+ }
18
+ };
19
+ }
20
+ async function fetchPublished(siteId) {
21
+ const url = new URL(`/api/sites/${siteId}/published`, cmsOrigin);
22
+ const response = await fetch(url);
23
+ if (!response.ok) {
24
+ throw new Error(
25
+ `Loading the published content from ${url.href} failed with status ${String(response.status)}`
26
+ );
27
+ }
28
+ return response.json();
29
+ }
30
+ export {
31
+ defineSeamletSite
32
+ };
package/package.json CHANGED
@@ -1,6 +1,72 @@
1
1
  {
2
2
  "name": "@seamlet/astro",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.1",
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
+ "license": "MIT",
6
+ "type": "module",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/seamlet/sdk.git",
10
+ "directory": "packages/astro"
11
+ },
12
+ "exports": {
13
+ ".": {
14
+ "types": "./dist/index.d.ts",
15
+ "default": "./dist/index.js"
16
+ },
17
+ "./schema": {
18
+ "types": "./dist/schema.d.ts",
19
+ "default": "./dist/schema.js"
20
+ },
21
+ "./bridge": {
22
+ "types": "./dist/bridge.d.ts",
23
+ "default": "./dist/bridge.js"
24
+ },
25
+ "./protocol": {
26
+ "types": "./dist/protocol.d.ts",
27
+ "default": "./dist/protocol.js"
28
+ },
29
+ "./site": {
30
+ "types": "./dist/site.d.ts",
31
+ "default": "./dist/site.js"
32
+ },
33
+ "./client": {
34
+ "types": "./dist/client.d.ts",
35
+ "default": "./dist/client.js"
36
+ },
37
+ "./preview-requests": {
38
+ "types": "./dist/preview-requests.d.ts",
39
+ "default": "./dist/preview-requests.js"
40
+ },
41
+ "./routes/preview.astro": "./routes/preview.astro",
42
+ "./routes/media": {
43
+ "types": "./dist/routes/media.d.ts",
44
+ "default": "./dist/routes/media.js"
45
+ }
46
+ },
47
+ "files": [
48
+ "dist",
49
+ "routes"
50
+ ],
51
+ "scripts": {
52
+ "build": "tsup"
53
+ },
54
+ "peerDependencies": {
55
+ "astro": ">=7.3.0"
56
+ },
57
+ "engines": {
58
+ "node": ">=24"
59
+ },
60
+ "publishConfig": {
61
+ "access": "public",
62
+ "provenance": true
63
+ },
64
+ "homepage": "https://github.com/seamlet/sdk/tree/main/packages/astro#readme",
65
+ "keywords": [
66
+ "astro",
67
+ "astro-integration",
68
+ "cms",
69
+ "visual-editing",
70
+ "seamlet"
71
+ ]
72
+ }
@@ -0,0 +1,37 @@
1
+ ---
2
+ // The preview the CMS's editor frames: a page of the site with the bridge to the editor.
3
+ // `/preview` is the first page, `/preview/menu` the page at /menu. GET shows the published
4
+ // content; POST shows the content in its JSON body, never saved: the bridge renders the editor's
5
+ // draft this way.
6
+ import { parseContent } from "@seamlet/astro/schema";
7
+ import { readDraft } from "@seamlet/astro/preview-requests";
8
+ import { cmsOrigin } from "virtual:seamlet/config";
9
+ import site from "virtual:seamlet/site";
10
+
11
+ const NOT_FOUND = 404;
12
+
13
+ const Page = site.pageAt(Astro.params.page);
14
+ if (Page === undefined) {
15
+ return new Response(null, { status: NOT_FOUND });
16
+ }
17
+
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
+ const reading =
27
+ Astro.request.method === "POST" ? await readDraft(Astro.request) : undefined;
28
+ if (reading !== undefined && !reading.isRead) {
29
+ return reading.response;
30
+ }
31
+ const content =
32
+ reading === undefined
33
+ ? await site.loadPublishedContent()
34
+ : parseContent(site.schema, reading.draft).content;
35
+ ---
36
+
37
+ <Page content={content} />