@ampless/runtime 1.0.0-alpha.17 → 1.0.0-alpha.19

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 CHANGED
@@ -88,8 +88,10 @@ middleware はリクエストごとに AppSync から `post.format` /
88
88
  キャッシュした上で、適切な内部ハンドラーにリクエストを書き換えます。
89
89
 
90
90
  - 通常の投稿 → 書き換えなし、`app/[slug]/page.tsx` で配信
91
- - `metadata.no_layout: true` HTML / `format: 'static'` → `/r/<slug>(/<path>)`、
92
- `app/r/[slug]/[[...path]]/route.ts` で配信
91
+ - `metadata.no_layout: true` HTML → `/raw/<slug>`、
92
+ `app/raw/[slug]/route.ts` で配信
93
+ - `format: 'static'` → `/static/<slug>(/<path>)`、
94
+ `app/static/[slug]/[[...path]]/route.ts` で配信
93
95
 
94
96
  また、`post.metadata.cache`(auto / deep / hot)+ `post.updatedAt` +
95
97
  `cms.config.cache.{cooldownMs, freshTtlSeconds, deepTtlSeconds}` から
@@ -100,7 +102,7 @@ middleware はリクエストごとに AppSync から `post.format` /
100
102
 
101
103
  - `@ampless/runtime` — `createAmpless`、ランタイム型、`renderBody`・`renderThemeCss`・フォーマットコンバーターの再エクスポート
102
104
  - `@ampless/runtime/middleware` — `createAmplessMiddleware`、`defaultMatcherConfig`
103
- - `@ampless/runtime/routes` — `createOgRouteHandler`、`createSitemapRouteHandler`、`createFeedRouteHandler`、`createUnderscoreRouteHandler`
105
+ - `@ampless/runtime/routes` — `createOgRouteHandler`、`createSitemapRouteHandler`、`createFeedRouteHandler`、`createRawRouteHandler`、`createStaticRouteHandler`
104
106
  - `@ampless/runtime/dispatchers` — `createThemeHomeDispatcher`、`createThemePostDispatcher`、`createThemeTagDispatcher`(それぞれ対応する `*Metadata` ファクトリーあり)
105
107
 
106
108
  ## テンプレートに残るもの
package/README.md CHANGED
@@ -88,8 +88,10 @@ from AppSync (apiKey auth) on each request, caches the result in a
88
88
  rewrites the request to the right internal handler:
89
89
 
90
90
  - themed post → no rewrite, served by `app/[slug]/page.tsx`
91
- - `metadata.no_layout: true` HTML / `format: 'static'` → `/r/<slug>(/<path>)`,
92
- served by `app/r/[slug]/[[...path]]/route.ts`
91
+ - `metadata.no_layout: true` HTML → `/raw/<slug>`, served by
92
+ `app/raw/[slug]/route.ts`
93
+ - `format: 'static'` → `/static/<slug>(/<path>)`, served by
94
+ `app/static/[slug]/[[...path]]/route.ts`
93
95
 
94
96
  It also computes `Cache-Control` from `post.metadata.cache` (auto /
95
97
  deep / hot) + `post.updatedAt` + `cms.config.cache.{cooldownMs,
@@ -100,7 +102,7 @@ See `docs/CONTENT.md` for the cache strategy contract.
100
102
 
101
103
  - `@ampless/runtime` — `createAmpless`, runtime types, and re-exports of `renderBody`, `renderThemeCss`, format converters
102
104
  - `@ampless/runtime/middleware` — `createAmplessMiddleware`, `defaultMatcherConfig`
103
- - `@ampless/runtime/routes` — `createOgRouteHandler`, `createSitemapRouteHandler`, `createFeedRouteHandler`, `createUnderscoreRouteHandler`
105
+ - `@ampless/runtime/routes` — `createOgRouteHandler`, `createSitemapRouteHandler`, `createFeedRouteHandler`, `createRawRouteHandler`, `createStaticRouteHandler`
104
106
  - `@ampless/runtime/dispatchers` — `createThemeHomeDispatcher`, `createThemePostDispatcher`, `createThemeTagDispatcher` (each with a matching `*Metadata` factory)
105
107
 
106
108
  ## What's still in the template
@@ -32,10 +32,10 @@ type ThemePostMetadata = (props: Props$1) => Promise<Metadata>;
32
32
  * in middleware. Middleware fetches `post.format` / `post.metadata` /
33
33
  * `post.updatedAt` from AppSync once per slug (Lambda-memory LRU, 60s
34
34
  * TTL) and rewrites the request to either this dispatcher (themed
35
- * render) or the unified route handler at `/r/<slug>(/...)` (no_layout
36
- * HTML or static bundle). That keeps the decision in one place and
37
- * avoids the dispatcher's extra AppSync round-trip when the post is a
38
- * themed render.
35
+ * render), `/raw/<slug>` (no_layout HTML), or `/static/<slug>(/...)`
36
+ * (static bundle). That keeps the decision in one place and avoids the
37
+ * dispatcher's extra AppSync round-trip when the post is a themed
38
+ * render.
39
39
  *
40
40
  * If the theme doesn't declare a Post component at all, returns
41
41
  * Next.js's notFound() (404).
@@ -27,10 +27,11 @@ declare function computeCacheControl(flags: {
27
27
  * - AppSync flag fetch (`format` / `metadata` / `updatedAt`) per
28
28
  * slug, with a 200-entry LRU keyed by slug, 60s TTL. Hot slugs
29
29
  * cost zero queries for the cache lifetime.
30
- * - Rewrite to `/r/<slug>(/<path>)` when the post is no_layout HTML
31
- * (`format=html` + `metadata.no_layout=true`) or a static bundle
32
- * (`format='static'`). Themed posts (default) get no rewrite
33
- * `app/[slug]/page.tsx` serves them directly.
30
+ * - Rewrite to `/raw/<slug>` when the post is no_layout HTML
31
+ * (`format=html` + `metadata.no_layout=true`).
32
+ * - Rewrite to `/static/<slug>(/<path>)` when the post is a static
33
+ * bundle (`format='static'`). Themed posts (default) get no
34
+ * rewrite — `app/[slug]/page.tsx` serves them directly.
34
35
  * - `Cache-Control` header computed from `post.metadata.cache` +
35
36
  * `post.updatedAt` + `cms.config.cache.*` and set on the response.
36
37
  * - `?previewTheme` / `?previewColorScheme` → `x-preview-theme` /
@@ -127,8 +127,9 @@ var RESERVED_PREFIXES = /* @__PURE__ */ new Set([
127
127
  "sitemap.xml",
128
128
  "og",
129
129
  "tag",
130
- // internal rewrite target — direct hits should not be middleware-driven
131
- "r"
130
+ // internal rewrite targets — direct hits should not be middleware-driven
131
+ "raw",
132
+ "static"
132
133
  ]);
133
134
  function createAmplessMiddleware(opts) {
134
135
  return async function middleware(request) {
@@ -161,12 +162,12 @@ function createAmplessMiddleware(opts) {
161
162
  let response;
162
163
  if (restPath.length === 0) {
163
164
  if (flags.format === "html" && flags.metadata?.no_layout === true) {
164
- url.pathname = `/r/${slug}`;
165
+ url.pathname = `/raw/${slug}`;
165
166
  response = NextResponse.rewrite(url, {
166
167
  request: { headers: requestHeaders }
167
168
  });
168
169
  } else if (flags.format === "static") {
169
- url.pathname = `/r/${slug}`;
170
+ url.pathname = `/static/${slug}`;
170
171
  response = NextResponse.rewrite(url, {
171
172
  request: { headers: requestHeaders }
172
173
  });
@@ -175,7 +176,7 @@ function createAmplessMiddleware(opts) {
175
176
  }
176
177
  } else {
177
178
  if (flags.format === "static") {
178
- url.pathname = `/r/${slug}/${restPath.join("/")}`;
179
+ url.pathname = `/static/${slug}/${restPath.join("/")}`;
179
180
  response = NextResponse.rewrite(url, {
180
181
  request: { headers: requestHeaders }
181
182
  });
@@ -2,18 +2,18 @@ import { Ampless } from '../index.js';
2
2
  import 'ampless';
3
3
  import 'next';
4
4
 
5
- interface Ctx$3 {
5
+ interface Ctx$4 {
6
6
  params: Promise<{
7
7
  slug: string;
8
8
  }>;
9
9
  }
10
- type OgRouteHandler = (req: Request, ctx: Ctx$3) => Promise<Response>;
10
+ type OgRouteHandler = (req: Request, ctx: Ctx$4) => Promise<Response>;
11
11
  declare function createOgRouteHandler(ampless: Ampless): OgRouteHandler;
12
12
 
13
- interface Ctx$2 {
13
+ interface Ctx$3 {
14
14
  params: Promise<Record<string, never>>;
15
15
  }
16
- type SitemapRouteHandler = (req: Request, ctx: Ctx$2) => Promise<Response>;
16
+ type SitemapRouteHandler = (req: Request, ctx: Ctx$3) => Promise<Response>;
17
17
  /**
18
18
  * Sitemap route delegate. Looks up the active theme and forwards to
19
19
  * whichever `routes.sitemap` handler the theme provides. Themes
@@ -21,10 +21,10 @@ type SitemapRouteHandler = (req: Request, ctx: Ctx$2) => Promise<Response>;
21
21
  */
22
22
  declare function createSitemapRouteHandler(ampless: Ampless): SitemapRouteHandler;
23
23
 
24
- interface Ctx$1 {
24
+ interface Ctx$2 {
25
25
  params: Promise<Record<string, never>>;
26
26
  }
27
- type FeedRouteHandler = (req: Request, ctx: Ctx$1) => Promise<Response>;
27
+ type FeedRouteHandler = (req: Request, ctx: Ctx$2) => Promise<Response>;
28
28
  /**
29
29
  * Feed route delegate. Looks up the active theme and forwards to
30
30
  * whichever `routes.feed` handler the theme provides. Themes without
@@ -32,55 +32,68 @@ type FeedRouteHandler = (req: Request, ctx: Ctx$1) => Promise<Response>;
32
32
  */
33
33
  declare function createFeedRouteHandler(ampless: Ampless): FeedRouteHandler;
34
34
 
35
- interface Ctx {
35
+ interface Ctx$1 {
36
36
  params: Promise<{
37
37
  slug: string;
38
- path?: string[];
39
38
  }>;
40
39
  }
41
- type UnderscoreRouteHandler = (req: Request, ctx: Ctx) => Promise<Response>;
40
+ type RawRouteHandler = (req: Request, ctx: Ctx$1) => Promise<Response>;
42
41
  /**
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.
42
+ * Internal route handler for `format: 'html'` posts whose
43
+ * `metadata.no_layout === true` the body is its own complete HTML
44
+ * document and ships as the entire response (no Next.js root layout,
45
+ * no theme chrome).
46
+ *
47
+ * Mounted at `app/raw/[slug]/route.ts`; reached via middleware rewrite
48
+ * of `/<slug>` → `/raw/<slug>`. Never hit directly — middleware adds
49
+ * `raw` to its reserved-prefix list so a user post with slug `raw`
50
+ * passes through to a 404 rather than reaching this handler with the
51
+ * wrong content.
47
52
  *
48
- * Two cases that both need to bypass the themed post page:
53
+ * Trust model: the body is emitted verbatim, same trust shape as
54
+ * regular `format: 'html'` posts on the themed route. See
55
+ * docs/architecture/04-access-layer-mcp.md §"editor の信頼モデル".
49
56
  *
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>/...`).
57
+ * Cache-Control: deliberately omitted from the response. Middleware
58
+ * computes the strategy from `post.metadata.cache` + `post.updatedAt`
59
+ * and sets the header on the rewritten response.
60
+ */
61
+ declare function createRawRouteHandler(ampless: Ampless): RawRouteHandler;
62
+
63
+ interface Ctx {
64
+ params: Promise<{
65
+ slug: string;
66
+ path?: string[];
67
+ }>;
68
+ }
69
+ type StaticRouteHandler = (req: Request, ctx: Ctx) => Promise<Response>;
70
+ /**
71
+ * Internal route handler for `format: 'static'` posts — the body is a
72
+ * manifest describing a bundle of files in S3 at
73
+ * `public/static/<slug>/`. The bundle entrypoint is served at
74
+ * `/<slug>/`, every internal file at `/<slug>/<relative-path>`.
58
75
  *
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.
76
+ * Mounted at `app/static/[slug]/[[...path]]/route.ts`; reached via
77
+ * middleware rewrite of `/<slug>(/<path>)` → `/static/<slug>(/<path>)`.
78
+ * Never hit directly middleware adds `static` to its reserved-prefix
79
+ * list so a user post with slug `static` passes through to a 404
80
+ * rather than landing here with the wrong content.
68
81
  *
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.
82
+ * Trailing-slash responsibility lives here: `/<slug>` (no trailing
83
+ * slash) 308-redirects to `/<slug>/` so relative asset paths inside
84
+ * the bundle resolve correctly — `<img src="img.png">` must resolve
85
+ * to `/<slug>/img.png`, not the site root.
73
86
  *
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 の信頼モデル".
87
+ * Trust model: assets are served via short-lived S3 presigned URLs;
88
+ * the bucket stays private.
79
89
  *
80
90
  * Cache-Control: deliberately omitted from the responses here.
81
91
  * Middleware computes the strategy from `metadata.cache` +
82
- * `post.updatedAt` and sets the header on the rewritten response.
92
+ * `post.updatedAt` and sets it on the rewritten response. The 302
93
+ * presigned redirects also rely on S3's own headers for the actual
94
+ * asset bytes — adding a stale-while-revalidate window on the 302
95
+ * would risk serving an expired presign to repeat visitors.
83
96
  */
84
- declare function createUnderscoreRouteHandler(ampless: Ampless): UnderscoreRouteHandler;
97
+ declare function createStaticRouteHandler(ampless: Ampless): StaticRouteHandler;
85
98
 
86
- export { type FeedRouteHandler, type OgRouteHandler, type SitemapRouteHandler, type UnderscoreRouteHandler, createFeedRouteHandler, createOgRouteHandler, createSitemapRouteHandler, createUnderscoreRouteHandler };
99
+ export { type FeedRouteHandler, type OgRouteHandler, type RawRouteHandler, type SitemapRouteHandler, type StaticRouteHandler, createFeedRouteHandler, createOgRouteHandler, createRawRouteHandler, createSitemapRouteHandler, createStaticRouteHandler };
@@ -65,11 +65,32 @@ function createFeedRouteHandler(ampless) {
65
65
  };
66
66
  }
67
67
 
68
- // src/routes/underscore.ts
68
+ // src/routes/raw.ts
69
+ function createRawRouteHandler(ampless) {
70
+ return async function GET(_request, { params }) {
71
+ const { slug } = await params;
72
+ const post = await ampless.getPublishedPost(slug);
73
+ if (!post) {
74
+ return new Response("Not Found", { status: 404 });
75
+ }
76
+ if (post.format !== "html" || post.metadata?.no_layout !== true) {
77
+ return new Response("Not Found", { status: 404 });
78
+ }
79
+ return new Response(ampless.renderBody(post), {
80
+ status: 200,
81
+ headers: {
82
+ "Content-Type": "text/html; charset=utf-8"
83
+ // Cache-Control set by middleware.
84
+ }
85
+ });
86
+ };
87
+ }
88
+
89
+ // src/routes/static.ts
69
90
  import { createServerRunner } from "@aws-amplify/adapter-nextjs";
70
91
  import { cookies } from "next/headers";
71
92
  import { getUrl } from "aws-amplify/storage/server";
72
- function createUnderscoreRouteHandler(ampless) {
93
+ function createStaticRouteHandler(ampless) {
73
94
  const { runWithAmplifyServerContext } = createServerRunner({
74
95
  config: ampless.outputs
75
96
  });
@@ -80,40 +101,27 @@ function createUnderscoreRouteHandler(ampless) {
80
101
  if (!post) {
81
102
  return new Response("Not Found", { status: 404 });
82
103
  }
104
+ if (post.format !== "static") {
105
+ return new Response("Not Found", { status: 404 });
106
+ }
83
107
  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
- });
108
+ const url = new URL(request.url);
109
+ if (!url.pathname.endsWith("/")) {
110
+ const next = new URL(url);
111
+ next.pathname = `${url.pathname}/`;
112
+ return Response.redirect(next.toString(), 308);
93
113
  }
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);
114
+ const body2 = post.body ?? null;
115
+ const entrypoint = typeof body2?.entrypoint === "string" && body2.entrypoint ? body2.entrypoint : "index.html";
116
+ const presignedUrl2 = await signStaticAsset({
117
+ runWithAmplifyServerContext,
118
+ slug,
119
+ rest: entrypoint
120
+ });
121
+ if (!presignedUrl2) {
122
+ return new Response("Not Found", { status: 404 });
112
123
  }
113
- return new Response("Not Found", { status: 404 });
114
- }
115
- if (post.format !== "static") {
116
- return new Response("Not Found", { status: 404 });
124
+ return Response.redirect(presignedUrl2, 302);
117
125
  }
118
126
  if (restSegments.some(
119
127
  (s) => !s || s === "." || s === ".." || s.includes("/") || s.includes("\\") || s.includes("\0")
@@ -156,7 +164,7 @@ async function signStaticAsset({
156
164
  });
157
165
  } catch (err) {
158
166
  console.error(
159
- `[underscore-route] presign failed for ${objectPath}:`,
167
+ `[static-route] presign failed for ${objectPath}:`,
160
168
  err
161
169
  );
162
170
  return null;
@@ -165,6 +173,7 @@ async function signStaticAsset({
165
173
  export {
166
174
  createFeedRouteHandler,
167
175
  createOgRouteHandler,
176
+ createRawRouteHandler,
168
177
  createSitemapRouteHandler,
169
- createUnderscoreRouteHandler
178
+ createStaticRouteHandler
170
179
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ampless/runtime",
3
- "version": "1.0.0-alpha.17",
3
+ "version": "1.0.0-alpha.19",
4
4
  "description": "Public-side runtime for ampless: post fetching, theme dispatch, middleware, public route handlers",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -49,8 +49,8 @@
49
49
  "lucide-react": "^1.16.0",
50
50
  "marked": "^18.0.4",
51
51
  "tailwind-merge": "^3.6.0",
52
- "ampless": "1.0.0-alpha.12",
53
- "@ampless/plugin-og-image": "0.2.0-alpha.12"
52
+ "ampless": "1.0.0-alpha.14",
53
+ "@ampless/plugin-og-image": "0.2.0-alpha.14"
54
54
  },
55
55
  "peerDependencies": {
56
56
  "next": "^15 || ^16",