@ampless/runtime 0.2.0-alpha.3 → 0.2.0-alpha.5
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/dispatchers/index.d.ts +18 -2
- package/dist/dispatchers/index.js +11 -2
- package/dist/index.d.ts +1 -0
- package/dist/index.js +14 -1
- package/dist/middleware.d.ts +7 -1
- package/dist/middleware.js +2 -8
- package/dist/routes/index.d.ts +49 -13
- package/dist/routes/index.js +73 -1
- package/package.json +3 -3
|
@@ -29,8 +29,24 @@ type ThemePostDispatcher = (props: Props$1) => Promise<ReactNode>;
|
|
|
29
29
|
type ThemePostMetadata = (props: Props$1) => Promise<Metadata>;
|
|
30
30
|
/**
|
|
31
31
|
* Post page dispatcher. Resolves the active theme and renders the
|
|
32
|
-
* theme's `components.Post` server component.
|
|
33
|
-
*
|
|
32
|
+
* theme's `components.Post` server component.
|
|
33
|
+
*
|
|
34
|
+
* Before delegating, the dispatcher peeks at the post's metadata: if
|
|
35
|
+
* `metadata.no_layout === true`, the post is meant to be served as
|
|
36
|
+
* bare HTML (own DOCTYPE, no theme chrome, no Next.js root layout).
|
|
37
|
+
* Next.js page.tsx can't bypass the root layout, so we 308-redirect
|
|
38
|
+
* to the raw route handler at `/raw/<slug>` which returns the body
|
|
39
|
+
* unchanged. The redirect is permanent because no_layout is a
|
|
40
|
+
* persistent property of the post — bookmarks naturally settle on
|
|
41
|
+
* the `/raw/` URL.
|
|
42
|
+
*
|
|
43
|
+
* The metadata peek is an extra AppSync call before theme resolve,
|
|
44
|
+
* but it's the same query the theme's Post component would make
|
|
45
|
+
* anyway; AppSync's query-level dedupe within a single request keeps
|
|
46
|
+
* the wire cost flat in practice.
|
|
47
|
+
*
|
|
48
|
+
* If the theme doesn't declare a Post component at all, returns
|
|
49
|
+
* Next.js's notFound() (404).
|
|
34
50
|
*/
|
|
35
51
|
declare function createThemePostDispatcher(ampless: Ampless): ThemePostDispatcher;
|
|
36
52
|
/** generateMetadata factory for the post dispatcher. */
|
|
@@ -17,10 +17,19 @@ function createThemeHomeMetadata(ampless) {
|
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
// src/dispatchers/post.ts
|
|
20
|
-
import { notFound } from "next/navigation";
|
|
20
|
+
import { notFound, redirect } from "next/navigation";
|
|
21
21
|
function createThemePostDispatcher(ampless) {
|
|
22
22
|
return async function SitePostDispatcher({ params }) {
|
|
23
|
-
const { siteId } = await params;
|
|
23
|
+
const { siteId, slug } = await params;
|
|
24
|
+
const post = await ampless.getPublishedPost(slug, { siteId });
|
|
25
|
+
if (post?.metadata?.no_layout === true) {
|
|
26
|
+
redirect(`/raw/${slug}`);
|
|
27
|
+
}
|
|
28
|
+
if (post?.format === "static") {
|
|
29
|
+
const body = post.body ?? null;
|
|
30
|
+
const entrypoint = typeof body?.entrypoint === "string" && body.entrypoint ? body.entrypoint : "index.html";
|
|
31
|
+
redirect(`/${slug}/${entrypoint}`);
|
|
32
|
+
}
|
|
24
33
|
const { module } = await ampless.resolveActiveTheme(siteId);
|
|
25
34
|
const Post = module.components.Post;
|
|
26
35
|
if (!Post) notFound();
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -28,6 +28,18 @@ function decodeBody(value) {
|
|
|
28
28
|
return value;
|
|
29
29
|
}
|
|
30
30
|
}
|
|
31
|
+
function decodeMetadata(value) {
|
|
32
|
+
if (value === null || value === void 0) return void 0;
|
|
33
|
+
const parsed = typeof value === "string" ? safeJsonParse(value) : value;
|
|
34
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : void 0;
|
|
35
|
+
}
|
|
36
|
+
function safeJsonParse(value) {
|
|
37
|
+
try {
|
|
38
|
+
return JSON.parse(value);
|
|
39
|
+
} catch {
|
|
40
|
+
return value;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
31
43
|
function toCorePost(p) {
|
|
32
44
|
return {
|
|
33
45
|
postId: p.postId,
|
|
@@ -39,7 +51,8 @@ function toCorePost(p) {
|
|
|
39
51
|
body: decodeBody(p.body),
|
|
40
52
|
status: p.status ?? "draft",
|
|
41
53
|
publishedAt: p.publishedAt ?? void 0,
|
|
42
|
-
tags: (p.tags ?? []).filter((t) => typeof t === "string")
|
|
54
|
+
tags: (p.tags ?? []).filter((t) => typeof t === "string"),
|
|
55
|
+
metadata: decodeMetadata(p.metadata)
|
|
43
56
|
};
|
|
44
57
|
}
|
|
45
58
|
function createPostsApi(outputs) {
|
package/dist/middleware.d.ts
CHANGED
|
@@ -10,12 +10,18 @@ type MiddlewareFn = (request: NextRequest) => NextResponse;
|
|
|
10
10
|
*
|
|
11
11
|
* - hostname → siteId resolution (multi-site rewrites)
|
|
12
12
|
* - `/path` → `/site/<siteId>/path` internal rewrite
|
|
13
|
-
* - `<slug>.html` → `/site/<siteId>/raw/<slug>.html` (bare HTML route)
|
|
14
13
|
* - `?previewTheme=<name>` → `x-preview-theme` header forwarding
|
|
15
14
|
* - `Cache-Control: private, no-store` in multi-site mode (Amplify
|
|
16
15
|
* Hosting's CloudFront cache key doesn't include Host, so SSR
|
|
17
16
|
* responses would cross-contaminate at the same path)
|
|
18
17
|
*
|
|
18
|
+
* No-layout / bare-HTML routing is data-driven: the theme post
|
|
19
|
+
* dispatcher reads `post.metadata.no_layout` and redirects to
|
|
20
|
+
* `/raw/<slug>` when set. The plain prefix-prepend below picks up
|
|
21
|
+
* that path because the raw route lives at
|
|
22
|
+
* `app/site/<siteId>/raw/<slug>/route.ts` — no special middleware
|
|
23
|
+
* branch is needed.
|
|
24
|
+
*
|
|
19
25
|
* The factory captures the multi-site flag at construction time so the
|
|
20
26
|
* hot path stays a pair of cheap header lookups.
|
|
21
27
|
*/
|
package/dist/middleware.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
// src/middleware.ts
|
|
2
2
|
import { NextResponse } from "next/server";
|
|
3
3
|
import { resolveSiteId, isMultiSite } from "ampless";
|
|
4
|
-
var RAW_HTML_PATH_RE = /^\/([^/]+\.html)$/;
|
|
5
4
|
function createAmplessMiddleware({ cmsConfig }) {
|
|
6
5
|
const MULTI_SITE = isMultiSite(cmsConfig);
|
|
7
6
|
return function middleware(request) {
|
|
@@ -12,13 +11,8 @@ function createAmplessMiddleware({ cmsConfig }) {
|
|
|
12
11
|
}
|
|
13
12
|
const url = request.nextUrl.clone();
|
|
14
13
|
if (!url.pathname.startsWith("/site/")) {
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
url.pathname = `/site/${siteId}/raw/${rawMatch[1]}`;
|
|
18
|
-
} else {
|
|
19
|
-
const tail = url.pathname === "/" ? "" : url.pathname;
|
|
20
|
-
url.pathname = `/site/${siteId}${tail}`;
|
|
21
|
-
}
|
|
14
|
+
const tail = url.pathname === "/" ? "" : url.pathname;
|
|
15
|
+
url.pathname = `/site/${siteId}${tail}`;
|
|
22
16
|
}
|
|
23
17
|
const previewTheme = url.searchParams.get("previewTheme");
|
|
24
18
|
const requestHeaders = new Headers(request.headers);
|
package/dist/routes/index.d.ts
CHANGED
|
@@ -2,21 +2,21 @@ import { Ampless } from '../index.js';
|
|
|
2
2
|
import 'ampless';
|
|
3
3
|
import 'next';
|
|
4
4
|
|
|
5
|
-
interface Ctx$
|
|
5
|
+
interface Ctx$4 {
|
|
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$4) => Promise<Response>;
|
|
12
12
|
declare function createOgRouteHandler(ampless: Ampless): OgRouteHandler;
|
|
13
13
|
|
|
14
|
-
interface Ctx$
|
|
14
|
+
interface Ctx$3 {
|
|
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$3) => Promise<Response>;
|
|
20
20
|
/**
|
|
21
21
|
* Sitemap route delegate. Looks up the active theme for the request's
|
|
22
22
|
* siteId and forwards to whichever `routes.sitemap` handler the theme
|
|
@@ -24,12 +24,12 @@ type SitemapRouteHandler = (req: Request, ctx: Ctx$2) => Promise<Response>;
|
|
|
24
24
|
*/
|
|
25
25
|
declare function createSitemapRouteHandler(ampless: Ampless): SitemapRouteHandler;
|
|
26
26
|
|
|
27
|
-
interface Ctx$
|
|
27
|
+
interface Ctx$2 {
|
|
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$2) => Promise<Response>;
|
|
33
33
|
/**
|
|
34
34
|
* Feed route delegate. Looks up the active theme for the request's
|
|
35
35
|
* siteId and forwards to whichever `routes.feed` handler the theme
|
|
@@ -37,21 +37,24 @@ type FeedRouteHandler = (req: Request, ctx: Ctx$1) => Promise<Response>;
|
|
|
37
37
|
*/
|
|
38
38
|
declare function createFeedRouteHandler(ampless: Ampless): FeedRouteHandler;
|
|
39
39
|
|
|
40
|
-
interface Ctx {
|
|
40
|
+
interface Ctx$1 {
|
|
41
41
|
params: Promise<{
|
|
42
42
|
siteId: string;
|
|
43
43
|
slug: string;
|
|
44
44
|
}>;
|
|
45
45
|
}
|
|
46
|
-
type RawRouteHandler = (req: Request, ctx: Ctx) => Promise<Response>;
|
|
46
|
+
type RawRouteHandler = (req: Request, ctx: Ctx$1) => Promise<Response>;
|
|
47
47
|
/**
|
|
48
48
|
* Bare HTML route. Returns the published post's rendered body as the
|
|
49
49
|
* entire HTTP response — no Next.js root layout, no theme chrome.
|
|
50
50
|
*
|
|
51
|
-
* Reached via the
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
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).
|
|
55
58
|
*
|
|
56
59
|
* Why this is a route handler (not a page): Next.js's root layout
|
|
57
60
|
* always emits `<html>` / `<head>` / `<body>`, which means a normal
|
|
@@ -74,4 +77,37 @@ type RawRouteHandler = (req: Request, ctx: Ctx) => Promise<Response>;
|
|
|
74
77
|
*/
|
|
75
78
|
declare function createRawRouteHandler(ampless: Ampless): RawRouteHandler;
|
|
76
79
|
|
|
77
|
-
|
|
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).
|
|
93
|
+
*
|
|
94
|
+
* Routing precedence: Next.js prefers the more specific
|
|
95
|
+
* `[siteId]/[slug]/page.tsx` for single-segment URLs, so this catch-all
|
|
96
|
+
* only fires when path.length >= 2. The post dispatcher detects
|
|
97
|
+
* `format === 'static'` and 308-redirects to `/<slug>/<entrypoint>`
|
|
98
|
+
* so the catch-all picks up from there.
|
|
99
|
+
*
|
|
100
|
+
* Single-segment requests (`/<siteId>/<slug>`) that somehow do reach
|
|
101
|
+
* here are 308-redirected to add a trailing slash, which is what makes
|
|
102
|
+
* relative paths in the bundle resolve correctly in the browser.
|
|
103
|
+
*
|
|
104
|
+
* Implementation: generate a short-lived presigned URL for the S3
|
|
105
|
+
* object and 302-redirect to it. Same model as `/api/media/[...path]`
|
|
106
|
+
* — keeps the bucket private while letting CloudFront cache the 302
|
|
107
|
+
* for the Cache-Control window. The presigned URL itself expires in
|
|
108
|
+
* an hour; the 302 cache is 5 minutes so re-issued presigns stay
|
|
109
|
+
* fresh.
|
|
110
|
+
*/
|
|
111
|
+
declare function createStaticRouteHandler(ampless: Ampless): StaticRouteHandler;
|
|
112
|
+
|
|
113
|
+
export { type FeedRouteHandler, type OgRouteHandler, type RawRouteHandler, type SitemapRouteHandler, type StaticRouteHandler, createFeedRouteHandler, createOgRouteHandler, createRawRouteHandler, createSitemapRouteHandler, createStaticRouteHandler };
|
package/dist/routes/index.js
CHANGED
|
@@ -75,6 +75,9 @@ function createRawRouteHandler(ampless) {
|
|
|
75
75
|
if (!post) {
|
|
76
76
|
return new Response("Not Found", { status: 404 });
|
|
77
77
|
}
|
|
78
|
+
if (post.metadata?.no_layout !== true) {
|
|
79
|
+
return new Response("Not Found", { status: 404 });
|
|
80
|
+
}
|
|
78
81
|
return new Response(ampless.renderBody(post), {
|
|
79
82
|
status: 200,
|
|
80
83
|
headers: {
|
|
@@ -84,9 +87,78 @@ function createRawRouteHandler(ampless) {
|
|
|
84
87
|
});
|
|
85
88
|
};
|
|
86
89
|
}
|
|
90
|
+
|
|
91
|
+
// src/routes/static.ts
|
|
92
|
+
import { createServerRunner } from "@aws-amplify/adapter-nextjs";
|
|
93
|
+
import { cookies } from "next/headers";
|
|
94
|
+
import { getUrl } from "aws-amplify/storage/server";
|
|
95
|
+
var FALLBACK_MIME = {
|
|
96
|
+
".html": "text/html; charset=utf-8",
|
|
97
|
+
".htm": "text/html; charset=utf-8"
|
|
98
|
+
};
|
|
99
|
+
function createStaticRouteHandler(ampless) {
|
|
100
|
+
const { runWithAmplifyServerContext } = createServerRunner({
|
|
101
|
+
config: ampless.outputs
|
|
102
|
+
});
|
|
103
|
+
return async function GET(request, { params }) {
|
|
104
|
+
const { siteId, path } = await params;
|
|
105
|
+
if (!path || path.length === 0) {
|
|
106
|
+
return new Response("Not Found", { status: 404 });
|
|
107
|
+
}
|
|
108
|
+
const slug = path[0];
|
|
109
|
+
const restSegments = path.slice(1);
|
|
110
|
+
if (restSegments.some(
|
|
111
|
+
(s) => !s || s === "." || s === ".." || s.includes("/") || s.includes("\\") || s.includes("\0")
|
|
112
|
+
)) {
|
|
113
|
+
return new Response("Invalid path", { status: 400 });
|
|
114
|
+
}
|
|
115
|
+
const post = await ampless.getPublishedPost(slug, { siteId });
|
|
116
|
+
if (!post || post.format !== "static") {
|
|
117
|
+
return new Response("Not Found", { status: 404 });
|
|
118
|
+
}
|
|
119
|
+
const body = post.body ?? null;
|
|
120
|
+
const entrypoint = typeof body?.entrypoint === "string" && body.entrypoint ? body.entrypoint : "index.html";
|
|
121
|
+
const fileList = Array.isArray(body?.files) ? body.files : [];
|
|
122
|
+
let rest = restSegments.join("/");
|
|
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
|
+
}
|
|
132
|
+
if (fileList.length > 0 && !fileList.includes(rest)) {
|
|
133
|
+
return new Response("Not Found", { status: 404 });
|
|
134
|
+
}
|
|
135
|
+
const objectPath = `public/static/${siteId}/${slug}/${rest}`;
|
|
136
|
+
try {
|
|
137
|
+
const presignedUrl = await runWithAmplifyServerContext({
|
|
138
|
+
nextServerContext: { cookies },
|
|
139
|
+
operation: async (amplifyContext) => {
|
|
140
|
+
const result = await getUrl(amplifyContext, {
|
|
141
|
+
path: objectPath,
|
|
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
|
+
});
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
}
|
|
87
158
|
export {
|
|
88
159
|
createFeedRouteHandler,
|
|
89
160
|
createOgRouteHandler,
|
|
90
161
|
createRawRouteHandler,
|
|
91
|
-
createSitemapRouteHandler
|
|
162
|
+
createSitemapRouteHandler,
|
|
163
|
+
createStaticRouteHandler
|
|
92
164
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ampless/runtime",
|
|
3
|
-
"version": "0.2.0-alpha.
|
|
3
|
+
"version": "0.2.0-alpha.5",
|
|
4
4
|
"description": "Public-side runtime for ampless: post fetching, theme dispatch, middleware, public route handlers",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -48,8 +48,8 @@
|
|
|
48
48
|
"clsx": "^2.1.1",
|
|
49
49
|
"lucide-react": "^1.16.0",
|
|
50
50
|
"tailwind-merge": "^3.6.0",
|
|
51
|
-
"ampless": "0.2.0-alpha.
|
|
52
|
-
"@ampless/plugin-og-image": "0.2.0-alpha.
|
|
51
|
+
"ampless": "0.2.0-alpha.3",
|
|
52
|
+
"@ampless/plugin-og-image": "0.2.0-alpha.3"
|
|
53
53
|
},
|
|
54
54
|
"peerDependencies": {
|
|
55
55
|
"next": "^15 || ^16",
|