@caltho/site-kit 0.2.1 → 0.2.3
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/content/index.js +3 -3
- package/dist/finish.js +8 -2
- package/dist/next/data.js +14 -0
- package/dist/next/preview.js +47 -9
- package/dist/prepare.d.ts +2 -0
- package/dist/prepare.js +2 -2
- package/dist/preview-match.d.ts +2 -0
- package/dist/preview-match.js +2 -0
- package/package.json +1 -1
package/dist/content/index.js
CHANGED
|
@@ -101,11 +101,11 @@ export function defineSite(blocks, o = {}) {
|
|
|
101
101
|
const schema = z
|
|
102
102
|
.object({
|
|
103
103
|
/** The address it's served at once live (its main domain). */
|
|
104
|
-
url: z.url({ protocol: /^https$/, error: "The site's address, starting https://" }),
|
|
105
|
-
lang: z.string().default("en-US"),
|
|
104
|
+
url: z.url({ protocol: /^https$/, error: "The site's address, starting https://" }).meta({ title: "Web address" }),
|
|
105
|
+
lang: z.string().default("en-US").meta({ title: "Language" }),
|
|
106
106
|
business,
|
|
107
107
|
/** Where the contact forms post to, if the site has any (a CalOS form's address, or another service's). */
|
|
108
|
-
form: z.url({ protocol: /^https?$/ }).optional(),
|
|
108
|
+
form: z.url({ protocol: /^https?$/ }).meta({ title: "Where its forms send" }).optional(),
|
|
109
109
|
/** Old addresses kept answering, when CalOS doesn't supply them at build. */
|
|
110
110
|
redirects: z.array(z.object({ from: z.string().startsWith("/"), to: href, status: z.union([z.literal(301), z.literal(302)]).default(301) })).default([]),
|
|
111
111
|
...o.extend,
|
package/dist/finish.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
import { createHash } from "node:crypto";
|
|
6
6
|
import { readdir, readFile, writeFile } from "node:fs/promises";
|
|
7
7
|
import { join, relative } from "node:path";
|
|
8
|
+
import { BUILT_IMAGES } from "./preview-match.js";
|
|
8
9
|
import { headersFile, loadRedirects, PREVIEW_PATH } from "./seo.js";
|
|
9
10
|
export async function finish(root, outDir, env = process.env, log = console.log) {
|
|
10
11
|
const prepared = JSON.parse(await readFile(join(root, ".site", "prepared.json"), "utf8"));
|
|
@@ -42,9 +43,14 @@ export async function finish(root, outDir, env = process.env, log = console.log)
|
|
|
42
43
|
};
|
|
43
44
|
for (const [directive, sources] of Object.entries(prepared.csp ?? {}))
|
|
44
45
|
directives[directive] = [...new Set([...(directives[directive] ?? ["'self'"]), ...sources])];
|
|
45
|
-
// The preview shows the draft's pictures from CalOS, before any build has made them
|
|
46
|
-
|
|
46
|
+
// The preview shows the draft's pictures from CalOS, before any build has made them; those this
|
|
47
|
+
// build made, it shows as built (./next/preview.tsx), from the list it carries.
|
|
48
|
+
const isPreview = relative(outDir, file).replace(/(\/index)?\.html$/, "") === PREVIEW_PATH.slice(1);
|
|
49
|
+
if (isPreview) {
|
|
47
50
|
directives["img-src"].push(...editors);
|
|
51
|
+
const built = JSON.stringify(prepared.images ?? {}).replace(/</g, "\\u003c");
|
|
52
|
+
html = html.replace(/<\/head>/i, () => `<script type="application/json" id="${BUILT_IMAGES}">${built}</script></head>`);
|
|
53
|
+
}
|
|
48
54
|
const policy = Object.entries(directives).map(([d, v]) => `${d} ${v.join(" ")}`).join("; ");
|
|
49
55
|
html = html.replace(/<meta http-equiv="content-security-policy"[^>]*>/i, "");
|
|
50
56
|
html = html.replace(/<head([^>]*)>/i, `<head$1><meta http-equiv="content-security-policy" content="${policy}">`);
|
package/dist/next/data.js
CHANGED
|
@@ -8,10 +8,20 @@ let loader = null;
|
|
|
8
8
|
export function loadPreparedWith(load) {
|
|
9
9
|
loader = load;
|
|
10
10
|
}
|
|
11
|
+
/** Whether the store holds a draft from CalOS's preview, rather than a build's content. */
|
|
12
|
+
let drafting = false;
|
|
11
13
|
/** Puts content in the store: CalOS's preview, with the draft it's editing. */
|
|
12
14
|
export function setPrepared(p) {
|
|
13
15
|
current = p;
|
|
16
|
+
drafting = true;
|
|
14
17
|
}
|
|
18
|
+
/** A picture not chosen yet, in a draft: a grey box where it will go. */
|
|
19
|
+
const NOT_CHOSEN = {
|
|
20
|
+
width: 1600,
|
|
21
|
+
height: 1000,
|
|
22
|
+
src: `data:image/svg+xml,${encodeURIComponent('<svg xmlns="http://www.w3.org/2000/svg" width="1600" height="1000"><rect width="100%" height="100%" fill="#e5e5e5"/></svg>')}`,
|
|
23
|
+
srcset: [],
|
|
24
|
+
};
|
|
15
25
|
export function prepared() {
|
|
16
26
|
if (loader && (!current || process.env.NODE_ENV === "development"))
|
|
17
27
|
current = loader();
|
|
@@ -22,6 +32,10 @@ export function prepared() {
|
|
|
22
32
|
export const site = () => prepared().site;
|
|
23
33
|
export function image(src) {
|
|
24
34
|
const found = prepared().images[src];
|
|
35
|
+
// A section being filled in, in the preview, is drawn with its picture still to choose; a build
|
|
36
|
+
// with a picture it didn't prepare stops.
|
|
37
|
+
if (!found && drafting)
|
|
38
|
+
return NOT_CHOSEN;
|
|
25
39
|
if (!found)
|
|
26
40
|
throw new Error(`The image "${src}" wasn't prepared: run \`site-kit prepare\` again`);
|
|
27
41
|
return found;
|
package/dist/next/preview.js
CHANGED
|
@@ -10,7 +10,7 @@ import { jsx as _jsx } from "react/jsx-runtime";
|
|
|
10
10
|
// page → CalOS: { type: "calos:ready" }, { type: "calos:pick", path, block }, { type: "calos:go", path },
|
|
11
11
|
// { type: "calos:error", message }
|
|
12
12
|
import { Component, useEffect, useRef, useState } from "react";
|
|
13
|
-
import { fieldForImage, fieldForText } from "../preview-match.js";
|
|
13
|
+
import { BUILT_IMAGES, fieldForImage, fieldForText } from "../preview-match.js";
|
|
14
14
|
import { setPrepared } from "./data.js";
|
|
15
15
|
export function SitePreview({ origins, render }) {
|
|
16
16
|
const [shown, setShown] = useState(null);
|
|
@@ -31,9 +31,10 @@ export function SitePreview({ origins, render }) {
|
|
|
31
31
|
const onMessage = (e) => {
|
|
32
32
|
if (!origins.includes(e.origin) || e.source !== window.parent)
|
|
33
33
|
return;
|
|
34
|
-
const
|
|
35
|
-
if (
|
|
34
|
+
const sent = e.data;
|
|
35
|
+
if (sent?.type !== "calos:show")
|
|
36
36
|
return;
|
|
37
|
+
const m = { ...sent, images: asBuilt(sent.images) };
|
|
37
38
|
replyTo.current = e.origin;
|
|
38
39
|
setPrepared({ site: m.site, images: m.images, csp: {} });
|
|
39
40
|
latest.current = m;
|
|
@@ -114,13 +115,22 @@ export function SitePreview({ origins, render }) {
|
|
|
114
115
|
const el = blockElement(shown);
|
|
115
116
|
const label = (m) => (m?.selected && typeof m.selected[3] === "number" ? m.labels?.[m.selected[3]] : undefined);
|
|
116
117
|
outline("selected", el, label(shown));
|
|
117
|
-
//
|
|
118
|
-
|
|
119
|
-
|
|
118
|
+
// What was under the pointer has moved with the drawing: outlined again on the next move.
|
|
119
|
+
hovered = null;
|
|
120
|
+
outline("section", null);
|
|
121
|
+
outline("hover", null);
|
|
122
|
+
// Another page starts at its top, unless a section on it is to be brought into view.
|
|
123
|
+
if (shown.path !== drawnPath && !(el && shown.scroll))
|
|
124
|
+
window.scrollTo({ top: 0 });
|
|
125
|
+
drawnPath = shown.path;
|
|
120
126
|
// Scrolled here only: scrollIntoView would scroll CalOS's page around the frame too.
|
|
121
127
|
if (el && shown.scroll)
|
|
122
128
|
window.scrollTo({ top: el.getBoundingClientRect().top + window.scrollY - window.innerHeight / 4, behavior: "smooth" });
|
|
123
|
-
const keep = () =>
|
|
129
|
+
const keep = () => {
|
|
130
|
+
outline("selected", blockElement(latest.current), label(latest.current));
|
|
131
|
+
outline("section", null);
|
|
132
|
+
outline("hover", null);
|
|
133
|
+
};
|
|
124
134
|
window.addEventListener("scroll", keep, { passive: true });
|
|
125
135
|
window.addEventListener("resize", keep);
|
|
126
136
|
return () => {
|
|
@@ -135,6 +145,30 @@ export function SitePreview({ origins, render }) {
|
|
|
135
145
|
return null;
|
|
136
146
|
return (_jsx(Fallback, { onError: (message) => post({ type: "calos:error", message }), children: render(page, shown.site) }, round));
|
|
137
147
|
}
|
|
148
|
+
/** The pictures this build made (finish writes the list into this page), by path. */
|
|
149
|
+
let built = null;
|
|
150
|
+
/**
|
|
151
|
+
* CalOS's pictures, each one the build made unchanged (the same file: its built names carry the
|
|
152
|
+
* start of its SHA-256, CalOS's address all of it) swapped for the built copies, sized as the site
|
|
153
|
+
* serves them; a picture new since the build comes from CalOS.
|
|
154
|
+
*/
|
|
155
|
+
function asBuilt(images) {
|
|
156
|
+
if (!built) {
|
|
157
|
+
try {
|
|
158
|
+
built = JSON.parse(document.getElementById(BUILT_IMAGES)?.textContent || "{}");
|
|
159
|
+
}
|
|
160
|
+
catch {
|
|
161
|
+
built = {};
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
const out = {};
|
|
165
|
+
for (const [path, img] of Object.entries(images)) {
|
|
166
|
+
const sha = /\/assets\/([0-9a-f]{64})$/.exec(img.src)?.[1];
|
|
167
|
+
const made = built[path];
|
|
168
|
+
out[path] = made?.hash && sha?.startsWith(made.hash) ? made : img;
|
|
169
|
+
}
|
|
170
|
+
return out;
|
|
171
|
+
}
|
|
138
172
|
/** A path on this site from a link's address, as the content writes it; null for anywhere else. */
|
|
139
173
|
function internalPath(href, site) {
|
|
140
174
|
if (!href || href.startsWith("#") || /^(mailto|tel):/.test(href))
|
|
@@ -147,13 +181,15 @@ function internalPath(href, site) {
|
|
|
147
181
|
return site.pages.some((p) => p.path === bare) ? bare : site.pages.some((p) => p.path === withSlash) ? withSlash : null;
|
|
148
182
|
}
|
|
149
183
|
const pageIndex = (m) => m.site.pages.findIndex((p) => p.path === m.path);
|
|
150
|
-
/**
|
|
184
|
+
/** What's in `main` without being drawn: a page's structured data (JSON-LD) and the like. */
|
|
185
|
+
const UNDRAWN = new Set(["SCRIPT", "STYLE", "TEMPLATE", "NOSCRIPT", "LINK", "META"]);
|
|
186
|
+
/** The page's blocks as drawn: `main`'s children (those drawn), when there's one for each block. */
|
|
151
187
|
function blockElements(m) {
|
|
152
188
|
const main = document.querySelector("main");
|
|
153
189
|
const page = m.site.pages[pageIndex(m)];
|
|
154
190
|
if (!main || !page)
|
|
155
191
|
return null;
|
|
156
|
-
const kids = [...main.children];
|
|
192
|
+
const kids = [...main.children].filter((e) => !UNDRAWN.has(e.tagName));
|
|
157
193
|
return kids.length === page.blocks.length ? kids : null;
|
|
158
194
|
}
|
|
159
195
|
function blockElement(m) {
|
|
@@ -233,6 +269,8 @@ const looks = {
|
|
|
233
269
|
hover: { outline: `1px dashed ${BLUE}`, outlineOffset: "3px", borderRadius: "3px" },
|
|
234
270
|
};
|
|
235
271
|
let hovered = null;
|
|
272
|
+
/** The page last drawn, so another one starts at its top. */
|
|
273
|
+
let drawnPath = null;
|
|
236
274
|
const boxes = {};
|
|
237
275
|
function outline(which, el, label) {
|
|
238
276
|
let made = boxes[which];
|
package/dist/prepare.d.ts
CHANGED
|
@@ -11,6 +11,8 @@ export type PreparedImage = {
|
|
|
11
11
|
}[];
|
|
12
12
|
/** A 1200×630 JPEG, where a page shares it. */
|
|
13
13
|
share?: string;
|
|
14
|
+
/** The start of the file's SHA-256, as in its built names: CalOS's preview knows a picture unchanged since the build by it. */
|
|
15
|
+
hash?: string;
|
|
14
16
|
};
|
|
15
17
|
export type Prepared = {
|
|
16
18
|
site: Site;
|
package/dist/prepare.js
CHANGED
|
@@ -113,7 +113,7 @@ async function pictures(site, imagesDir, pub, log) {
|
|
|
113
113
|
if (!existsSync(join(dir, name)))
|
|
114
114
|
await writeFile(join(dir, name), bytes);
|
|
115
115
|
const meta = await sharp(bytes).metadata();
|
|
116
|
-
result[src] = { width: meta.width ?? 0, height: meta.height ?? 0, src: `/_img/${name}`, srcset: [] };
|
|
116
|
+
result[src] = { width: meta.width ?? 0, height: meta.height ?? 0, src: `/_img/${name}`, srcset: [], hash };
|
|
117
117
|
continue;
|
|
118
118
|
}
|
|
119
119
|
const img = sharp(bytes).rotate();
|
|
@@ -135,7 +135,7 @@ async function pictures(site, imagesDir, pub, log) {
|
|
|
135
135
|
}
|
|
136
136
|
srcset.push({ w, url: `/_img/${name}` });
|
|
137
137
|
}
|
|
138
|
-
const entry = { width, height, src: srcset.at(-1).url, srcset };
|
|
138
|
+
const entry = { width, height, src: srcset.at(-1).url, srcset, hash };
|
|
139
139
|
if (shared.has(src)) {
|
|
140
140
|
const name = `${base}.${hash}.share.jpg`;
|
|
141
141
|
if (!existsSync(join(dir, name))) {
|
package/dist/preview-match.d.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
/** The preview page's list of the pictures its build made (by path), as a JSON script with this id (../finish.ts). */
|
|
2
|
+
export declare const BUILT_IMAGES = "calos-built-images";
|
|
1
3
|
export type Path = (string | number)[];
|
|
2
4
|
/** Words as they read on the page: Markdown's marks gone, a link's address gone, spacing and case evened. */
|
|
3
5
|
export declare function readable(text: string): string;
|
package/dist/preview-match.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
// Which field of the content a click in CalOS's preview landed on, with no markers in the site's
|
|
2
2
|
// components: the words clicked are matched against the content's text, a picture by its address.
|
|
3
3
|
// Pure, so it's tested here and runs in the preview in the browser.
|
|
4
|
+
/** The preview page's list of the pictures its build made (by path), as a JSON script with this id (../finish.ts). */
|
|
5
|
+
export const BUILT_IMAGES = "calos-built-images";
|
|
4
6
|
/** Keys that hold no words a visitor reads: never what a click on words means. */
|
|
5
7
|
const SKIP = new Set(["type", "id", "href", "path", "style", "menuStyle", "footer", "ratio", "tone"]);
|
|
6
8
|
/** Words as they read on the page: Markdown's marks gone, a link's address gone, spacing and case evened. */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@caltho/site-kit",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.3",
|
|
4
4
|
"description": "What every Caltho site is built with: the contract with CalOS (content, and how a site declares what is editable), images, search and security, and the checks",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|