@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/README.ja.md
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
> English: [README.md](./README.md)
|
|
2
|
+
>
|
|
3
|
+
|
|
4
|
+
# @ampless/runtime
|
|
5
|
+
|
|
6
|
+
[ampless](https://github.com/heavymoons/ampless) 向けパブリックサイドランタイム。投稿取得クライアント、サイト設定、テーマ解決、SEO メタデータ集約、ミドルウェア、パブリックルートハンドラーを `createAmpless()` ひとつのファクトリーにまとめます。
|
|
7
|
+
|
|
8
|
+
> **プレリリース / アルファ版。** v1.0 まではマイナーバージョンでも破壊的変更が入る可能性があります。
|
|
9
|
+
|
|
10
|
+
テンプレートから切り出すことで、スキャフォールドされたファイルに触れずに `npm update @ampless/runtime` でアップデートできます — パブリックサイトの動作改善はスキャフォールダーの再実行ではなく、パッケージを通じて届きます。
|
|
11
|
+
|
|
12
|
+
## インストール
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
npm install @ampless/runtime@alpha ampless@alpha
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
`@ampless/runtime` は `next`(15+)、`react`(18/19)、`aws-amplify`(6+)、`@aws-amplify/adapter-nextjs`(1+)をピア依存として宣言します。CLI スキャフォールダーがテンプレートの `package.json` に互換バージョンをピン留めします。
|
|
19
|
+
|
|
20
|
+
## 使い方
|
|
21
|
+
|
|
22
|
+
テンプレートは `lib/ampless.ts` に共有インスタンスを 1 つ作成します:
|
|
23
|
+
|
|
24
|
+
```ts
|
|
25
|
+
import outputs from '../amplify_outputs.json'
|
|
26
|
+
import cmsConfig from '../cms.config'
|
|
27
|
+
import { themes, DEFAULT_THEME } from '../themes-registry'
|
|
28
|
+
import { createAmpless } from '@ampless/runtime'
|
|
29
|
+
|
|
30
|
+
export const ampless = createAmpless({
|
|
31
|
+
outputs,
|
|
32
|
+
cmsConfig,
|
|
33
|
+
themes: { themes, defaultTheme: DEFAULT_THEME },
|
|
34
|
+
})
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
`app/site/[siteId]/` 以下のルートとディスパッチャーはワンライナーのファクトリー呼び出しになります:
|
|
38
|
+
|
|
39
|
+
```ts
|
|
40
|
+
// app/site/[siteId]/page.tsx
|
|
41
|
+
import { ampless } from '@/lib/ampless'
|
|
42
|
+
import {
|
|
43
|
+
createThemeHomeDispatcher,
|
|
44
|
+
createThemeHomeMetadata,
|
|
45
|
+
} from '@ampless/runtime/dispatchers'
|
|
46
|
+
|
|
47
|
+
export const dynamic = 'force-dynamic'
|
|
48
|
+
export const generateMetadata = createThemeHomeMetadata(ampless)
|
|
49
|
+
export default createThemeHomeDispatcher(ampless)
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
```ts
|
|
53
|
+
// app/site/[siteId]/og/[slug]/route.ts
|
|
54
|
+
import { ampless } from '@/lib/ampless'
|
|
55
|
+
import { createOgRouteHandler } from '@ampless/runtime/routes'
|
|
56
|
+
|
|
57
|
+
export const runtime = 'nodejs'
|
|
58
|
+
export const dynamic = 'force-dynamic'
|
|
59
|
+
export const GET = createOgRouteHandler(ampless)
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
ミドルウェア:
|
|
63
|
+
|
|
64
|
+
```ts
|
|
65
|
+
// middleware.ts
|
|
66
|
+
import cmsConfig from './cms.config'
|
|
67
|
+
import { createAmplessMiddleware, defaultMatcherConfig } from '@ampless/runtime/middleware'
|
|
68
|
+
|
|
69
|
+
export const middleware = createAmplessMiddleware({ cmsConfig })
|
|
70
|
+
export const config = defaultMatcherConfig
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## サブパス
|
|
74
|
+
|
|
75
|
+
- `@ampless/runtime` — `createAmpless`、ランタイム型、`renderBody`・`renderThemeCss`・フォーマットコンバーターの再エクスポート
|
|
76
|
+
- `@ampless/runtime/middleware` — `createAmplessMiddleware`、`defaultMatcherConfig`
|
|
77
|
+
- `@ampless/runtime/routes` — `createOgRouteHandler`、`createSitemapRouteHandler`、`createFeedRouteHandler`、`createUnderscoreRouteHandler`
|
|
78
|
+
- `@ampless/runtime/dispatchers` — `createThemeHomeDispatcher`、`createThemePostDispatcher`、`createThemeTagDispatcher`(それぞれ対応する `*Metadata` ファクトリーあり)
|
|
79
|
+
|
|
80
|
+
## テンプレートに残るもの
|
|
81
|
+
|
|
82
|
+
管理側モジュール(投稿プロバイダー、テーマアクション、認証、KV 書き込み)とテーマコンポーネントはスキャフォールドに残ります。これらは後のリリースで `@ampless/admin` に移行する予定です。それまでは、これらのファイルへの変更はユーザーのプロジェクト内で行ってください。
|
|
83
|
+
|
|
84
|
+
## ライセンス
|
|
85
|
+
|
|
86
|
+
MIT
|
package/README.md
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
> 日本語版: [README.ja.md](./README.ja.md)
|
|
2
|
+
>
|
|
3
|
+
|
|
1
4
|
# @ampless/runtime
|
|
2
5
|
|
|
3
6
|
Public-side runtime for [ampless](https://github.com/heavymoons/ampless). Bundles the post-fetching client, site settings, theme resolution, SEO metadata aggregation, middleware, and public route handlers behind a single `createAmpless()` factory.
|
|
@@ -71,7 +74,7 @@ export const config = defaultMatcherConfig
|
|
|
71
74
|
|
|
72
75
|
- `@ampless/runtime` — `createAmpless`, runtime types, and re-exports of `renderBody`, `renderThemeCss`, format converters
|
|
73
76
|
- `@ampless/runtime/middleware` — `createAmplessMiddleware`, `defaultMatcherConfig`
|
|
74
|
-
- `@ampless/runtime/routes` — `createOgRouteHandler`, `createSitemapRouteHandler`, `createFeedRouteHandler`, `
|
|
77
|
+
- `@ampless/runtime/routes` — `createOgRouteHandler`, `createSitemapRouteHandler`, `createFeedRouteHandler`, `createUnderscoreRouteHandler`
|
|
75
78
|
- `@ampless/runtime/dispatchers` — `createThemeHomeDispatcher`, `createThemePostDispatcher`, `createThemeTagDispatcher` (each with a matching `*Metadata` factory)
|
|
76
79
|
|
|
77
80
|
## What's still in the template
|
|
@@ -11,9 +11,9 @@ interface Props$2 {
|
|
|
11
11
|
type ThemeHomeDispatcher = (props: Props$2) => Promise<ReactNode>;
|
|
12
12
|
type ThemeHomeMetadata = (props: Props$2) => Promise<Metadata>;
|
|
13
13
|
/**
|
|
14
|
-
* Home page dispatcher. Resolves the active theme
|
|
15
|
-
*
|
|
16
|
-
*
|
|
14
|
+
* Home page dispatcher. Resolves the active theme and renders the
|
|
15
|
+
* theme's `components.Home` server component with the same `params`
|
|
16
|
+
* Promise it was passed.
|
|
17
17
|
*/
|
|
18
18
|
declare function createThemeHomeDispatcher(ampless: Ampless): ThemeHomeDispatcher;
|
|
19
19
|
/** generateMetadata factory for the home dispatcher. */
|
|
@@ -31,14 +31,24 @@ type ThemePostMetadata = (props: Props$1) => Promise<Metadata>;
|
|
|
31
31
|
* Post page dispatcher. Resolves the active theme and renders the
|
|
32
32
|
* theme's `components.Post` server component.
|
|
33
33
|
*
|
|
34
|
-
* Before delegating, the dispatcher peeks at the post's
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
* the
|
|
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.
|
|
42
52
|
*
|
|
43
53
|
* The metadata peek is an extra AppSync call before theme resolve,
|
|
44
54
|
* but it's the same query the theme's Post component would make
|
|
@@ -1,16 +1,14 @@
|
|
|
1
1
|
// src/dispatchers/home.ts
|
|
2
2
|
function createThemeHomeDispatcher(ampless) {
|
|
3
3
|
return async function SiteHomeDispatcher({ params }) {
|
|
4
|
-
const {
|
|
5
|
-
const { module } = await ampless.resolveActiveTheme(siteId);
|
|
4
|
+
const { module } = await ampless.resolveActiveTheme();
|
|
6
5
|
const Home = module.components.Home;
|
|
7
6
|
return await Home({ params });
|
|
8
7
|
};
|
|
9
8
|
}
|
|
10
9
|
function createThemeHomeMetadata(ampless) {
|
|
11
10
|
return async function generateMetadata({ params }) {
|
|
12
|
-
const {
|
|
13
|
-
const { module } = await ampless.resolveActiveTheme(siteId);
|
|
11
|
+
const { module } = await ampless.resolveActiveTheme();
|
|
14
12
|
const fn = module.metadata?.Home;
|
|
15
13
|
return fn ? await fn({ params }) : {};
|
|
16
14
|
};
|
|
@@ -20,17 +18,12 @@ function createThemeHomeMetadata(ampless) {
|
|
|
20
18
|
import { notFound, redirect } from "next/navigation";
|
|
21
19
|
function createThemePostDispatcher(ampless) {
|
|
22
20
|
return async function SitePostDispatcher({ params }) {
|
|
23
|
-
const {
|
|
24
|
-
const post = await ampless.getPublishedPost(slug
|
|
25
|
-
if (post?.metadata?.no_layout === true) {
|
|
26
|
-
redirect(`/
|
|
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}`);
|
|
27
25
|
}
|
|
28
|
-
|
|
29
|
-
const body = post.body ?? null;
|
|
30
|
-
const entrypoint = typeof body?.entrypoint === "string" && body.entrypoint ? body.entrypoint : "index.html";
|
|
31
|
-
redirect(`/${slug}/${entrypoint}`);
|
|
32
|
-
}
|
|
33
|
-
const { module } = await ampless.resolveActiveTheme(siteId);
|
|
26
|
+
const { module } = await ampless.resolveActiveTheme();
|
|
34
27
|
const Post = module.components.Post;
|
|
35
28
|
if (!Post) notFound();
|
|
36
29
|
return await Post({ params });
|
|
@@ -38,8 +31,7 @@ function createThemePostDispatcher(ampless) {
|
|
|
38
31
|
}
|
|
39
32
|
function createThemePostMetadata(ampless) {
|
|
40
33
|
return async function generateMetadata({ params }) {
|
|
41
|
-
const {
|
|
42
|
-
const { module } = await ampless.resolveActiveTheme(siteId);
|
|
34
|
+
const { module } = await ampless.resolveActiveTheme();
|
|
43
35
|
const fn = module.metadata?.Post;
|
|
44
36
|
return fn ? await fn({ params }) : {};
|
|
45
37
|
};
|
|
@@ -49,8 +41,7 @@ function createThemePostMetadata(ampless) {
|
|
|
49
41
|
import { notFound as notFound2 } from "next/navigation";
|
|
50
42
|
function createThemeTagDispatcher(ampless) {
|
|
51
43
|
return async function SiteTagDispatcher({ params }) {
|
|
52
|
-
const {
|
|
53
|
-
const { module } = await ampless.resolveActiveTheme(siteId);
|
|
44
|
+
const { module } = await ampless.resolveActiveTheme();
|
|
54
45
|
const Tag = module.components.Tag;
|
|
55
46
|
if (!Tag) notFound2();
|
|
56
47
|
return await Tag({ params });
|
|
@@ -58,8 +49,7 @@ function createThemeTagDispatcher(ampless) {
|
|
|
58
49
|
}
|
|
59
50
|
function createThemeTagMetadata(ampless) {
|
|
60
51
|
return async function generateMetadata({ params }) {
|
|
61
|
-
const {
|
|
62
|
-
const { module } = await ampless.resolveActiveTheme(siteId);
|
|
52
|
+
const { module } = await ampless.resolveActiveTheme();
|
|
63
53
|
const fn = module.metadata?.Tag;
|
|
64
54
|
return fn ? await fn({ params }) : {};
|
|
65
55
|
};
|
package/dist/index.d.ts
CHANGED
|
@@ -25,7 +25,6 @@ interface StorageApi {
|
|
|
25
25
|
|
|
26
26
|
interface PublicPostShape {
|
|
27
27
|
postId: string;
|
|
28
|
-
siteId: string;
|
|
29
28
|
slug: string;
|
|
30
29
|
title: string;
|
|
31
30
|
excerpt?: string | null;
|
|
@@ -41,7 +40,6 @@ interface PublicPostConnectionShape {
|
|
|
41
40
|
nextToken?: string | null;
|
|
42
41
|
}
|
|
43
42
|
interface ListPostsOptions {
|
|
44
|
-
siteId?: string;
|
|
45
43
|
/** ISO 8601 timestamp; SK lower bound (inclusive). */
|
|
46
44
|
from?: string;
|
|
47
45
|
/** ISO 8601 timestamp; SK upper bound (inclusive). */
|
|
@@ -51,7 +49,6 @@ interface ListPostsOptions {
|
|
|
51
49
|
nextToken?: string;
|
|
52
50
|
}
|
|
53
51
|
interface ListPostsByTagOptions {
|
|
54
|
-
siteId?: string;
|
|
55
52
|
limit?: number;
|
|
56
53
|
nextToken?: string;
|
|
57
54
|
}
|
|
@@ -61,9 +58,7 @@ interface ListPostsResult {
|
|
|
61
58
|
}
|
|
62
59
|
interface PostsApi {
|
|
63
60
|
listPublishedPosts(opts?: ListPostsOptions): Promise<ListPostsResult>;
|
|
64
|
-
getPublishedPost(slug: string
|
|
65
|
-
siteId?: string;
|
|
66
|
-
}): Promise<Post | null>;
|
|
61
|
+
getPublishedPost(slug: string): Promise<Post | null>;
|
|
67
62
|
listPostsByTag(tag: string, opts?: ListPostsByTagOptions): Promise<ListPostsResult>;
|
|
68
63
|
}
|
|
69
64
|
|
|
@@ -87,18 +82,18 @@ interface EffectiveSiteSettings {
|
|
|
87
82
|
timezone?: string;
|
|
88
83
|
}
|
|
89
84
|
interface SiteSettingsApi {
|
|
90
|
-
loadSiteSettings(
|
|
85
|
+
loadSiteSettings(): Promise<EffectiveSiteSettings>;
|
|
91
86
|
}
|
|
92
87
|
|
|
93
88
|
interface SeoApi {
|
|
94
|
-
postMetadata(post: Post
|
|
95
|
-
siteMetadata(
|
|
89
|
+
postMetadata(post: Post): Promise<Metadata>;
|
|
90
|
+
siteMetadata(): Promise<Metadata>;
|
|
96
91
|
}
|
|
97
92
|
|
|
98
93
|
interface ThemesRegistry {
|
|
99
94
|
/** Map of theme name → loaded theme module. */
|
|
100
95
|
themes: Record<string, ThemeModule>;
|
|
101
|
-
/** Name used when no `theme.active` override is stored
|
|
96
|
+
/** Name used when no `theme.active` override is stored. */
|
|
102
97
|
defaultTheme: string;
|
|
103
98
|
}
|
|
104
99
|
interface ResolvedTheme {
|
|
@@ -106,7 +101,7 @@ interface ResolvedTheme {
|
|
|
106
101
|
module: ThemeModule;
|
|
107
102
|
}
|
|
108
103
|
interface ThemeActiveApi {
|
|
109
|
-
resolveActiveTheme(
|
|
104
|
+
resolveActiveTheme(): Promise<ResolvedTheme>;
|
|
110
105
|
}
|
|
111
106
|
|
|
112
107
|
type ColorScheme = 'auto' | 'light' | 'dark';
|
|
@@ -145,7 +140,7 @@ interface EffectiveThemeConfig {
|
|
|
145
140
|
colorScheme: ColorScheme;
|
|
146
141
|
}
|
|
147
142
|
interface ThemeConfigApi {
|
|
148
|
-
loadThemeConfig(
|
|
143
|
+
loadThemeConfig(): Promise<EffectiveThemeConfig>;
|
|
149
144
|
}
|
|
150
145
|
/**
|
|
151
146
|
* Render `cssVars` as the body of a `:root { ... }` CSS block. Values
|
|
@@ -159,32 +154,42 @@ declare function renderBody(post: Post): string;
|
|
|
159
154
|
* Convert a tiptap doc to its HTML form. Same renderer the public
|
|
160
155
|
* site uses. Defensive: tiptap accepts an HTML string as initial
|
|
161
156
|
* content and parses it on mount, but won't fire onUpdate until the
|
|
162
|
-
* user edits
|
|
157
|
+
* user edits, so a format-switch chain (e.g. markdown -> tiptap ->
|
|
163
158
|
* markdown without editing) can still hand us a raw HTML string
|
|
164
159
|
* here. In that case, return it as-is rather than walking it as a
|
|
165
160
|
* malformed tiptap node and producing empty output.
|
|
166
161
|
*/
|
|
167
162
|
declare function tiptapToHtml(doc: unknown): string;
|
|
168
|
-
/** Convert markdown to HTML using
|
|
163
|
+
/** Convert markdown to HTML using marked + GFM. */
|
|
169
164
|
declare function markdownToHtml(md: string): string;
|
|
170
165
|
/**
|
|
171
166
|
* Walk a tiptap doc and emit Markdown. Mirrors `renderTiptap` in
|
|
172
167
|
* shape but produces markdown syntax. Loses anything markdown can't
|
|
173
168
|
* express (data attributes, image display modes, custom marks).
|
|
174
169
|
*
|
|
170
|
+
* Notes on info loss:
|
|
171
|
+
* - underline / highlight are not in GFM, so they fall back to the
|
|
172
|
+
* literal `<u>` / `<mark>` HTML tags (preserved as-is across round trips).
|
|
173
|
+
* - paragraph / heading textAlign cannot be expressed in markdown and
|
|
174
|
+
* is therefore lost on conversion.
|
|
175
|
+
*
|
|
175
176
|
* Same defensive path as tiptapToHtml: a string input means tiptap
|
|
176
177
|
* hasn't emitted JSON yet (the body is still the HTML we handed it).
|
|
177
178
|
* Route through htmlToMarkdown so the content survives.
|
|
178
179
|
*/
|
|
179
180
|
declare function tiptapToMarkdown(doc: unknown): string;
|
|
180
181
|
/**
|
|
181
|
-
* Regex-based HTML
|
|
182
|
+
* Regex-based HTML -> Markdown converter. Handles the tag set the
|
|
182
183
|
* editor produces (`<p>` `<h1>`-`<h6>` `<strong>` `<em>` `<a>`
|
|
183
184
|
* `<img>` `<ul>` `<ol>` `<li>` `<code>` `<pre>` `<blockquote>` `<hr>`
|
|
184
|
-
* `<br>`
|
|
185
|
-
*
|
|
185
|
+
* `<br>` `<u>` `<mark>` `<table>` task-list `<ul data-type="taskList">`).
|
|
186
|
+
* Decorative containers like `<div style="text-align:...">` are dropped.
|
|
187
|
+
*
|
|
188
|
+
* Tables are reduced to GFM pipe syntax via convertHtmlTable. Complex
|
|
189
|
+
* nested content inside cells (lists, other tables) is flattened to
|
|
190
|
+
* plain text.
|
|
186
191
|
*
|
|
187
|
-
* Not a full library
|
|
192
|
+
* Not a full library, there are known limits like nested formatting
|
|
188
193
|
* inside list items potentially merging. Acceptable for a v0.x
|
|
189
194
|
* format-switch convenience; complex HTML round-trips shouldn't be
|
|
190
195
|
* relied on.
|
|
@@ -198,15 +203,13 @@ interface CreateAmplessOpts {
|
|
|
198
203
|
}
|
|
199
204
|
interface Ampless {
|
|
200
205
|
listPublishedPosts(opts?: ListPostsOptions): Promise<ListPostsResult>;
|
|
201
|
-
getPublishedPost(slug: string
|
|
202
|
-
siteId?: string;
|
|
203
|
-
}): Promise<Post | null>;
|
|
206
|
+
getPublishedPost(slug: string): Promise<Post | null>;
|
|
204
207
|
listPostsByTag(tag: string, opts?: ListPostsByTagOptions): Promise<ListPostsResult>;
|
|
205
|
-
loadSiteSettings(
|
|
206
|
-
resolveActiveTheme(
|
|
207
|
-
loadThemeConfig(
|
|
208
|
-
postMetadata(post: Post
|
|
209
|
-
siteMetadata(
|
|
208
|
+
loadSiteSettings(): Promise<EffectiveSiteSettings>;
|
|
209
|
+
resolveActiveTheme(): Promise<ResolvedTheme>;
|
|
210
|
+
loadThemeConfig(): Promise<EffectiveThemeConfig>;
|
|
211
|
+
postMetadata(post: Post): Promise<Metadata>;
|
|
212
|
+
siteMetadata(): Promise<Metadata>;
|
|
210
213
|
renderBody(post: Post): string;
|
|
211
214
|
renderThemeCss(cssVars: Record<string, string>): string;
|
|
212
215
|
publicAssetUrl(key: string): string;
|