@ampless/runtime 0.2.0-alpha.9 → 1.0.0-alpha.14
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 +35 -9
- package/README.md +35 -9
- package/dist/dispatchers/index.d.ts +14 -23
- package/dist/dispatchers/index.js +7 -22
- package/dist/index.d.ts +35 -26
- package/dist/index.js +176 -96
- package/dist/middleware.d.ts +33 -21
- package/dist/middleware.js +181 -19
- package/dist/routes/index.d.ts +53 -80
- package/dist/routes/index.js +85 -79
- package/package.json +4 -3
package/dist/middleware.js
CHANGED
|
@@ -1,30 +1,189 @@
|
|
|
1
1
|
// src/middleware.ts
|
|
2
2
|
import { NextResponse } from "next/server";
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
3
|
+
var runtime = "nodejs";
|
|
4
|
+
var FLAG_CACHE_MAX = 200;
|
|
5
|
+
var FLAG_CACHE_TTL_MS = 6e4;
|
|
6
|
+
var FLAG_CACHE = /* @__PURE__ */ new Map();
|
|
7
|
+
function cacheGet(slug) {
|
|
8
|
+
const hit = FLAG_CACHE.get(slug);
|
|
9
|
+
if (!hit) return void 0;
|
|
10
|
+
if (hit.expires < Date.now()) {
|
|
11
|
+
FLAG_CACHE.delete(slug);
|
|
12
|
+
return void 0;
|
|
13
|
+
}
|
|
14
|
+
FLAG_CACHE.delete(slug);
|
|
15
|
+
FLAG_CACHE.set(slug, hit);
|
|
16
|
+
return hit.value;
|
|
17
|
+
}
|
|
18
|
+
function cacheSet(slug, value) {
|
|
19
|
+
if (FLAG_CACHE.size >= FLAG_CACHE_MAX) {
|
|
20
|
+
const oldest = FLAG_CACHE.keys().next().value;
|
|
21
|
+
if (oldest !== void 0) FLAG_CACHE.delete(oldest);
|
|
22
|
+
}
|
|
23
|
+
FLAG_CACHE.set(slug, { value, expires: Date.now() + FLAG_CACHE_TTL_MS });
|
|
24
|
+
}
|
|
25
|
+
function _resetFlagCache() {
|
|
26
|
+
FLAG_CACHE.clear();
|
|
27
|
+
}
|
|
28
|
+
async function fetchFlags(opts, slug) {
|
|
29
|
+
const query = `query MiddlewareFlags($slug: String!) {
|
|
30
|
+
getPublishedPost(slug: $slug) { format metadata updatedAt }
|
|
31
|
+
}`;
|
|
32
|
+
let res;
|
|
33
|
+
try {
|
|
34
|
+
res = await fetch(opts.appsyncUrl, {
|
|
35
|
+
method: "POST",
|
|
36
|
+
headers: {
|
|
37
|
+
"Content-Type": "application/json",
|
|
38
|
+
"x-api-key": opts.apiKey
|
|
39
|
+
},
|
|
40
|
+
body: JSON.stringify({ query, variables: { slug } })
|
|
41
|
+
});
|
|
42
|
+
} catch (err) {
|
|
43
|
+
console.error("[ampless-middleware] AppSync fetch failed", err);
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
if (!res.ok) {
|
|
47
|
+
console.error(
|
|
48
|
+
`[ampless-middleware] AppSync returned ${res.status} for slug=${slug}`
|
|
49
|
+
);
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
let body;
|
|
53
|
+
try {
|
|
54
|
+
body = await res.json();
|
|
55
|
+
} catch (err) {
|
|
56
|
+
console.error("[ampless-middleware] AppSync JSON parse failed", err);
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
if (body.errors && body.errors.length > 0) {
|
|
60
|
+
console.error(
|
|
61
|
+
"[ampless-middleware] AppSync errors",
|
|
62
|
+
body.errors.map((e) => e.message)
|
|
63
|
+
);
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
const post = body.data?.getPublishedPost;
|
|
67
|
+
if (!post) return null;
|
|
68
|
+
let metadata = null;
|
|
69
|
+
if (post.metadata) {
|
|
70
|
+
try {
|
|
71
|
+
const parsed = typeof post.metadata === "string" ? JSON.parse(post.metadata) : post.metadata;
|
|
72
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
73
|
+
metadata = parsed;
|
|
74
|
+
}
|
|
75
|
+
} catch (err) {
|
|
76
|
+
console.error(
|
|
77
|
+
`[ampless-middleware] metadata parse failed for slug=${slug}`,
|
|
78
|
+
err
|
|
79
|
+
);
|
|
80
|
+
metadata = null;
|
|
11
81
|
}
|
|
82
|
+
}
|
|
83
|
+
return {
|
|
84
|
+
format: post.format ?? "markdown",
|
|
85
|
+
metadata,
|
|
86
|
+
updatedAt: post.updatedAt ?? ""
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
async function getCachedFlags(opts, slug) {
|
|
90
|
+
const cached = cacheGet(slug);
|
|
91
|
+
if (cached !== void 0) return cached;
|
|
92
|
+
const fresh = await fetchFlags(opts, slug);
|
|
93
|
+
cacheSet(slug, fresh);
|
|
94
|
+
return fresh;
|
|
95
|
+
}
|
|
96
|
+
var DEFAULT_COOLDOWN_MS = 60 * 60 * 1e3;
|
|
97
|
+
var DEFAULT_FRESH_TTL_SECONDS = 300;
|
|
98
|
+
var DEFAULT_DEEP_TTL_SECONDS = 60 * 60;
|
|
99
|
+
var NO_STORE = "public, max-age=0, must-revalidate, s-maxage=0";
|
|
100
|
+
function computeCacheControl(flags, cmsConfig) {
|
|
101
|
+
const cache = cmsConfig.cache ?? {};
|
|
102
|
+
const strategy = flags.metadata?.cache ?? "auto";
|
|
103
|
+
if (strategy === "hot") return NO_STORE;
|
|
104
|
+
if (strategy === "deep") {
|
|
105
|
+
const ttl = cache.deepTtlSeconds ?? DEFAULT_DEEP_TTL_SECONDS;
|
|
106
|
+
return `public, max-age=${ttl}, s-maxage=${ttl}`;
|
|
107
|
+
}
|
|
108
|
+
const cooldownMs = cache.cooldownMs ?? DEFAULT_COOLDOWN_MS;
|
|
109
|
+
const freshTtl = cache.freshTtlSeconds ?? DEFAULT_FRESH_TTL_SECONDS;
|
|
110
|
+
const updatedMs = flags.updatedAt ? Date.parse(flags.updatedAt) : NaN;
|
|
111
|
+
if (Number.isFinite(updatedMs)) {
|
|
112
|
+
const ageMs = Date.now() - updatedMs;
|
|
113
|
+
if (ageMs < cooldownMs) return NO_STORE;
|
|
114
|
+
}
|
|
115
|
+
return `public, max-age=${freshTtl}, s-maxage=${freshTtl}`;
|
|
116
|
+
}
|
|
117
|
+
var RESERVED_PREFIXES = /* @__PURE__ */ new Set([
|
|
118
|
+
"admin",
|
|
119
|
+
"api",
|
|
120
|
+
"login",
|
|
121
|
+
"_next",
|
|
122
|
+
"favicon.ico",
|
|
123
|
+
"robots.txt",
|
|
124
|
+
"amplify_outputs.json",
|
|
125
|
+
// route handler folders that live alongside `app/[slug]/page.tsx`
|
|
126
|
+
"feed.xml",
|
|
127
|
+
"sitemap.xml",
|
|
128
|
+
"og",
|
|
129
|
+
"tag",
|
|
130
|
+
// internal rewrite target — direct hits should not be middleware-driven
|
|
131
|
+
"r"
|
|
132
|
+
]);
|
|
133
|
+
function createAmplessMiddleware(opts) {
|
|
134
|
+
return async function middleware(request) {
|
|
12
135
|
const url = request.nextUrl.clone();
|
|
13
|
-
if (!url.pathname.startsWith("/site/")) {
|
|
14
|
-
const tail = url.pathname === "/" ? "" : url.pathname;
|
|
15
|
-
url.pathname = `/site/${siteId}${tail}`;
|
|
16
|
-
}
|
|
17
136
|
const previewTheme = url.searchParams.get("previewTheme");
|
|
18
137
|
const previewColorScheme = url.searchParams.get("previewColorScheme");
|
|
19
138
|
const requestHeaders = new Headers(request.headers);
|
|
20
|
-
requestHeaders.set("x-site-id", siteId);
|
|
21
139
|
if (previewTheme) requestHeaders.set("x-preview-theme", previewTheme);
|
|
22
|
-
if (previewColorScheme)
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
140
|
+
if (previewColorScheme) {
|
|
141
|
+
requestHeaders.set("x-preview-color-scheme", previewColorScheme);
|
|
142
|
+
}
|
|
143
|
+
const passthrough = () => NextResponse.next({ request: { headers: requestHeaders } });
|
|
144
|
+
const segments = url.pathname.split("/").filter(Boolean);
|
|
145
|
+
if (segments.length === 0) {
|
|
146
|
+
return passthrough();
|
|
147
|
+
}
|
|
148
|
+
const slug = segments[0];
|
|
149
|
+
if (RESERVED_PREFIXES.has(slug)) {
|
|
150
|
+
return passthrough();
|
|
151
|
+
}
|
|
152
|
+
const restPath = segments.slice(1);
|
|
153
|
+
const flags = await getCachedFlags(opts, slug);
|
|
154
|
+
if (!flags) {
|
|
155
|
+
if (restPath.length > 0) {
|
|
156
|
+
return new NextResponse("Not Found", { status: 404 });
|
|
157
|
+
}
|
|
158
|
+
return passthrough();
|
|
159
|
+
}
|
|
160
|
+
const cacheControl = computeCacheControl(flags, opts.cmsConfig);
|
|
161
|
+
let response;
|
|
162
|
+
if (restPath.length === 0) {
|
|
163
|
+
if (flags.format === "html" && flags.metadata?.no_layout === true) {
|
|
164
|
+
url.pathname = `/r/${slug}`;
|
|
165
|
+
response = NextResponse.rewrite(url, {
|
|
166
|
+
request: { headers: requestHeaders }
|
|
167
|
+
});
|
|
168
|
+
} else if (flags.format === "static") {
|
|
169
|
+
url.pathname = `/r/${slug}`;
|
|
170
|
+
response = NextResponse.rewrite(url, {
|
|
171
|
+
request: { headers: requestHeaders }
|
|
172
|
+
});
|
|
173
|
+
} else {
|
|
174
|
+
response = passthrough();
|
|
175
|
+
}
|
|
176
|
+
} else {
|
|
177
|
+
if (flags.format === "static") {
|
|
178
|
+
url.pathname = `/r/${slug}/${restPath.join("/")}`;
|
|
179
|
+
response = NextResponse.rewrite(url, {
|
|
180
|
+
request: { headers: requestHeaders }
|
|
181
|
+
});
|
|
182
|
+
} else {
|
|
183
|
+
return new NextResponse("Not Found", { status: 404 });
|
|
184
|
+
}
|
|
27
185
|
}
|
|
186
|
+
response.headers.set("Cache-Control", cacheControl);
|
|
28
187
|
return response;
|
|
29
188
|
};
|
|
30
189
|
}
|
|
@@ -34,6 +193,9 @@ var defaultMatcherConfig = {
|
|
|
34
193
|
]
|
|
35
194
|
};
|
|
36
195
|
export {
|
|
196
|
+
_resetFlagCache,
|
|
197
|
+
computeCacheControl,
|
|
37
198
|
createAmplessMiddleware,
|
|
38
|
-
defaultMatcherConfig
|
|
199
|
+
defaultMatcherConfig,
|
|
200
|
+
runtime
|
|
39
201
|
};
|
package/dist/routes/index.d.ts
CHANGED
|
@@ -2,112 +2,85 @@ 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
|
-
siteId: string;
|
|
8
7
|
slug: string;
|
|
9
8
|
}>;
|
|
10
9
|
}
|
|
11
|
-
type OgRouteHandler = (req: Request, ctx: Ctx$
|
|
10
|
+
type OgRouteHandler = (req: Request, ctx: Ctx$3) => Promise<Response>;
|
|
12
11
|
declare function createOgRouteHandler(ampless: Ampless): OgRouteHandler;
|
|
13
12
|
|
|
14
|
-
interface Ctx$
|
|
15
|
-
params: Promise<
|
|
16
|
-
siteId: string;
|
|
17
|
-
}>;
|
|
13
|
+
interface Ctx$2 {
|
|
14
|
+
params: Promise<Record<string, never>>;
|
|
18
15
|
}
|
|
19
|
-
type SitemapRouteHandler = (req: Request, ctx: Ctx$
|
|
16
|
+
type SitemapRouteHandler = (req: Request, ctx: Ctx$2) => Promise<Response>;
|
|
20
17
|
/**
|
|
21
|
-
* Sitemap route delegate. Looks up the active theme
|
|
22
|
-
*
|
|
23
|
-
*
|
|
18
|
+
* Sitemap route delegate. Looks up the active theme and forwards to
|
|
19
|
+
* whichever `routes.sitemap` handler the theme provides. Themes
|
|
20
|
+
* without a sitemap handler return 404.
|
|
24
21
|
*/
|
|
25
22
|
declare function createSitemapRouteHandler(ampless: Ampless): SitemapRouteHandler;
|
|
26
23
|
|
|
27
|
-
interface Ctx$
|
|
28
|
-
params: Promise<
|
|
29
|
-
siteId: string;
|
|
30
|
-
}>;
|
|
24
|
+
interface Ctx$1 {
|
|
25
|
+
params: Promise<Record<string, never>>;
|
|
31
26
|
}
|
|
32
|
-
type FeedRouteHandler = (req: Request, ctx: Ctx$
|
|
27
|
+
type FeedRouteHandler = (req: Request, ctx: Ctx$1) => Promise<Response>;
|
|
33
28
|
/**
|
|
34
|
-
* Feed route delegate. Looks up the active theme
|
|
35
|
-
*
|
|
36
|
-
*
|
|
29
|
+
* Feed route delegate. Looks up the active theme and forwards to
|
|
30
|
+
* whichever `routes.feed` handler the theme provides. Themes without
|
|
31
|
+
* a feed handler return 404.
|
|
37
32
|
*/
|
|
38
33
|
declare function createFeedRouteHandler(ampless: Ampless): FeedRouteHandler;
|
|
39
34
|
|
|
40
|
-
interface Ctx
|
|
35
|
+
interface Ctx {
|
|
41
36
|
params: Promise<{
|
|
42
|
-
siteId: string;
|
|
43
37
|
slug: string;
|
|
38
|
+
path?: string[];
|
|
44
39
|
}>;
|
|
45
40
|
}
|
|
46
|
-
type
|
|
41
|
+
type UnderscoreRouteHandler = (req: Request, ctx: Ctx) => Promise<Response>;
|
|
47
42
|
/**
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
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).
|
|
43
|
+
* Unified internal route handler for no_layout HTML and static bundle
|
|
44
|
+
* posts. Mounted at `app/r/[slug]/[[...path]]/route.ts`; reached via
|
|
45
|
+
* middleware rewrite from the public `/<slug>(/<path>)` URL, never
|
|
46
|
+
* directly.
|
|
58
47
|
*
|
|
59
|
-
*
|
|
60
|
-
* always emits `<html>` / `<head>` / `<body>`, which means a normal
|
|
61
|
-
* page can't replace the document. A `route.ts` returns a Response
|
|
62
|
-
* directly and bypasses React rendering entirely.
|
|
48
|
+
* Two cases that both need to bypass the themed post page:
|
|
63
49
|
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
50
|
+
* 1. `format: 'html'` posts with `metadata.no_layout === true` — the
|
|
51
|
+
* body is its own complete HTML document and ships as the entire
|
|
52
|
+
* response. Reached via middleware rewrite of `/<slug>`.
|
|
53
|
+
* 2. `format: 'static'` posts — the body is a manifest describing a
|
|
54
|
+
* bundle of files in S3 at `public/static/<slug>/`. The bundle's
|
|
55
|
+
* entrypoint is served at `/<slug>/`, every internal file at
|
|
56
|
+
* `/<slug>/<relative-path>` (middleware rewrites both to
|
|
57
|
+
* `/r/<slug>/...`).
|
|
72
58
|
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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).
|
|
59
|
+
* Routing model:
|
|
60
|
+
* - File location: `app/r/[slug]/[[...path]]/route.ts`. The literal
|
|
61
|
+
* folder is `r/` (not `_/` etc.) because Next.js's App Router
|
|
62
|
+
* skips any path part starting with `_` during route discovery
|
|
63
|
+
* (see `recursive-readdir` + `ignorePartFilter` in
|
|
64
|
+
* next/dist/build/route-discovery.js). The `r/` prefix is an
|
|
65
|
+
* internal filesystem detail — the public URL stays `/<slug>(/...)`.
|
|
66
|
+
* - `params.path` is `undefined` (or `[]`) for single-segment
|
|
67
|
+
* requests, an array of remaining segments otherwise.
|
|
93
68
|
*
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
*
|
|
98
|
-
* so the catch-all picks up from there.
|
|
69
|
+
* Trailing-slash responsibility lives here. For static bundles the
|
|
70
|
+
* handler 308-redirects `/<slug>` to `/<slug>/` so relative asset
|
|
71
|
+
* paths inside the bundle resolve correctly — `<img src="img.png">`
|
|
72
|
+
* must resolve to `/<slug>/img.png`, not the site root.
|
|
99
73
|
*
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
*
|
|
74
|
+
* Trust model: no_layout HTML bodies are emitted verbatim, same trust
|
|
75
|
+
* shape as `format: 'html'` post bodies on the regular path. Static
|
|
76
|
+
* bundle assets are served via short-lived S3 presigned URLs; the
|
|
77
|
+
* bucket stays private. See docs/architecture/04-access-layer-mcp.md
|
|
78
|
+
* §"editor の信頼モデル".
|
|
103
79
|
*
|
|
104
|
-
*
|
|
105
|
-
*
|
|
106
|
-
*
|
|
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.
|
|
80
|
+
* Cache-Control: deliberately omitted from the responses here.
|
|
81
|
+
* Middleware computes the strategy from `metadata.cache` +
|
|
82
|
+
* `post.updatedAt` and sets the header on the rewritten response.
|
|
110
83
|
*/
|
|
111
|
-
declare function
|
|
84
|
+
declare function createUnderscoreRouteHandler(ampless: Ampless): UnderscoreRouteHandler;
|
|
112
85
|
|
|
113
|
-
export { type FeedRouteHandler, type OgRouteHandler, type
|
|
86
|
+
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,128 @@ 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 set by middleware (cache strategy depends on
|
|
90
|
+
// post.metadata.cache + post.updatedAt).
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
if (post.format === "static") {
|
|
95
|
+
const url = new URL(request.url);
|
|
96
|
+
if (!url.pathname.endsWith("/")) {
|
|
97
|
+
const next = new URL(url);
|
|
98
|
+
next.pathname = `${url.pathname}/`;
|
|
99
|
+
return Response.redirect(next.toString(), 308);
|
|
100
|
+
}
|
|
101
|
+
const body2 = post.body ?? null;
|
|
102
|
+
const entrypoint = typeof body2?.entrypoint === "string" && body2.entrypoint ? body2.entrypoint : "index.html";
|
|
103
|
+
const presignedUrl2 = await signStaticAsset({
|
|
104
|
+
runWithAmplifyServerContext,
|
|
105
|
+
slug,
|
|
106
|
+
rest: entrypoint
|
|
107
|
+
});
|
|
108
|
+
if (!presignedUrl2) {
|
|
109
|
+
return new Response("Not Found", { status: 404 });
|
|
110
|
+
}
|
|
111
|
+
return Response.redirect(presignedUrl2, 302);
|
|
112
|
+
}
|
|
113
|
+
return new Response("Not Found", { status: 404 });
|
|
114
|
+
}
|
|
115
|
+
if (post.format !== "static") {
|
|
106
116
|
return new Response("Not Found", { status: 404 });
|
|
107
117
|
}
|
|
108
|
-
const slug = path[0];
|
|
109
|
-
const restSegments = path.slice(1);
|
|
110
118
|
if (restSegments.some(
|
|
111
119
|
(s) => !s || s === "." || s === ".." || s.includes("/") || s.includes("\\") || s.includes("\0")
|
|
112
120
|
)) {
|
|
113
121
|
return new Response("Invalid path", { status: 400 });
|
|
114
122
|
}
|
|
115
|
-
const post = await ampless.getPublishedPost(slug, { siteId });
|
|
116
|
-
if (!post || post.format !== "static") {
|
|
117
|
-
return new Response("Not Found", { status: 404 });
|
|
118
|
-
}
|
|
119
123
|
const body = post.body ?? null;
|
|
120
|
-
const entrypoint = typeof body?.entrypoint === "string" && body.entrypoint ? body.entrypoint : "index.html";
|
|
121
124
|
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
|
-
}
|
|
125
|
+
const rest = restSegments.join("/");
|
|
132
126
|
if (fileList.length > 0 && !fileList.includes(rest)) {
|
|
133
127
|
return new Response("Not Found", { status: 404 });
|
|
134
128
|
}
|
|
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
|
-
});
|
|
129
|
+
const presignedUrl = await signStaticAsset({
|
|
130
|
+
runWithAmplifyServerContext,
|
|
131
|
+
slug,
|
|
132
|
+
rest
|
|
133
|
+
});
|
|
134
|
+
if (!presignedUrl) {
|
|
135
|
+
return new Response("Not Found", { status: 404 });
|
|
155
136
|
}
|
|
137
|
+
return Response.redirect(presignedUrl, 302);
|
|
156
138
|
};
|
|
157
139
|
}
|
|
140
|
+
async function signStaticAsset({
|
|
141
|
+
runWithAmplifyServerContext,
|
|
142
|
+
slug,
|
|
143
|
+
rest
|
|
144
|
+
}) {
|
|
145
|
+
const objectPath = `public/static/${slug}/${rest}`;
|
|
146
|
+
try {
|
|
147
|
+
return await runWithAmplifyServerContext({
|
|
148
|
+
nextServerContext: { cookies },
|
|
149
|
+
operation: async (amplifyContext) => {
|
|
150
|
+
const result = await getUrl(amplifyContext, {
|
|
151
|
+
path: objectPath,
|
|
152
|
+
options: { expiresIn: 60 * 60 }
|
|
153
|
+
});
|
|
154
|
+
return result.url.toString();
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
} catch (err) {
|
|
158
|
+
console.error(
|
|
159
|
+
`[underscore-route] presign failed for ${objectPath}:`,
|
|
160
|
+
err
|
|
161
|
+
);
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
158
165
|
export {
|
|
159
166
|
createFeedRouteHandler,
|
|
160
167
|
createOgRouteHandler,
|
|
161
|
-
createRawRouteHandler,
|
|
162
168
|
createSitemapRouteHandler,
|
|
163
|
-
|
|
169
|
+
createUnderscoreRouteHandler
|
|
164
170
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ampless/runtime",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.0-alpha.14",
|
|
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.10",
|
|
53
|
+
"@ampless/plugin-og-image": "0.2.0-alpha.10"
|
|
53
54
|
},
|
|
54
55
|
"peerDependencies": {
|
|
55
56
|
"next": "^15 || ^16",
|