@pirateandfox/pagestack 0.1.0-beta.5 → 0.1.0-beta.6
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 +2 -1
- package/index.mjs +6 -4
- package/package.json +1 -1
- package/src/client/collection.ts +1 -1
- package/src/routes/Nav.astro +1 -1
- package/src/routes/api/collection.ts +5 -1
- package/src/routes/api/pages.ts +2 -1
- package/src/routes/edit-collection.astro +1 -1
- package/src/routes/edit-index.astro +5 -1
- package/src/routes/preview.astro +1 -0
- package/src/runtime.ts +2 -0
- package/src/server/context.ts +1 -1
- package/src/server/middleware.ts +1 -0
package/README.md
CHANGED
|
@@ -293,7 +293,8 @@ editor response is `noindex` and `no-store`.
|
|
|
293
293
|
|
|
294
294
|
| option | default | |
|
|
295
295
|
|---|---|---|
|
|
296
|
-
| `renderer` | required | Astro component rendering `{ page, editing }` |
|
|
296
|
+
| `renderer` | required, unless `pages: false` | Astro component rendering `{ page, editing }` |
|
|
297
|
+
| `pages` | `true` | `false` for a site with no block pages: only collections, globals and media |
|
|
297
298
|
| `sections` | required | module exporting `{ schemas, presets }` |
|
|
298
299
|
| `pagesDir` | `src/content/pages` | |
|
|
299
300
|
| `imagesDir` / `uploadsDir` / `publicDir` | `public/images` / `public/images/uploads` / `public` | |
|
package/index.mjs
CHANGED
|
@@ -18,7 +18,8 @@ const route = (file) => new URL(`./src/routes/${file}`, import.meta.url);
|
|
|
18
18
|
|
|
19
19
|
/**
|
|
20
20
|
* @typedef {object} SectionEditorOptions
|
|
21
|
-
* @property {string} renderer
|
|
21
|
+
* @property {string} [renderer] Astro component rendering a page: props { page, editing }. Required unless `pages: false`.
|
|
22
|
+
* @property {boolean} [pages] false for a site with no block pages: only collections, globals and media.
|
|
22
23
|
* @property {string} sections Module exporting { schemas, presets } (see runtime.ts types).
|
|
23
24
|
* @property {string} [pagesDir='src/content/pages'] Directory of <slug>.json pages.
|
|
24
25
|
* @property {string} [imagesDir='public/images'] Browsable image library root.
|
|
@@ -58,7 +59,8 @@ function staticRoutes(srcDir) {
|
|
|
58
59
|
|
|
59
60
|
/** @param {SectionEditorOptions} userOptions */
|
|
60
61
|
export default function sectionEditor(userOptions) {
|
|
61
|
-
if (!userOptions?.
|
|
62
|
+
if (!userOptions?.sections) throw new Error('pagestack: `sections` is required');
|
|
63
|
+
if (!userOptions.renderer && userOptions.pages !== false) throw new Error('pagestack: `renderer` is required unless `pages: false`');
|
|
62
64
|
// When the site has no adapter of its own we bring @astrojs/node along and forward every
|
|
63
65
|
// integration hook to it (an integration added via updateConfig() doesn't get to call
|
|
64
66
|
// setAdapter() in time, so we proxy its hooks instead).
|
|
@@ -99,7 +101,7 @@ export default function sectionEditor(userOptions) {
|
|
|
99
101
|
const root = fileURLToPath(config.root);
|
|
100
102
|
const abs = (p) => path.isAbsolute(p) ? p : path.resolve(root, p);
|
|
101
103
|
const reservedSlugs = [...new Set([...(options.reservedSlugs ?? []), ...staticRoutes(fileURLToPath(config.srcDir))])];
|
|
102
|
-
const serial = { ...options, reservedSlugs, root, renderer: abs(options.renderer), sections: abs(options.sections), command };
|
|
104
|
+
const serial = { ...options, reservedSlugs, root, renderer: options.renderer ? abs(options.renderer) : null, sections: abs(options.sections), command };
|
|
103
105
|
|
|
104
106
|
updateConfig({
|
|
105
107
|
vite: {
|
|
@@ -109,7 +111,7 @@ export default function sectionEditor(userOptions) {
|
|
|
109
111
|
load(id) {
|
|
110
112
|
// The renderer (and the site layout/CSS behind it) is only imported by the preview
|
|
111
113
|
// route, so the editor shell doesn't drag the whole site's CSS into its page.
|
|
112
|
-
if (id === '\0' + VIRTUAL_RENDERER) return `export { default as Renderer } from ${JSON.stringify(serial.renderer)}
|
|
114
|
+
if (id === '\0' + VIRTUAL_RENDERER) return serial.renderer ? `export { default as Renderer } from ${JSON.stringify(serial.renderer)};` : 'export const Renderer = null;';
|
|
113
115
|
if (id !== '\0' + VIRTUAL) return;
|
|
114
116
|
return [
|
|
115
117
|
`export * as site from ${JSON.stringify(serial.sections)};`,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pirateandfox/pagestack",
|
|
3
|
-
"version": "0.1.0-beta.
|
|
3
|
+
"version": "0.1.0-beta.6",
|
|
4
4
|
"description": "PageStack: visual section editor for Astro sites. Git-only, static public site, passkey login, saves are commits.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
package/src/client/collection.ts
CHANGED
|
@@ -5,7 +5,7 @@ import { confirmDialog, promptDialog, toast } from './ui';
|
|
|
5
5
|
const boot = (window as any).__ASE_COLLECTION__ as { id: string; singular: string; slugs: string[]; reserved: string[] };
|
|
6
6
|
const api = `/api/editor/collection/${boot.id}`;
|
|
7
7
|
|
|
8
|
-
document.getElementById('ase-add-item')
|
|
8
|
+
document.getElementById('ase-add-item')?.addEventListener('click', async () => {
|
|
9
9
|
const title = await promptDialog({ title: `Add ${boot.singular}`, label: 'Title', submit: `Add ${boot.singular}` });
|
|
10
10
|
if (!title) return;
|
|
11
11
|
const slug = uniqueSlug(slugify(title) || boot.singular, [...boot.slugs, ...boot.reserved]);
|
package/src/routes/Nav.astro
CHANGED
|
@@ -5,7 +5,7 @@ const { current } = Astro.props as { current: string };
|
|
|
5
5
|
const user = editorOf(Astro.locals);
|
|
6
6
|
const firstGlobal = Object.keys(globals)[0];
|
|
7
7
|
const links = [
|
|
8
|
-
{ id: 'pages', label: 'Pages', href: '/edit' },
|
|
8
|
+
...(opts.pages === false ? [] : [{ id: 'pages', label: 'Pages', href: '/edit' }]),
|
|
9
9
|
...Object.entries(collections).map(([id, c]) => ({ id: `collection-${id}`, label: c.label, href: `/edit/collection/${id}` })),
|
|
10
10
|
{ id: 'media', label: 'Media', href: '/edit/media' },
|
|
11
11
|
...(firstGlobal ? [{ id: 'settings', label: Object.keys(globals).length > 1 ? 'Site settings' : globals[firstGlobal].label, href: `/edit/global/${firstGlobal}` }] : []),
|
|
@@ -39,6 +39,7 @@ export const GET: APIRoute = async ({ params }) => {
|
|
|
39
39
|
export const POST: APIRoute = async ({ params, request, locals }) => {
|
|
40
40
|
const c = find(params.id);
|
|
41
41
|
if (!c || params.slug) return bad('no such collection', 404);
|
|
42
|
+
if (c.create === false) return bad('Items here cannot be added from the editor.', 405);
|
|
42
43
|
const body = await request.json().catch(() => null);
|
|
43
44
|
const title = plainText(String(body?.title ?? '')).trim();
|
|
44
45
|
const slug = String(body?.slug ?? '').trim();
|
|
@@ -47,7 +48,10 @@ export const POST: APIRoute = async ({ params, request, locals }) => {
|
|
|
47
48
|
const problem = SLUG.test(slug) ? slugProblem(slug, { existing, reserved: reservedSlugs() }) : 'Use letters, numbers and dashes in the URL.';
|
|
48
49
|
if (problem) return bad(problem, 409);
|
|
49
50
|
const data: Record<string, unknown> = { title };
|
|
50
|
-
for (const [k, f] of Object.entries(c.fields)) if (f.
|
|
51
|
+
for (const [k, f] of Object.entries(c.fields)) if (f.default !== undefined && k !== 'title') data[k] = f.default;
|
|
52
|
+
// Only the first date is "today": others (modified, reviewed on) mean something else happened.
|
|
53
|
+
const dated = Object.entries(c.fields).find(([, f]) => f.type === 'date');
|
|
54
|
+
if (dated && data[dated[0]] === undefined) data[dated[0]] = new Date().toISOString().slice(0, dated[1].time === false ? 10 : 19);
|
|
51
55
|
if (c.fields.draft?.type === 'boolean') data.draft = true;
|
|
52
56
|
try {
|
|
53
57
|
const result = await storage().commit({ message: commitMessage(`Add ${name(c)} ${slug}`, locals), files: [{ path: file(c, slug), content: writeMarkdown(null, data, '\n') }] });
|
package/src/routes/api/pages.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// GET /api/editor/pages -> { pages }
|
|
2
2
|
// POST /api/editor/pages { title, slug, copyOf?, template? } -> creates a draft page.
|
|
3
3
|
import type { APIRoute } from 'astro';
|
|
4
|
-
import { bad, clean, commitMessage, json, listPages, pagePath, readPage, reservedSlugs, serializePage, storage, templates, validate } from '../../server/context';
|
|
4
|
+
import { opts, bad, clean, commitMessage, json, listPages, pagePath, readPage, reservedSlugs, serializePage, storage, templates, validate } from '../../server/context';
|
|
5
5
|
import { copyPage, newPage, slugProblem } from '../../core/pages';
|
|
6
6
|
import type { PageDoc } from '../../runtime';
|
|
7
7
|
export const prerender = false;
|
|
@@ -9,6 +9,7 @@ export const prerender = false;
|
|
|
9
9
|
export const GET: APIRoute = async () => json({ pages: await listPages() });
|
|
10
10
|
|
|
11
11
|
export const POST: APIRoute = async ({ request, locals }) => {
|
|
12
|
+
if (opts.pages === false) return bad('This site has no block pages.', 404);
|
|
12
13
|
let body: any;
|
|
13
14
|
try { body = await request.json(); } catch { return bad('invalid JSON'); }
|
|
14
15
|
const title = String(body?.title ?? '').trim();
|
|
@@ -24,7 +24,7 @@ const boot = { id, singular, slugs: items.map((i) => i.slug), reserved: reserved
|
|
|
24
24
|
<main class="ase-index__main">
|
|
25
25
|
<header class="ase-index__head">
|
|
26
26
|
<div><h1>{c.label} <span class="ase-muted">{items.length}</span></h1></div>
|
|
27
|
-
<button type="button" id="ase-add-item" class="ase-btn ase-btn--primary">Add {singular}</button>
|
|
27
|
+
{c.create !== false && <button type="button" id="ase-add-item" class="ase-btn ase-btn--primary">Add {singular}</button>}
|
|
28
28
|
</header>
|
|
29
29
|
<div class="ase-index__tools"><input type="search" id="ase-page-search" placeholder={`Search ${c.label.toLowerCase()}`} aria-label="Search" /></div>
|
|
30
30
|
<ul class="ase-pagelist" id="ase-pagelist">
|
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
---
|
|
2
2
|
// /edit — the Pages screen: every page, with add, duplicate, delete and search.
|
|
3
|
-
import { globals, listPages, opts, publicPath, reservedSlugs, templates } from '../server/context';
|
|
3
|
+
import { collections, globals, listPages, opts, publicPath, reservedSlugs, templates } from '../server/context';
|
|
4
4
|
import Nav from './Nav.astro';
|
|
5
5
|
import '../client/shell.css';
|
|
6
6
|
export const prerender = false;
|
|
7
|
+
if (opts.pages === false) {
|
|
8
|
+
const first = Object.keys(collections)[0];
|
|
9
|
+
return Astro.redirect(first ? `/edit/collection/${first}` : '/edit/media');
|
|
10
|
+
}
|
|
7
11
|
const pages = await listPages();
|
|
8
12
|
const boot = {
|
|
9
13
|
pages: pages.map((p) => ({ slug: p.slug, title: p.title })),
|
package/src/routes/preview.astro
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
// Preview button) renders the draft without edit mode, for a look at the finished page in a tab.
|
|
6
6
|
import { Renderer } from 'virtual:pagestack/renderer';
|
|
7
7
|
import { clean, readPage, validate } from '../server/context';
|
|
8
|
+
if (!Renderer) return new Response('Not found', { status: 404 });
|
|
8
9
|
const locals = Astro.locals;
|
|
9
10
|
export const prerender = false;
|
|
10
11
|
const slug = Astro.params.slug || 'index';
|
package/src/runtime.ts
CHANGED
|
@@ -101,6 +101,8 @@ export interface CollectionDoc {
|
|
|
101
101
|
fields: Record<string, Field>;
|
|
102
102
|
/** Site URL of an item, for "View" links. */
|
|
103
103
|
url?: (slug: string) => string;
|
|
104
|
+
/** false hides Add, for a fixed set of items. Otherwise new items start from each field's `default`. */
|
|
105
|
+
create?: boolean;
|
|
104
106
|
}
|
|
105
107
|
|
|
106
108
|
/** A starting point offered by "Add page". */
|
package/src/server/context.ts
CHANGED
|
@@ -17,7 +17,7 @@ import type { SavedPattern } from '../core/insert';
|
|
|
17
17
|
export { options, site };
|
|
18
18
|
|
|
19
19
|
export interface EditorOptions {
|
|
20
|
-
root: string; renderer: string; sections: string; pagesDir: string; imagesDir: string; uploadsDir: string; publicDir: string; usersFile: string;
|
|
20
|
+
root: string; renderer: string | null; pages?: boolean; sections: string; pagesDir: string; imagesDir: string; uploadsDir: string; publicDir: string; usersFile: string;
|
|
21
21
|
auth: 'none' | 'passkey' | 'none-behind-proxy'; storage: 'local' | 'github'; github: { repo: string; branch?: string; pullRequest?: boolean } | null;
|
|
22
22
|
siteName: string; secretEnv: string; command: string; interactiveSelector: string;
|
|
23
23
|
reservedSlugs: string[]; redirectsFile: string; patternsFile: string;
|
package/src/server/middleware.ts
CHANGED
|
@@ -47,6 +47,7 @@ function harden(res: Response, url: URL, sandbox: boolean): Response {
|
|
|
47
47
|
let warned = false;
|
|
48
48
|
|
|
49
49
|
export const onRequest = defineMiddleware(async (ctx, next) => {
|
|
50
|
+
if (ctx.isPrerendered) return next(); // public pages, which never carry a session
|
|
50
51
|
const { pathname } = ctx.url;
|
|
51
52
|
const { request } = ctx;
|
|
52
53
|
const isEditor = pathname === '/edit' || pathname.startsWith('/edit/') || pathname.startsWith('/api/editor/');
|