@ampless/runtime 0.2.0-alpha.8 → 1.0.0-alpha.13
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.ja.md +86 -0
- package/README.md +4 -1
- package/dist/dispatchers/index.d.ts +21 -11
- package/dist/dispatchers/index.js +10 -20
- package/dist/index.d.ts +29 -26
- package/dist/index.js +174 -95
- package/dist/middleware.d.ts +16 -14
- package/dist/middleware.js +8 -17
- package/dist/routes/index.d.ts +48 -74
- package/dist/routes/index.js +84 -79
- package/package.json +4 -3
package/dist/routes/index.d.ts
CHANGED
|
@@ -2,112 +2,86 @@ import { Ampless } from '../index.js';
|
|
|
2
2
|
import 'ampless';
|
|
3
3
|
import 'next';
|
|
4
4
|
|
|
5
|
-
interface Ctx$
|
|
5
|
+
interface Ctx$3 {
|
|
6
6
|
params: Promise<{
|
|
7
7
|
siteId: string;
|
|
8
8
|
slug: string;
|
|
9
9
|
}>;
|
|
10
10
|
}
|
|
11
|
-
type OgRouteHandler = (req: Request, ctx: Ctx$
|
|
11
|
+
type OgRouteHandler = (req: Request, ctx: Ctx$3) => Promise<Response>;
|
|
12
12
|
declare function createOgRouteHandler(ampless: Ampless): OgRouteHandler;
|
|
13
13
|
|
|
14
|
-
interface Ctx$
|
|
14
|
+
interface Ctx$2 {
|
|
15
15
|
params: Promise<{
|
|
16
16
|
siteId: string;
|
|
17
17
|
}>;
|
|
18
18
|
}
|
|
19
|
-
type SitemapRouteHandler = (req: Request, ctx: Ctx$
|
|
19
|
+
type SitemapRouteHandler = (req: Request, ctx: Ctx$2) => Promise<Response>;
|
|
20
20
|
/**
|
|
21
|
-
* Sitemap route delegate. Looks up the active theme
|
|
22
|
-
*
|
|
23
|
-
*
|
|
21
|
+
* Sitemap route delegate. Looks up the active theme and forwards to
|
|
22
|
+
* whichever `routes.sitemap` handler the theme provides. Themes
|
|
23
|
+
* without a sitemap handler return 404.
|
|
24
24
|
*/
|
|
25
25
|
declare function createSitemapRouteHandler(ampless: Ampless): SitemapRouteHandler;
|
|
26
26
|
|
|
27
|
-
interface Ctx$
|
|
27
|
+
interface Ctx$1 {
|
|
28
28
|
params: Promise<{
|
|
29
29
|
siteId: string;
|
|
30
30
|
}>;
|
|
31
31
|
}
|
|
32
|
-
type FeedRouteHandler = (req: Request, ctx: Ctx$
|
|
32
|
+
type FeedRouteHandler = (req: Request, ctx: Ctx$1) => Promise<Response>;
|
|
33
33
|
/**
|
|
34
|
-
* Feed route delegate. Looks up the active theme
|
|
35
|
-
*
|
|
36
|
-
*
|
|
34
|
+
* Feed route delegate. Looks up the active theme and forwards to
|
|
35
|
+
* whichever `routes.feed` handler the theme provides. Themes without
|
|
36
|
+
* a feed handler return 404.
|
|
37
37
|
*/
|
|
38
38
|
declare function createFeedRouteHandler(ampless: Ampless): FeedRouteHandler;
|
|
39
39
|
|
|
40
|
-
interface Ctx
|
|
40
|
+
interface Ctx {
|
|
41
41
|
params: Promise<{
|
|
42
|
-
siteId: string;
|
|
43
42
|
slug: string;
|
|
43
|
+
path?: string[];
|
|
44
44
|
}>;
|
|
45
45
|
}
|
|
46
|
-
type
|
|
46
|
+
type UnderscoreRouteHandler = (req: Request, ctx: Ctx) => Promise<Response>;
|
|
47
47
|
/**
|
|
48
|
-
*
|
|
49
|
-
* entire HTTP response — no Next.js root layout, no theme chrome.
|
|
50
|
-
*
|
|
51
|
-
* Reached via the data-driven dispatcher path: the theme post page
|
|
52
|
-
* checks `post.metadata.no_layout` and redirects to `/raw/<slug>` when
|
|
53
|
-
* set. Posts without that flag never hit this route in practice; the
|
|
54
|
-
* handler additionally enforces the same check here so a direct
|
|
55
|
-
* `/raw/<slug>` request can't bypass the theme for a post that wasn't
|
|
56
|
-
* marked no-layout (would otherwise surface the body without any
|
|
57
|
-
* chrome the editor didn't intend).
|
|
48
|
+
* Unified `/_/<slug>(/...)` route handler.
|
|
58
49
|
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
* page can't replace the document. A `route.ts` returns a Response
|
|
62
|
-
* directly and bypasses React rendering entirely.
|
|
50
|
+
* The public URL prefix `/_/` is reserved for two related cases that
|
|
51
|
+
* both need to bypass the theme's post page:
|
|
63
52
|
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
* about those, use `format: 'html'`.
|
|
72
|
-
*
|
|
73
|
-
* Trust model: the response body is the editor's content verbatim,
|
|
74
|
-
* with no sanitization. Same trust assumption as `format: 'html'`
|
|
75
|
-
* post bodies on the regular path — see
|
|
76
|
-
* docs/architecture/04-access-layer-mcp.md §"editor の信頼モデル".
|
|
77
|
-
*/
|
|
78
|
-
declare function createRawRouteHandler(ampless: Ampless): RawRouteHandler;
|
|
79
|
-
|
|
80
|
-
interface Ctx {
|
|
81
|
-
params: Promise<{
|
|
82
|
-
siteId: string;
|
|
83
|
-
path: string[];
|
|
84
|
-
}>;
|
|
85
|
-
}
|
|
86
|
-
type StaticRouteHandler = (req: Request, ctx: Ctx) => Promise<Response>;
|
|
87
|
-
/**
|
|
88
|
-
* Catch-all route for static bundles. Lives at
|
|
89
|
-
* `app/site/[siteId]/[...path]/route.ts` and serves every file
|
|
90
|
-
* inside a `format: 'static'` post's bundle. The first segment of
|
|
91
|
-
* `path` is the post slug; the rest is the relative path inside the
|
|
92
|
-
* bundle (or empty, in which case the manifest's entrypoint is used).
|
|
53
|
+
* 1. `format: 'html'` posts with `metadata.no_layout === true` — the
|
|
54
|
+
* body is its own complete HTML document and ships as the entire
|
|
55
|
+
* response. URL: `/_/<slug>` (no trailing slash, no extra path).
|
|
56
|
+
* 2. `format: 'static'` posts — the body is a manifest describing a
|
|
57
|
+
* bundle of files in S3 at `public/static/<slug>/`. The bundle's
|
|
58
|
+
* entrypoint is served at `/_/<slug>/`, every internal file at
|
|
59
|
+
* `/_/<slug>/<relative-path>`.
|
|
93
60
|
*
|
|
94
|
-
* Routing
|
|
95
|
-
* `[siteId]/[slug]/
|
|
96
|
-
*
|
|
97
|
-
*
|
|
98
|
-
*
|
|
61
|
+
* Routing model:
|
|
62
|
+
* - File location: `app/site/[siteId]/r/[slug]/[[...path]]/route.ts`.
|
|
63
|
+
* The literal folder is `r/` (not `_/`) because Next.js's App
|
|
64
|
+
* Router skips any path part starting with `_` during route
|
|
65
|
+
* discovery (see `recursive-readdir` + `ignorePartFilter` in
|
|
66
|
+
* next/dist/build/route-discovery.js). Middleware rewrites the
|
|
67
|
+
* public `/_/` prefix to `/r/` internally; the public URL stays
|
|
68
|
+
* `/_/<slug>(/...)`.
|
|
69
|
+
* - `params.path` is `undefined` (or `[]`) for single-segment
|
|
70
|
+
* requests `/_/<slug>`, an array of remaining segments otherwise.
|
|
99
71
|
*
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
*
|
|
72
|
+
* Trailing-slash responsibility lives here, not in the dispatcher:
|
|
73
|
+
* the dispatcher redirects `format='static'` posts to `/_/<slug>`,
|
|
74
|
+
* and this handler then 308-redirects to `/_/<slug>/` to anchor
|
|
75
|
+
* relative paths inside the bundle. Same anchoring reason as the
|
|
76
|
+
* legacy static route's behaviour — `<img src="img.png">` must resolve
|
|
77
|
+
* to `/_/<slug>/img.png`, not the site root.
|
|
103
78
|
*
|
|
104
|
-
*
|
|
105
|
-
*
|
|
106
|
-
*
|
|
107
|
-
*
|
|
108
|
-
*
|
|
109
|
-
* fresh.
|
|
79
|
+
* Trust model: no_layout HTML bodies are emitted verbatim, same trust
|
|
80
|
+
* shape as `format: 'html'` post bodies on the regular path. Static
|
|
81
|
+
* bundle assets are served via short-lived S3 presigned URLs; the
|
|
82
|
+
* bucket stays private. See docs/architecture/04-access-layer-mcp.md
|
|
83
|
+
* §"editor の信頼モデル".
|
|
110
84
|
*/
|
|
111
|
-
declare function
|
|
85
|
+
declare function createUnderscoreRouteHandler(ampless: Ampless): UnderscoreRouteHandler;
|
|
112
86
|
|
|
113
|
-
export { type FeedRouteHandler, type OgRouteHandler, type
|
|
87
|
+
export { type FeedRouteHandler, type OgRouteHandler, type SitemapRouteHandler, type UnderscoreRouteHandler, createFeedRouteHandler, createOgRouteHandler, createSitemapRouteHandler, createUnderscoreRouteHandler };
|
package/dist/routes/index.js
CHANGED
|
@@ -6,13 +6,13 @@ function hasOgImage(p) {
|
|
|
6
6
|
}
|
|
7
7
|
function createOgRouteHandler(ampless) {
|
|
8
8
|
return async function GET(_req, { params }) {
|
|
9
|
-
const {
|
|
9
|
+
const { slug } = await params;
|
|
10
10
|
const cleanSlug = slug.replace(/\.png$/, "");
|
|
11
|
-
const post = await ampless.getPublishedPost(cleanSlug
|
|
11
|
+
const post = await ampless.getPublishedPost(cleanSlug);
|
|
12
12
|
if (!post) return new Response("not found", { status: 404 });
|
|
13
13
|
const plugin = (ampless.cmsConfig.plugins ?? []).find(hasOgImage);
|
|
14
14
|
if (!plugin) return new Response("og not configured", { status: 404 });
|
|
15
|
-
const settings = await ampless.loadSiteSettings(
|
|
15
|
+
const settings = await ampless.loadSiteSettings();
|
|
16
16
|
const fonts = await Promise.all(
|
|
17
17
|
plugin.ogImage.fonts.map(async (f) => ({
|
|
18
18
|
name: f.name,
|
|
@@ -43,122 +43,127 @@ function createOgRouteHandler(ampless) {
|
|
|
43
43
|
|
|
44
44
|
// src/routes/sitemap.ts
|
|
45
45
|
function createSitemapRouteHandler(ampless) {
|
|
46
|
-
return async function GET(request
|
|
47
|
-
const {
|
|
48
|
-
const { module } = await ampless.resolveActiveTheme(siteId);
|
|
46
|
+
return async function GET(request) {
|
|
47
|
+
const { module } = await ampless.resolveActiveTheme();
|
|
49
48
|
const handler = module.routes?.sitemap;
|
|
50
49
|
if (!handler) {
|
|
51
50
|
return new Response("sitemap not implemented for this theme", { status: 404 });
|
|
52
51
|
}
|
|
53
|
-
return handler({
|
|
52
|
+
return handler({ request });
|
|
54
53
|
};
|
|
55
54
|
}
|
|
56
55
|
|
|
57
56
|
// src/routes/feed.ts
|
|
58
57
|
function createFeedRouteHandler(ampless) {
|
|
59
|
-
return async function GET(request
|
|
60
|
-
const {
|
|
61
|
-
const { module } = await ampless.resolveActiveTheme(siteId);
|
|
58
|
+
return async function GET(request) {
|
|
59
|
+
const { module } = await ampless.resolveActiveTheme();
|
|
62
60
|
const handler = module.routes?.feed;
|
|
63
61
|
if (!handler) {
|
|
64
62
|
return new Response("feed not implemented for this theme", { status: 404 });
|
|
65
63
|
}
|
|
66
|
-
return handler({
|
|
64
|
+
return handler({ request });
|
|
67
65
|
};
|
|
68
66
|
}
|
|
69
67
|
|
|
70
|
-
// src/routes/
|
|
71
|
-
function createRawRouteHandler(ampless) {
|
|
72
|
-
return async function GET(_request, { params }) {
|
|
73
|
-
const { siteId, slug } = await params;
|
|
74
|
-
const post = await ampless.getPublishedPost(slug, { siteId });
|
|
75
|
-
if (!post) {
|
|
76
|
-
return new Response("Not Found", { status: 404 });
|
|
77
|
-
}
|
|
78
|
-
if (post.metadata?.no_layout !== true) {
|
|
79
|
-
return new Response("Not Found", { status: 404 });
|
|
80
|
-
}
|
|
81
|
-
return new Response(ampless.renderBody(post), {
|
|
82
|
-
status: 200,
|
|
83
|
-
headers: {
|
|
84
|
-
"Content-Type": "text/html; charset=utf-8",
|
|
85
|
-
"Cache-Control": "public, max-age=300"
|
|
86
|
-
}
|
|
87
|
-
});
|
|
88
|
-
};
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
// src/routes/static.ts
|
|
68
|
+
// src/routes/underscore.ts
|
|
92
69
|
import { createServerRunner } from "@aws-amplify/adapter-nextjs";
|
|
93
70
|
import { cookies } from "next/headers";
|
|
94
71
|
import { getUrl } from "aws-amplify/storage/server";
|
|
95
|
-
|
|
96
|
-
".html": "text/html; charset=utf-8",
|
|
97
|
-
".htm": "text/html; charset=utf-8"
|
|
98
|
-
};
|
|
99
|
-
function createStaticRouteHandler(ampless) {
|
|
72
|
+
function createUnderscoreRouteHandler(ampless) {
|
|
100
73
|
const { runWithAmplifyServerContext } = createServerRunner({
|
|
101
74
|
config: ampless.outputs
|
|
102
75
|
});
|
|
103
76
|
return async function GET(request, { params }) {
|
|
104
|
-
const {
|
|
105
|
-
|
|
77
|
+
const { slug, path } = await params;
|
|
78
|
+
const restSegments = path ?? [];
|
|
79
|
+
const post = await ampless.getPublishedPost(slug);
|
|
80
|
+
if (!post) {
|
|
81
|
+
return new Response("Not Found", { status: 404 });
|
|
82
|
+
}
|
|
83
|
+
if (restSegments.length === 0) {
|
|
84
|
+
if (post.format === "html" && post.metadata?.no_layout === true) {
|
|
85
|
+
return new Response(ampless.renderBody(post), {
|
|
86
|
+
status: 200,
|
|
87
|
+
headers: {
|
|
88
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
89
|
+
"Cache-Control": "public, max-age=300"
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
if (post.format === "static") {
|
|
94
|
+
const url = new URL(request.url);
|
|
95
|
+
if (!url.pathname.endsWith("/")) {
|
|
96
|
+
const next = new URL(url);
|
|
97
|
+
next.pathname = `${url.pathname}/`;
|
|
98
|
+
return Response.redirect(next.toString(), 308);
|
|
99
|
+
}
|
|
100
|
+
const body2 = post.body ?? null;
|
|
101
|
+
const entrypoint = typeof body2?.entrypoint === "string" && body2.entrypoint ? body2.entrypoint : "index.html";
|
|
102
|
+
const presignedUrl2 = await signStaticAsset({
|
|
103
|
+
runWithAmplifyServerContext,
|
|
104
|
+
slug,
|
|
105
|
+
rest: entrypoint
|
|
106
|
+
});
|
|
107
|
+
if (!presignedUrl2) {
|
|
108
|
+
return new Response("Not Found", { status: 404 });
|
|
109
|
+
}
|
|
110
|
+
return Response.redirect(presignedUrl2, 302);
|
|
111
|
+
}
|
|
112
|
+
return new Response("Not Found", { status: 404 });
|
|
113
|
+
}
|
|
114
|
+
if (post.format !== "static") {
|
|
106
115
|
return new Response("Not Found", { status: 404 });
|
|
107
116
|
}
|
|
108
|
-
const slug = path[0];
|
|
109
|
-
const restSegments = path.slice(1);
|
|
110
117
|
if (restSegments.some(
|
|
111
118
|
(s) => !s || s === "." || s === ".." || s.includes("/") || s.includes("\\") || s.includes("\0")
|
|
112
119
|
)) {
|
|
113
120
|
return new Response("Invalid path", { status: 400 });
|
|
114
121
|
}
|
|
115
|
-
const post = await ampless.getPublishedPost(slug, { siteId });
|
|
116
|
-
if (!post || post.format !== "static") {
|
|
117
|
-
return new Response("Not Found", { status: 404 });
|
|
118
|
-
}
|
|
119
122
|
const body = post.body ?? null;
|
|
120
|
-
const entrypoint = typeof body?.entrypoint === "string" && body.entrypoint ? body.entrypoint : "index.html";
|
|
121
123
|
const fileList = Array.isArray(body?.files) ? body.files : [];
|
|
122
|
-
|
|
123
|
-
if (rest === "") {
|
|
124
|
-
const url = new URL(request.url);
|
|
125
|
-
if (!url.pathname.endsWith("/")) {
|
|
126
|
-
const next = new URL(url);
|
|
127
|
-
next.pathname = `${url.pathname}/`;
|
|
128
|
-
return Response.redirect(next.toString(), 308);
|
|
129
|
-
}
|
|
130
|
-
rest = entrypoint;
|
|
131
|
-
}
|
|
124
|
+
const rest = restSegments.join("/");
|
|
132
125
|
if (fileList.length > 0 && !fileList.includes(rest)) {
|
|
133
126
|
return new Response("Not Found", { status: 404 });
|
|
134
127
|
}
|
|
135
|
-
const
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
options: { expiresIn: 60 * 60 }
|
|
143
|
-
});
|
|
144
|
-
return result.url.toString();
|
|
145
|
-
}
|
|
146
|
-
});
|
|
147
|
-
return Response.redirect(presignedUrl, 302);
|
|
148
|
-
} catch {
|
|
149
|
-
const ext = rest.slice(rest.lastIndexOf("."));
|
|
150
|
-
const fallback = FALLBACK_MIME[ext] ?? "text/plain; charset=utf-8";
|
|
151
|
-
return new Response("Not Found", {
|
|
152
|
-
status: 404,
|
|
153
|
-
headers: { "Content-Type": fallback }
|
|
154
|
-
});
|
|
128
|
+
const presignedUrl = await signStaticAsset({
|
|
129
|
+
runWithAmplifyServerContext,
|
|
130
|
+
slug,
|
|
131
|
+
rest
|
|
132
|
+
});
|
|
133
|
+
if (!presignedUrl) {
|
|
134
|
+
return new Response("Not Found", { status: 404 });
|
|
155
135
|
}
|
|
136
|
+
return Response.redirect(presignedUrl, 302);
|
|
156
137
|
};
|
|
157
138
|
}
|
|
139
|
+
async function signStaticAsset({
|
|
140
|
+
runWithAmplifyServerContext,
|
|
141
|
+
slug,
|
|
142
|
+
rest
|
|
143
|
+
}) {
|
|
144
|
+
const objectPath = `public/static/${slug}/${rest}`;
|
|
145
|
+
try {
|
|
146
|
+
return await runWithAmplifyServerContext({
|
|
147
|
+
nextServerContext: { cookies },
|
|
148
|
+
operation: async (amplifyContext) => {
|
|
149
|
+
const result = await getUrl(amplifyContext, {
|
|
150
|
+
path: objectPath,
|
|
151
|
+
options: { expiresIn: 60 * 60 }
|
|
152
|
+
});
|
|
153
|
+
return result.url.toString();
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
} catch (err) {
|
|
157
|
+
console.error(
|
|
158
|
+
`[underscore-route] presign failed for ${objectPath}:`,
|
|
159
|
+
err
|
|
160
|
+
);
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
158
164
|
export {
|
|
159
165
|
createFeedRouteHandler,
|
|
160
166
|
createOgRouteHandler,
|
|
161
|
-
createRawRouteHandler,
|
|
162
167
|
createSitemapRouteHandler,
|
|
163
|
-
|
|
168
|
+
createUnderscoreRouteHandler
|
|
164
169
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ampless/runtime",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.0-alpha.13",
|
|
4
4
|
"description": "Public-side runtime for ampless: post fetching, theme dispatch, middleware, public route handlers",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -47,9 +47,10 @@
|
|
|
47
47
|
"class-variance-authority": "^0.7.1",
|
|
48
48
|
"clsx": "^2.1.1",
|
|
49
49
|
"lucide-react": "^1.16.0",
|
|
50
|
+
"marked": "^14.1.4",
|
|
50
51
|
"tailwind-merge": "^3.6.0",
|
|
51
|
-
"ampless": "0.
|
|
52
|
-
"@ampless/plugin-og-image": "0.2.0-alpha.
|
|
52
|
+
"ampless": "1.0.0-alpha.9",
|
|
53
|
+
"@ampless/plugin-og-image": "0.2.0-alpha.9"
|
|
53
54
|
},
|
|
54
55
|
"peerDependencies": {
|
|
55
56
|
"next": "^15 || ^16",
|