@ampless/runtime 1.0.0-alpha.13 → 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 +34 -8
- package/README.md +34 -8
- package/dist/dispatchers/index.d.ts +11 -30
- package/dist/dispatchers/index.js +1 -6
- package/dist/index.d.ts +6 -0
- package/dist/index.js +2 -1
- package/dist/middleware.d.ts +33 -23
- package/dist/middleware.js +183 -12
- package/dist/routes/index.d.ts +26 -27
- package/dist/routes/index.js +3 -2
- package/package.json +3 -3
package/README.ja.md
CHANGED
|
@@ -34,10 +34,10 @@ export const ampless = createAmpless({
|
|
|
34
34
|
})
|
|
35
35
|
```
|
|
36
36
|
|
|
37
|
-
`app
|
|
37
|
+
`app/` 直下のルートとディスパッチャーはワンライナーのファクトリー呼び出しになります:
|
|
38
38
|
|
|
39
39
|
```ts
|
|
40
|
-
// app/
|
|
40
|
+
// app/page.tsx
|
|
41
41
|
import { ampless } from '@/lib/ampless'
|
|
42
42
|
import {
|
|
43
43
|
createThemeHomeDispatcher,
|
|
@@ -50,7 +50,7 @@ export default createThemeHomeDispatcher(ampless)
|
|
|
50
50
|
```
|
|
51
51
|
|
|
52
52
|
```ts
|
|
53
|
-
// app/
|
|
53
|
+
// app/og/[slug]/route.ts
|
|
54
54
|
import { ampless } from '@/lib/ampless'
|
|
55
55
|
import { createOgRouteHandler } from '@ampless/runtime/routes'
|
|
56
56
|
|
|
@@ -59,17 +59,43 @@ export const dynamic = 'force-dynamic'
|
|
|
59
59
|
export const GET = createOgRouteHandler(ampless)
|
|
60
60
|
```
|
|
61
61
|
|
|
62
|
-
|
|
62
|
+
ミドルウェア(proxy):
|
|
63
63
|
|
|
64
64
|
```ts
|
|
65
|
-
// middleware.ts
|
|
65
|
+
// proxy.ts (Next.js 16 で middleware.ts から改名)
|
|
66
66
|
import cmsConfig from './cms.config'
|
|
67
|
-
import
|
|
67
|
+
import outputs from './amplify_outputs.json'
|
|
68
|
+
import { createAmplessMiddleware } from '@ampless/runtime/middleware'
|
|
68
69
|
|
|
69
|
-
export const
|
|
70
|
-
|
|
70
|
+
export const proxy = createAmplessMiddleware({
|
|
71
|
+
cmsConfig,
|
|
72
|
+
appsyncUrl: outputs.data.url,
|
|
73
|
+
apiKey: outputs.data.api_key!,
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
// matcher はインライン定義 — Next.js 16 の Turbopack は `export const config`
|
|
77
|
+
// が静的オブジェクトリテラルであることを要求します。
|
|
78
|
+
export const config = {
|
|
79
|
+
matcher: [
|
|
80
|
+
'/((?!admin|api|login|_next/static|_next/image|favicon\\.ico|amplify_outputs\\.json).*)',
|
|
81
|
+
],
|
|
82
|
+
}
|
|
71
83
|
```
|
|
72
84
|
|
|
85
|
+
middleware はリクエストごとに AppSync から `post.format` /
|
|
86
|
+
`post.metadata` / `post.updatedAt` を取得(apiKey 認証)し、slug をキー
|
|
87
|
+
にした 200 エントリの LRU(60 秒 TTL、Lambda モジュールスコープ)で
|
|
88
|
+
キャッシュした上で、適切な内部ハンドラーにリクエストを書き換えます。
|
|
89
|
+
|
|
90
|
+
- 通常の投稿 → 書き換えなし、`app/[slug]/page.tsx` で配信
|
|
91
|
+
- `metadata.no_layout: true` HTML / `format: 'static'` → `/r/<slug>(/<path>)`、
|
|
92
|
+
`app/r/[slug]/[[...path]]/route.ts` で配信
|
|
93
|
+
|
|
94
|
+
また、`post.metadata.cache`(auto / deep / hot)+ `post.updatedAt` +
|
|
95
|
+
`cms.config.cache.{cooldownMs, freshTtlSeconds, deepTtlSeconds}` から
|
|
96
|
+
`Cache-Control` を算出してレスポンスに付与します。キャッシュ戦略の
|
|
97
|
+
契約については `docs/CONTENT.md` を参照してください。
|
|
98
|
+
|
|
73
99
|
## サブパス
|
|
74
100
|
|
|
75
101
|
- `@ampless/runtime` — `createAmpless`、ランタイム型、`renderBody`・`renderThemeCss`・フォーマットコンバーターの再エクスポート
|
package/README.md
CHANGED
|
@@ -34,10 +34,10 @@ export const ampless = createAmpless({
|
|
|
34
34
|
})
|
|
35
35
|
```
|
|
36
36
|
|
|
37
|
-
Routes and dispatchers under `app
|
|
37
|
+
Routes and dispatchers under `app/` become one-line factory invocations:
|
|
38
38
|
|
|
39
39
|
```ts
|
|
40
|
-
// app/
|
|
40
|
+
// app/page.tsx
|
|
41
41
|
import { ampless } from '@/lib/ampless'
|
|
42
42
|
import {
|
|
43
43
|
createThemeHomeDispatcher,
|
|
@@ -50,7 +50,7 @@ export default createThemeHomeDispatcher(ampless)
|
|
|
50
50
|
```
|
|
51
51
|
|
|
52
52
|
```ts
|
|
53
|
-
// app/
|
|
53
|
+
// app/og/[slug]/route.ts
|
|
54
54
|
import { ampless } from '@/lib/ampless'
|
|
55
55
|
import { createOgRouteHandler } from '@ampless/runtime/routes'
|
|
56
56
|
|
|
@@ -59,17 +59,43 @@ export const dynamic = 'force-dynamic'
|
|
|
59
59
|
export const GET = createOgRouteHandler(ampless)
|
|
60
60
|
```
|
|
61
61
|
|
|
62
|
-
Middleware:
|
|
62
|
+
Middleware (proxy):
|
|
63
63
|
|
|
64
64
|
```ts
|
|
65
|
-
// middleware.ts
|
|
65
|
+
// proxy.ts (Next.js 16 rename of middleware.ts)
|
|
66
66
|
import cmsConfig from './cms.config'
|
|
67
|
-
import
|
|
67
|
+
import outputs from './amplify_outputs.json'
|
|
68
|
+
import { createAmplessMiddleware } from '@ampless/runtime/middleware'
|
|
68
69
|
|
|
69
|
-
export const
|
|
70
|
-
|
|
70
|
+
export const proxy = createAmplessMiddleware({
|
|
71
|
+
cmsConfig,
|
|
72
|
+
appsyncUrl: outputs.data.url,
|
|
73
|
+
apiKey: outputs.data.api_key!,
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
// Inline the matcher — Next.js 16's Turbopack requires
|
|
77
|
+
// `export const config` to be a static object literal.
|
|
78
|
+
export const config = {
|
|
79
|
+
matcher: [
|
|
80
|
+
'/((?!admin|api|login|_next/static|_next/image|favicon\\.ico|amplify_outputs\\.json).*)',
|
|
81
|
+
],
|
|
82
|
+
}
|
|
71
83
|
```
|
|
72
84
|
|
|
85
|
+
The middleware fetches `post.format` / `post.metadata` / `post.updatedAt`
|
|
86
|
+
from AppSync (apiKey auth) on each request, caches the result in a
|
|
87
|
+
200-entry LRU keyed by slug (60-second TTL, Lambda module scope), and
|
|
88
|
+
rewrites the request to the right internal handler:
|
|
89
|
+
|
|
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`
|
|
93
|
+
|
|
94
|
+
It also computes `Cache-Control` from `post.metadata.cache` (auto /
|
|
95
|
+
deep / hot) + `post.updatedAt` + `cms.config.cache.{cooldownMs,
|
|
96
|
+
freshTtlSeconds, deepTtlSeconds}` and sets the header on the response.
|
|
97
|
+
See `docs/CONTENT.md` for the cache strategy contract.
|
|
98
|
+
|
|
73
99
|
## Sub-paths
|
|
74
100
|
|
|
75
101
|
- `@ampless/runtime` — `createAmpless`, runtime types, and re-exports of `renderBody`, `renderThemeCss`, format converters
|
|
@@ -4,9 +4,7 @@ import { Ampless } from '../index.js';
|
|
|
4
4
|
import 'ampless';
|
|
5
5
|
|
|
6
6
|
interface Props$2 {
|
|
7
|
-
params: Promise<
|
|
8
|
-
siteId: string;
|
|
9
|
-
}>;
|
|
7
|
+
params: Promise<Record<string, never>>;
|
|
10
8
|
}
|
|
11
9
|
type ThemeHomeDispatcher = (props: Props$2) => Promise<ReactNode>;
|
|
12
10
|
type ThemeHomeMetadata = (props: Props$2) => Promise<Metadata>;
|
|
@@ -21,39 +19,23 @@ declare function createThemeHomeMetadata(ampless: Ampless): ThemeHomeMetadata;
|
|
|
21
19
|
|
|
22
20
|
interface Props$1 {
|
|
23
21
|
params: Promise<{
|
|
24
|
-
siteId: string;
|
|
25
22
|
slug: string;
|
|
26
23
|
}>;
|
|
27
24
|
}
|
|
28
25
|
type ThemePostDispatcher = (props: Props$1) => Promise<ReactNode>;
|
|
29
26
|
type ThemePostMetadata = (props: Props$1) => Promise<Metadata>;
|
|
30
27
|
/**
|
|
31
|
-
*
|
|
32
|
-
* theme's `components.Post` server component.
|
|
33
|
-
*
|
|
34
|
-
* Before delegating, the dispatcher peeks at the post's data and
|
|
35
|
-
* 308-redirects to `/_/<slug>` for both formats that need to bypass
|
|
36
|
-
* the theme's post page:
|
|
37
|
-
*
|
|
38
|
-
* - `metadata.no_layout === true` — the body is its own complete
|
|
39
|
-
* HTML document and ships as the entire response. The unified
|
|
40
|
-
* route handler at `/_/<slug>` emits the body verbatim.
|
|
41
|
-
* - `format === 'static'` — the body is a manifest pointing at a
|
|
42
|
-
* bundle of files in S3. The unified route handler then 308s
|
|
43
|
-
* again to `/_/<slug>/` (trailing slash) so relative paths inside
|
|
44
|
-
* the bundle resolve correctly.
|
|
45
|
-
*
|
|
46
|
-
* Why the redirect lands on `/_/<slug>` rather than `/_/<slug>/` for
|
|
47
|
-
* static posts: trailing-slash anchoring is the unified route
|
|
48
|
-
* handler's responsibility — keeps the dispatcher format-agnostic
|
|
49
|
-
* and the public URL pattern symmetric (no_layout and static both
|
|
50
|
-
* settle on `/_/<slug>(/...)`). One extra 308 round-trip is cheap;
|
|
51
|
-
* it deduplicates the trailing-slash branch into a single place.
|
|
28
|
+
* Themed post page dispatcher. Resolves the active theme and renders
|
|
29
|
+
* the theme's `components.Post` server component.
|
|
52
30
|
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
31
|
+
* Routing decision (no_layout HTML vs static bundle vs themed) lives
|
|
32
|
+
* in middleware now — middleware fetches `post.format` /
|
|
33
|
+
* `post.metadata` / `post.updatedAt` from AppSync once per slug
|
|
34
|
+
* (Lambda-memory LRU, 60s TTL) and rewrites the request to either
|
|
35
|
+
* this dispatcher (themed render) or the unified route handler at
|
|
36
|
+
* `/r/<slug>(/...)` (no_layout HTML or static bundle). That keeps the
|
|
37
|
+
* decision in one place and avoids the dispatcher's extra AppSync
|
|
38
|
+
* round-trip when the post is a themed render.
|
|
57
39
|
*
|
|
58
40
|
* If the theme doesn't declare a Post component at all, returns
|
|
59
41
|
* Next.js's notFound() (404).
|
|
@@ -64,7 +46,6 @@ declare function createThemePostMetadata(ampless: Ampless): ThemePostMetadata;
|
|
|
64
46
|
|
|
65
47
|
interface Props {
|
|
66
48
|
params: Promise<{
|
|
67
|
-
siteId: string;
|
|
68
49
|
tag: string;
|
|
69
50
|
}>;
|
|
70
51
|
}
|
|
@@ -15,14 +15,9 @@ function createThemeHomeMetadata(ampless) {
|
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
// src/dispatchers/post.ts
|
|
18
|
-
import { notFound
|
|
18
|
+
import { notFound } from "next/navigation";
|
|
19
19
|
function createThemePostDispatcher(ampless) {
|
|
20
20
|
return async function SitePostDispatcher({ params }) {
|
|
21
|
-
const { slug } = await params;
|
|
22
|
-
const post = await ampless.getPublishedPost(slug);
|
|
23
|
-
if (post?.metadata?.no_layout === true || post?.format === "static") {
|
|
24
|
-
redirect(`/_/${slug}`);
|
|
25
|
-
}
|
|
26
21
|
const { module } = await ampless.resolveActiveTheme();
|
|
27
22
|
const Post = module.components.Post;
|
|
28
23
|
if (!Post) notFound();
|
package/dist/index.d.ts
CHANGED
|
@@ -34,6 +34,12 @@ interface PublicPostShape {
|
|
|
34
34
|
publishedAt?: string | null;
|
|
35
35
|
tags?: Array<string | null> | null;
|
|
36
36
|
metadata?: unknown;
|
|
37
|
+
/**
|
|
38
|
+
* ISO 8601 timestamp from DynamoDB's auto-managed `updatedAt`.
|
|
39
|
+
* Surfaced through the `PublicPost` projection in v0.2 alpha so
|
|
40
|
+
* middleware can compute the `metadata.cache='auto'` cooldown.
|
|
41
|
+
*/
|
|
42
|
+
updatedAt?: string | null;
|
|
37
43
|
}
|
|
38
44
|
interface PublicPostConnectionShape {
|
|
39
45
|
items?: Array<PublicPostShape | null> | null;
|
package/dist/index.js
CHANGED
|
@@ -37,7 +37,8 @@ function toCorePost(p) {
|
|
|
37
37
|
status: p.status ?? "draft",
|
|
38
38
|
publishedAt: p.publishedAt ?? void 0,
|
|
39
39
|
tags: (p.tags ?? []).filter((t) => typeof t === "string"),
|
|
40
|
-
metadata: decodeMetadata(p.metadata)
|
|
40
|
+
metadata: decodeMetadata(p.metadata),
|
|
41
|
+
updatedAt: p.updatedAt ?? void 0
|
|
41
42
|
};
|
|
42
43
|
}
|
|
43
44
|
function createPostsApi(outputs) {
|
package/dist/middleware.d.ts
CHANGED
|
@@ -1,37 +1,47 @@
|
|
|
1
1
|
import { NextRequest, NextResponse } from 'next/server';
|
|
2
|
-
import { Config } from 'ampless';
|
|
2
|
+
import { Config, PostMetadata } from 'ampless';
|
|
3
3
|
|
|
4
|
+
declare const runtime = "nodejs";
|
|
4
5
|
interface CreateMiddlewareOpts {
|
|
5
6
|
cmsConfig: Config;
|
|
7
|
+
/** AppSync GraphQL endpoint URL (from `amplify_outputs.json` `data.url`). */
|
|
8
|
+
appsyncUrl: string;
|
|
9
|
+
/** Public API key for the AppSync endpoint (from `amplify_outputs.json` `data.api_key`). */
|
|
10
|
+
apiKey: string;
|
|
6
11
|
}
|
|
7
|
-
type MiddlewareFn = (request: NextRequest) => NextResponse
|
|
12
|
+
type MiddlewareFn = (request: NextRequest) => Promise<NextResponse>;
|
|
13
|
+
declare function _resetFlagCache(): void;
|
|
14
|
+
/**
|
|
15
|
+
* Compute the `Cache-Control` header for a post response. The
|
|
16
|
+
* strategy lives on `post.metadata.cache`; absent (or 'auto') falls
|
|
17
|
+
* back to cooldown-by-edit-time logic. See `CacheStrategy` /
|
|
18
|
+
* `CacheConfig` in ampless types for the semantic contract.
|
|
19
|
+
*/
|
|
20
|
+
declare function computeCacheControl(flags: {
|
|
21
|
+
metadata: PostMetadata | null;
|
|
22
|
+
updatedAt: string;
|
|
23
|
+
}, cmsConfig: Config): string;
|
|
8
24
|
/**
|
|
9
25
|
* Build the ampless public-site middleware. Performs:
|
|
10
26
|
*
|
|
11
|
-
* -
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* -
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
* `/_/<slug>(/...)`; only the rewrite target uses `r/`.
|
|
24
|
-
*
|
|
25
|
-
* Single site per Amplify deployment: the rewritten internal path uses
|
|
26
|
-
* the fixed `default` segment. The internal `/site/[siteId]/` routing
|
|
27
|
-
* tree is retained as an implementation detail; a follow-up PR
|
|
28
|
-
* flattens the URL structure further.
|
|
27
|
+
* - AppSync flag fetch (`format` / `metadata` / `updatedAt`) per
|
|
28
|
+
* slug, with a 200-entry LRU keyed by slug, 60s TTL. Hot slugs
|
|
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.
|
|
34
|
+
* - `Cache-Control` header computed from `post.metadata.cache` +
|
|
35
|
+
* `post.updatedAt` + `cms.config.cache.*` and set on the response.
|
|
36
|
+
* - `?previewTheme` / `?previewColorScheme` → `x-preview-theme` /
|
|
37
|
+
* `x-preview-color-scheme` header forwarding for the admin's
|
|
38
|
+
* iframe-based theme preview.
|
|
29
39
|
*/
|
|
30
|
-
declare function createAmplessMiddleware(
|
|
40
|
+
declare function createAmplessMiddleware(opts: CreateMiddlewareOpts): MiddlewareFn;
|
|
31
41
|
/**
|
|
32
42
|
* Reference matcher config — admin / api / login / static assets /
|
|
33
43
|
* amplify_outputs.json are excluded so middleware doesn't rewrite
|
|
34
|
-
* legitimate non-blog routes
|
|
44
|
+
* legitimate non-blog routes.
|
|
35
45
|
*
|
|
36
46
|
* **You can't re-export this directly.** Next.js 16's Turbopack
|
|
37
47
|
* requires `export const config` in `proxy.ts` (or `middleware.ts`)
|
|
@@ -48,4 +58,4 @@ declare const defaultMatcherConfig: {
|
|
|
48
58
|
matcher: string[];
|
|
49
59
|
};
|
|
50
60
|
|
|
51
|
-
export { type CreateMiddlewareOpts, type MiddlewareFn, createAmplessMiddleware, defaultMatcherConfig };
|
|
61
|
+
export { type CreateMiddlewareOpts, type MiddlewareFn, _resetFlagCache, computeCacheControl, createAmplessMiddleware, defaultMatcherConfig, runtime };
|
package/dist/middleware.js
CHANGED
|
@@ -1,22 +1,190 @@
|
|
|
1
1
|
// src/middleware.ts
|
|
2
2
|
import { NextResponse } from "next/server";
|
|
3
|
-
var
|
|
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;
|
|
11
74
|
}
|
|
12
|
-
|
|
75
|
+
} catch (err) {
|
|
76
|
+
console.error(
|
|
77
|
+
`[ampless-middleware] metadata parse failed for slug=${slug}`,
|
|
78
|
+
err
|
|
79
|
+
);
|
|
80
|
+
metadata = null;
|
|
13
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) {
|
|
135
|
+
const url = request.nextUrl.clone();
|
|
14
136
|
const previewTheme = url.searchParams.get("previewTheme");
|
|
15
137
|
const previewColorScheme = url.searchParams.get("previewColorScheme");
|
|
16
138
|
const requestHeaders = new Headers(request.headers);
|
|
17
139
|
if (previewTheme) requestHeaders.set("x-preview-theme", previewTheme);
|
|
18
|
-
if (previewColorScheme)
|
|
19
|
-
|
|
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
|
+
}
|
|
185
|
+
}
|
|
186
|
+
response.headers.set("Cache-Control", cacheControl);
|
|
187
|
+
return response;
|
|
20
188
|
};
|
|
21
189
|
}
|
|
22
190
|
var defaultMatcherConfig = {
|
|
@@ -25,6 +193,9 @@ var defaultMatcherConfig = {
|
|
|
25
193
|
]
|
|
26
194
|
};
|
|
27
195
|
export {
|
|
196
|
+
_resetFlagCache,
|
|
197
|
+
computeCacheControl,
|
|
28
198
|
createAmplessMiddleware,
|
|
29
|
-
defaultMatcherConfig
|
|
199
|
+
defaultMatcherConfig,
|
|
200
|
+
runtime
|
|
30
201
|
};
|
package/dist/routes/index.d.ts
CHANGED
|
@@ -4,7 +4,6 @@ import 'next';
|
|
|
4
4
|
|
|
5
5
|
interface Ctx$3 {
|
|
6
6
|
params: Promise<{
|
|
7
|
-
siteId: string;
|
|
8
7
|
slug: string;
|
|
9
8
|
}>;
|
|
10
9
|
}
|
|
@@ -12,9 +11,7 @@ type OgRouteHandler = (req: Request, ctx: Ctx$3) => Promise<Response>;
|
|
|
12
11
|
declare function createOgRouteHandler(ampless: Ampless): OgRouteHandler;
|
|
13
12
|
|
|
14
13
|
interface Ctx$2 {
|
|
15
|
-
params: Promise<
|
|
16
|
-
siteId: string;
|
|
17
|
-
}>;
|
|
14
|
+
params: Promise<Record<string, never>>;
|
|
18
15
|
}
|
|
19
16
|
type SitemapRouteHandler = (req: Request, ctx: Ctx$2) => Promise<Response>;
|
|
20
17
|
/**
|
|
@@ -25,9 +22,7 @@ type SitemapRouteHandler = (req: Request, ctx: Ctx$2) => Promise<Response>;
|
|
|
25
22
|
declare function createSitemapRouteHandler(ampless: Ampless): SitemapRouteHandler;
|
|
26
23
|
|
|
27
24
|
interface Ctx$1 {
|
|
28
|
-
params: Promise<
|
|
29
|
-
siteId: string;
|
|
30
|
-
}>;
|
|
25
|
+
params: Promise<Record<string, never>>;
|
|
31
26
|
}
|
|
32
27
|
type FeedRouteHandler = (req: Request, ctx: Ctx$1) => Promise<Response>;
|
|
33
28
|
/**
|
|
@@ -45,42 +40,46 @@ interface Ctx {
|
|
|
45
40
|
}
|
|
46
41
|
type UnderscoreRouteHandler = (req: Request, ctx: Ctx) => Promise<Response>;
|
|
47
42
|
/**
|
|
48
|
-
* Unified
|
|
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.
|
|
49
47
|
*
|
|
50
|
-
*
|
|
51
|
-
* both need to bypass the theme's post page:
|
|
48
|
+
* Two cases that both need to bypass the themed post page:
|
|
52
49
|
*
|
|
53
50
|
* 1. `format: 'html'` posts with `metadata.no_layout === true` — the
|
|
54
51
|
* body is its own complete HTML document and ships as the entire
|
|
55
|
-
* response.
|
|
52
|
+
* response. Reached via middleware rewrite of `/<slug>`.
|
|
56
53
|
* 2. `format: 'static'` posts — the body is a manifest describing a
|
|
57
54
|
* bundle of files in S3 at `public/static/<slug>/`. The bundle's
|
|
58
|
-
* entrypoint is served at
|
|
59
|
-
*
|
|
55
|
+
* entrypoint is served at `/<slug>/`, every internal file at
|
|
56
|
+
* `/<slug>/<relative-path>` (middleware rewrites both to
|
|
57
|
+
* `/r/<slug>/...`).
|
|
60
58
|
*
|
|
61
59
|
* Routing model:
|
|
62
|
-
* - File location: `app/
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
* next/dist/build/route-discovery.js).
|
|
67
|
-
*
|
|
68
|
-
* `/_/<slug>(/...)`.
|
|
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>(/...)`.
|
|
69
66
|
* - `params.path` is `undefined` (or `[]`) for single-segment
|
|
70
|
-
* requests
|
|
67
|
+
* requests, an array of remaining segments otherwise.
|
|
71
68
|
*
|
|
72
|
-
* Trailing-slash responsibility lives here
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
* legacy static route's behaviour — `<img src="img.png">` must resolve
|
|
77
|
-
* to `/_/<slug>/img.png`, not the site root.
|
|
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.
|
|
78
73
|
*
|
|
79
74
|
* Trust model: no_layout HTML bodies are emitted verbatim, same trust
|
|
80
75
|
* shape as `format: 'html'` post bodies on the regular path. Static
|
|
81
76
|
* bundle assets are served via short-lived S3 presigned URLs; the
|
|
82
77
|
* bucket stays private. See docs/architecture/04-access-layer-mcp.md
|
|
83
78
|
* §"editor の信頼モデル".
|
|
79
|
+
*
|
|
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.
|
|
84
83
|
*/
|
|
85
84
|
declare function createUnderscoreRouteHandler(ampless: Ampless): UnderscoreRouteHandler;
|
|
86
85
|
|
package/dist/routes/index.js
CHANGED
|
@@ -85,8 +85,9 @@ function createUnderscoreRouteHandler(ampless) {
|
|
|
85
85
|
return new Response(ampless.renderBody(post), {
|
|
86
86
|
status: 200,
|
|
87
87
|
headers: {
|
|
88
|
-
"Content-Type": "text/html; charset=utf-8"
|
|
89
|
-
|
|
88
|
+
"Content-Type": "text/html; charset=utf-8"
|
|
89
|
+
// Cache-Control set by middleware (cache strategy depends on
|
|
90
|
+
// post.metadata.cache + post.updatedAt).
|
|
90
91
|
}
|
|
91
92
|
});
|
|
92
93
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ampless/runtime",
|
|
3
|
-
"version": "1.0.0-alpha.
|
|
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",
|
|
@@ -49,8 +49,8 @@
|
|
|
49
49
|
"lucide-react": "^1.16.0",
|
|
50
50
|
"marked": "^14.1.4",
|
|
51
51
|
"tailwind-merge": "^3.6.0",
|
|
52
|
-
"ampless": "1.0.0-alpha.
|
|
53
|
-
"@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"
|
|
54
54
|
},
|
|
55
55
|
"peerDependencies": {
|
|
56
56
|
"next": "^15 || ^16",
|