@louise-toolkit/astro 0.2.7 → 0.4.0
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 -2
- package/dist/actions.d.ts +15 -2
- package/dist/actions.js +18 -3
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/middleware.d.ts +21 -2
- package/dist/middleware.js +15 -0
- package/dist/seo.d.ts +16 -0
- package/dist/seo.js +16 -0
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -44,8 +44,8 @@ Pass `apiGate: true` to deny the editor API by default: every request under
|
|
|
44
44
|
writes and WebSocket upgrades origin-checked. This is the same gate as
|
|
45
45
|
`composeWorker({ gate })` (ADR 0012), for routes mounted as Astro API routes.
|
|
46
46
|
Middleware runs before Astro knows which route file answers, so a public route
|
|
47
|
-
is declared by path: the toolkit's form and
|
|
48
|
-
default paths, and `apiGate: { isPublic: (path) => … }` adds your own.
|
|
47
|
+
is declared by path: the toolkit's form, vitals, and status routes are exempt
|
|
48
|
+
at their default paths, and `apiGate: { isPublic: (path) => … }` adds your own.
|
|
49
49
|
|
|
50
50
|
**Actions**: the editor write path as Astro Actions, so a save is a typed call
|
|
51
51
|
rather than a hand-rolled endpoint. `louiseSaveAction`, `louiseSaveDraftAction`,
|
package/dist/actions.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { z } from "astro/zod";
|
|
|
2
2
|
import { type SaveCollectionConfig } from "louise-toolkit/editor";
|
|
3
3
|
import { type SettingsPatchConfig } from "louise-toolkit/editor";
|
|
4
4
|
import type { EditorRouteEnv } from "louise-toolkit/editor";
|
|
5
|
-
import { type SaveDraftDeps } from "louise-toolkit/editor";
|
|
5
|
+
import { type DraftSoftLocks, type SaveDraftDeps } from "louise-toolkit/editor";
|
|
6
6
|
/** The subset of Astro's `ActionError` codes the editor handlers emit. */
|
|
7
7
|
type ActionErrorCode = "BAD_REQUEST" | "UNAUTHORIZED" | "FORBIDDEN" | "NOT_FOUND" | "INTERNAL_SERVER_ERROR";
|
|
8
8
|
/** The shape of Astro's `ActionError` constructor the handlers depend on—injected
|
|
@@ -77,8 +77,13 @@ export interface LouiseSettingsActionConfig<Env extends EditorRouteEnv = EditorR
|
|
|
77
77
|
export interface SaveDraftActionInput {
|
|
78
78
|
id: number;
|
|
79
79
|
data: Record<string, unknown>;
|
|
80
|
+
/** The field revisions the save started from (`SaveDraftOptions.base`). */
|
|
81
|
+
base?: Record<string, string>;
|
|
80
82
|
}
|
|
81
83
|
export interface LouiseSaveDraftActionConfig<Env extends EditorRouteEnv = EditorRouteEnv> extends EditorActionDeps<Env>, SaveDraftDeps<Env> {
|
|
84
|
+
/** The soft-locks a save respects (#572): `realtimeSoftLocks` from
|
|
85
|
+
* `louise-toolkit/realtime`, as `versionsRoute` takes it. */
|
|
86
|
+
softLocks?: DraftSoftLocks<Env>;
|
|
82
87
|
}
|
|
83
88
|
/**
|
|
84
89
|
* Build the `{ input, handler }` config for the editor `save` Action (the inline
|
|
@@ -121,12 +126,20 @@ export declare function louiseSettingsAction<Env extends EditorRouteEnv = Editor
|
|
|
121
126
|
* the id). The handler shares the raw `versionsRoute` store path via
|
|
122
127
|
* {@link applySaveDraft}—the concurrent-surface merge base and the #70 KV
|
|
123
128
|
* write-buffer—and returns that path's JSON body (a created `version`, or
|
|
124
|
-
* `{ buffered: true }` when a write is coalesced into the buffer)
|
|
129
|
+
* `{ buffered: true }` when a write is coalesced into the buffer), with the
|
|
130
|
+
* saved fields' `revs`.
|
|
131
|
+
*
|
|
132
|
+
* A save whose `base` is stale for a field someone else changed returns
|
|
133
|
+
* `{ conflicts }` rather than throwing (#572). An Action's rejection reaches the
|
|
134
|
+
* client only as an error, which can't carry the current values the owner
|
|
135
|
+
* needs to choose between, so the conflict comes back as data. A save that
|
|
136
|
+
* changes a field another editor holds returns `{ locked }` the same way.
|
|
125
137
|
*/
|
|
126
138
|
export declare function louiseSaveDraftAction<Env extends EditorRouteEnv = EditorRouteEnv>(config: LouiseSaveDraftActionConfig<Env>): {
|
|
127
139
|
input: z.ZodObject<{
|
|
128
140
|
id: z.ZodNumber;
|
|
129
141
|
data: z.ZodRecord<z.ZodString, z.ZodUnknown>;
|
|
142
|
+
base: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
130
143
|
}, z.core.$strip>;
|
|
131
144
|
handler: (input: SaveDraftActionInput, context: EditorActionContext) => Promise<Record<string, unknown>>;
|
|
132
145
|
};
|
package/dist/actions.js
CHANGED
|
@@ -35,6 +35,7 @@
|
|
|
35
35
|
// the raw routes (`applyFieldSave`, `applySettingsPatch`), so nothing is parsed or
|
|
36
36
|
// written twice.
|
|
37
37
|
import { z } from "astro/zod";
|
|
38
|
+
import { toPageId } from "louise-toolkit/content";
|
|
38
39
|
import { D1_BOOKMARK_COOKIE } from "louise-toolkit/db";
|
|
39
40
|
import { applyFieldSave } from "louise-toolkit/editor";
|
|
40
41
|
import { applySettingsPatch } from "louise-toolkit/editor";
|
|
@@ -140,18 +141,32 @@ export function louiseSettingsAction(config) {
|
|
|
140
141
|
* the id). The handler shares the raw `versionsRoute` store path via
|
|
141
142
|
* {@link applySaveDraft}—the concurrent-surface merge base and the #70 KV
|
|
142
143
|
* write-buffer—and returns that path's JSON body (a created `version`, or
|
|
143
|
-
* `{ buffered: true }` when a write is coalesced into the buffer)
|
|
144
|
+
* `{ buffered: true }` when a write is coalesced into the buffer), with the
|
|
145
|
+
* saved fields' `revs`.
|
|
146
|
+
*
|
|
147
|
+
* A save whose `base` is stale for a field someone else changed returns
|
|
148
|
+
* `{ conflicts }` rather than throwing (#572). An Action's rejection reaches the
|
|
149
|
+
* client only as an error, which can't carry the current values the owner
|
|
150
|
+
* needs to choose between, so the conflict comes back as data. A save that
|
|
151
|
+
* changes a field another editor holds returns `{ locked }` the same way.
|
|
144
152
|
*/
|
|
145
153
|
export function louiseSaveDraftAction(config) {
|
|
146
154
|
const resolved = resolveDeps(config);
|
|
147
155
|
return {
|
|
148
156
|
input: z.object({
|
|
149
|
-
|
|
157
|
+
// Positive, like every row ID, so `toPageId` below can't throw on input
|
|
158
|
+
// this schema accepted.
|
|
159
|
+
id: z.number().int().positive(),
|
|
150
160
|
data: z.record(z.string(), z.unknown()),
|
|
161
|
+
base: z.record(z.string(), z.string()).optional(),
|
|
151
162
|
}),
|
|
152
163
|
handler: async (input, context) => {
|
|
153
164
|
const editor = requireEditor(resolved, context);
|
|
154
|
-
const result = await applySaveDraft(resolved.getEnv(context), config, editor, input.id, input.data);
|
|
165
|
+
const result = await applySaveDraft(resolved.getEnv(context), config, editor, toPageId(input.id), input.data, { base: input.base, softLocks: config.softLocks });
|
|
166
|
+
if (!result.ok && result.conflicts)
|
|
167
|
+
return { conflicts: result.conflicts };
|
|
168
|
+
if (!result.ok && result.locked)
|
|
169
|
+
return { locked: result.locked };
|
|
155
170
|
if (!result.ok)
|
|
156
171
|
throwActionError(resolved.ActionError, result.status, result.error);
|
|
157
172
|
// Persist the D1 bookmark so this Action's draft is read-your-writes on the
|
package/dist/index.d.ts
CHANGED
|
@@ -3,4 +3,5 @@ export { type CatalogLoaderConfig, defineCatalogLoader } from "./catalog.js";
|
|
|
3
3
|
export { collectionToAstroSchema, louiseLoader, type LouiseLoaderConfig, type LouiseRow, } from "./content-loader.js";
|
|
4
4
|
export { formToAstroSchema } from "./form-schema.js";
|
|
5
5
|
export { type ResumeReadSession, resumeReadSession } from "./resume.js";
|
|
6
|
+
export { type SeoHeadContext, type SeoHeadInput, seoHead } from "./seo.js";
|
|
6
7
|
export { createLouiseMiddleware, type LouiseMiddlewareConfig, type LouiseMiddlewareRateLimit, } from "./middleware.js";
|
package/dist/index.js
CHANGED
|
@@ -9,4 +9,5 @@ export { defineCatalogLoader } from "./catalog.js";
|
|
|
9
9
|
export { collectionToAstroSchema, louiseLoader, } from "./content-loader.js";
|
|
10
10
|
export { formToAstroSchema } from "./form-schema.js";
|
|
11
11
|
export { resumeReadSession } from "./resume.js";
|
|
12
|
+
export { seoHead } from "./seo.js";
|
|
12
13
|
export { createLouiseMiddleware, } from "./middleware.js";
|
package/dist/middleware.d.ts
CHANGED
|
@@ -18,8 +18,8 @@ export interface LouiseMiddlewareApiGate {
|
|
|
18
18
|
prefix?: string;
|
|
19
19
|
/**
|
|
20
20
|
* Paths under the prefix an anonymous request may still reach, on top of the
|
|
21
|
-
* toolkit's own public routes at their default mounts (forms and
|
|
22
|
-
* `isLouisePublicPath`).
|
|
21
|
+
* toolkit's own public routes at their default mounts (forms, vitals, and
|
|
22
|
+
* status—see `isLouisePublicPath`).
|
|
23
23
|
*
|
|
24
24
|
* A path, not a mark on the route, because middleware runs before it knows
|
|
25
25
|
* which route file will answer: a route can't declare itself public here the
|
|
@@ -113,6 +113,25 @@ export interface LouiseMiddlewareConfig<TEditor = unknown> {
|
|
|
113
113
|
* resolved on every request regardless.
|
|
114
114
|
*/
|
|
115
115
|
apiGate?: boolean | LouiseMiddlewareApiGate;
|
|
116
|
+
/**
|
|
117
|
+
* Where a path moved, for a GET or HEAD that would otherwise answer 404
|
|
118
|
+
* (#574). Return `{ location, status }` to redirect, or `null` to keep the 404.
|
|
119
|
+
* It runs only after the page answered 404, so a live page always wins. The
|
|
120
|
+
* visitor's query string carries over.
|
|
121
|
+
*
|
|
122
|
+
* ```ts
|
|
123
|
+
* redirectFor: (path) => resolvePageRedirect(db(env.DB), pageRedirects, path),
|
|
124
|
+
* ```
|
|
125
|
+
*
|
|
126
|
+
* A lookup that throws keeps the 404.
|
|
127
|
+
*/
|
|
128
|
+
redirectFor?: (pathname: string, context: APIContext) => {
|
|
129
|
+
location: string;
|
|
130
|
+
status: number;
|
|
131
|
+
} | null | Promise<{
|
|
132
|
+
location: string;
|
|
133
|
+
status: number;
|
|
134
|
+
} | null>;
|
|
116
135
|
/** Edit-mode cookie name. Default {@link LOUISE_EDIT_COOKIE} (`"louise_edit"`).
|
|
117
136
|
* Change it and the `withEdgeCache` bypass predicate must be told too, or an
|
|
118
137
|
* editor gets served the cached public page. */
|
package/dist/middleware.js
CHANGED
|
@@ -137,6 +137,21 @@ export function createLouiseMiddleware(config) {
|
|
|
137
137
|
// for host dispatch is another tenant's page.
|
|
138
138
|
const rewrite = await config.rewrite?.(context);
|
|
139
139
|
const response = rewrite === undefined ? await next() : await next(rewrite);
|
|
140
|
+
// A page that moved: answer its old URL with a redirect instead of the 404.
|
|
141
|
+
if (config.redirectFor &&
|
|
142
|
+
response.status === 404 &&
|
|
143
|
+
(context.request.method === "GET" || context.request.method === "HEAD")) {
|
|
144
|
+
// An async wrapper, so a lookup that throws before returning a promise
|
|
145
|
+
// keeps the 404 too.
|
|
146
|
+
const lookup = config.redirectFor;
|
|
147
|
+
const moved = await (async () => lookup(context.url.pathname, context))().catch(() => null);
|
|
148
|
+
if (moved) {
|
|
149
|
+
return finish(context, new Response(null, {
|
|
150
|
+
status: moved.status,
|
|
151
|
+
headers: { location: `${moved.location}${context.url.search}` },
|
|
152
|
+
}));
|
|
153
|
+
}
|
|
154
|
+
}
|
|
140
155
|
// An editor's JSON must not land in a shared cache. A route that chose its
|
|
141
156
|
// own policy keeps it.
|
|
142
157
|
if (gatedApi && !response.headers.has("cache-control")) {
|
package/dist/seo.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { type PageHeadInput } from "louise-toolkit/seo";
|
|
2
|
+
/** The part of `Astro` (or an `APIContext`) that `seoHead` reads. */
|
|
3
|
+
export interface SeoHeadContext {
|
|
4
|
+
/** The configured `site`, the canonical origin. */
|
|
5
|
+
site?: URL | undefined;
|
|
6
|
+
url: URL;
|
|
7
|
+
}
|
|
8
|
+
/** Everything `pageHead` takes except what the request supplies. */
|
|
9
|
+
export type SeoHeadInput = Omit<PageHeadInput, "origin" | "path"> & Partial<Pick<PageHeadInput, "origin">>;
|
|
10
|
+
/**
|
|
11
|
+
* The page's head tags as HTML, from `pageHead` and `renderHeadTags`. The
|
|
12
|
+
* origin is `input.origin`, then Astro's configured `site`, then the request's
|
|
13
|
+
* own origin; set `site` in `astro.config` so a preview host or a proxy can't
|
|
14
|
+
* leak into the canonical URL. The path is the request's.
|
|
15
|
+
*/
|
|
16
|
+
export declare function seoHead(context: SeoHeadContext, input: SeoHeadInput): string;
|
package/dist/seo.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// Copyright (c) 2026 BowenLabs. Louise Toolkit is MIT licensed.
|
|
2
|
+
//
|
|
3
|
+
// `pageHead` from louise-toolkit/seo, with the origin and path filled in from
|
|
4
|
+
// Astro's request context. The adapter ships no `.astro` components, so a page
|
|
5
|
+
// prints the result itself: `<Fragment set:html={seoHead(Astro, { … })} />`.
|
|
6
|
+
import { pageHead, renderHeadTags } from "louise-toolkit/seo";
|
|
7
|
+
/**
|
|
8
|
+
* The page's head tags as HTML, from `pageHead` and `renderHeadTags`. The
|
|
9
|
+
* origin is `input.origin`, then Astro's configured `site`, then the request's
|
|
10
|
+
* own origin; set `site` in `astro.config` so a preview host or a proxy can't
|
|
11
|
+
* leak into the canonical URL. The path is the request's.
|
|
12
|
+
*/
|
|
13
|
+
export function seoHead(context, input) {
|
|
14
|
+
const origin = input.origin ?? context.site?.origin ?? context.url.origin;
|
|
15
|
+
return renderHeadTags(pageHead({ ...input, origin, path: `${context.url.pathname}${context.url.search}` }));
|
|
16
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@louise-toolkit/astro",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Astro adapter for louise-toolkit: middleware, Actions, content-layer loaders, and the forms schema bridge.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"astro",
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"louise",
|
|
10
10
|
"louise-toolkit"
|
|
11
11
|
],
|
|
12
|
-
"homepage": "https://louisetoolkit.
|
|
12
|
+
"homepage": "https://docs.louisetoolkit.org",
|
|
13
13
|
"bugs": "https://github.com/bowenlabs/louise-toolkit/issues",
|
|
14
14
|
"license": "MIT",
|
|
15
15
|
"author": "BowenLabs",
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
"access": "public"
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
|
-
"louise-toolkit": "0.
|
|
38
|
+
"louise-toolkit": "0.35.0"
|
|
39
39
|
},
|
|
40
40
|
"devDependencies": {
|
|
41
41
|
"@cloudflare/workers-types": "^5.20260829.1",
|