@caltho/site-kit 0.2.1 → 0.2.2
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 +41 -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,18 @@ 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);
|
|
120
122
|
// Scrolled here only: scrollIntoView would scroll CalOS's page around the frame too.
|
|
121
123
|
if (el && shown.scroll)
|
|
122
124
|
window.scrollTo({ top: el.getBoundingClientRect().top + window.scrollY - window.innerHeight / 4, behavior: "smooth" });
|
|
123
|
-
const keep = () =>
|
|
125
|
+
const keep = () => {
|
|
126
|
+
outline("selected", blockElement(latest.current), label(latest.current));
|
|
127
|
+
outline("section", null);
|
|
128
|
+
outline("hover", null);
|
|
129
|
+
};
|
|
124
130
|
window.addEventListener("scroll", keep, { passive: true });
|
|
125
131
|
window.addEventListener("resize", keep);
|
|
126
132
|
return () => {
|
|
@@ -135,6 +141,30 @@ export function SitePreview({ origins, render }) {
|
|
|
135
141
|
return null;
|
|
136
142
|
return (_jsx(Fallback, { onError: (message) => post({ type: "calos:error", message }), children: render(page, shown.site) }, round));
|
|
137
143
|
}
|
|
144
|
+
/** The pictures this build made (finish writes the list into this page), by path. */
|
|
145
|
+
let built = null;
|
|
146
|
+
/**
|
|
147
|
+
* CalOS's pictures, each one the build made unchanged (the same file: its built names carry the
|
|
148
|
+
* start of its SHA-256, CalOS's address all of it) swapped for the built copies, sized as the site
|
|
149
|
+
* serves them; a picture new since the build comes from CalOS.
|
|
150
|
+
*/
|
|
151
|
+
function asBuilt(images) {
|
|
152
|
+
if (!built) {
|
|
153
|
+
try {
|
|
154
|
+
built = JSON.parse(document.getElementById(BUILT_IMAGES)?.textContent || "{}");
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
built = {};
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
const out = {};
|
|
161
|
+
for (const [path, img] of Object.entries(images)) {
|
|
162
|
+
const sha = /\/assets\/([0-9a-f]{64})$/.exec(img.src)?.[1];
|
|
163
|
+
const made = built[path];
|
|
164
|
+
out[path] = made?.hash && sha?.startsWith(made.hash) ? made : img;
|
|
165
|
+
}
|
|
166
|
+
return out;
|
|
167
|
+
}
|
|
138
168
|
/** A path on this site from a link's address, as the content writes it; null for anywhere else. */
|
|
139
169
|
function internalPath(href, site) {
|
|
140
170
|
if (!href || href.startsWith("#") || /^(mailto|tel):/.test(href))
|
|
@@ -147,13 +177,15 @@ function internalPath(href, site) {
|
|
|
147
177
|
return site.pages.some((p) => p.path === bare) ? bare : site.pages.some((p) => p.path === withSlash) ? withSlash : null;
|
|
148
178
|
}
|
|
149
179
|
const pageIndex = (m) => m.site.pages.findIndex((p) => p.path === m.path);
|
|
150
|
-
/**
|
|
180
|
+
/** What's in `main` without being drawn: a page's structured data (JSON-LD) and the like. */
|
|
181
|
+
const UNDRAWN = new Set(["SCRIPT", "STYLE", "TEMPLATE", "NOSCRIPT", "LINK", "META"]);
|
|
182
|
+
/** The page's blocks as drawn: `main`'s children (those drawn), when there's one for each block. */
|
|
151
183
|
function blockElements(m) {
|
|
152
184
|
const main = document.querySelector("main");
|
|
153
185
|
const page = m.site.pages[pageIndex(m)];
|
|
154
186
|
if (!main || !page)
|
|
155
187
|
return null;
|
|
156
|
-
const kids = [...main.children];
|
|
188
|
+
const kids = [...main.children].filter((e) => !UNDRAWN.has(e.tagName));
|
|
157
189
|
return kids.length === page.blocks.length ? kids : null;
|
|
158
190
|
}
|
|
159
191
|
function blockElement(m) {
|
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.2",
|
|
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",
|