@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 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`, `createRawRouteHandler`
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 for the request's
15
- * siteId and renders the theme's `components.Home` server component
16
- * with the same `params` Promise it was passed.
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 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.
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 { siteId } = await params;
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 { siteId } = await params;
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 { siteId, slug } = await params;
24
- const post = await ampless.getPublishedPost(slug, { siteId });
25
- if (post?.metadata?.no_layout === true) {
26
- redirect(`/raw/${slug}`);
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
- 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
- }
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 { siteId } = await params;
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 { siteId } = await params;
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 { siteId } = await params;
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, opts?: {
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(siteId?: string): Promise<EffectiveSiteSettings>;
85
+ loadSiteSettings(): Promise<EffectiveSiteSettings>;
91
86
  }
92
87
 
93
88
  interface SeoApi {
94
- postMetadata(post: Post, siteId?: string): Promise<Metadata>;
95
- siteMetadata(siteId?: string): Promise<Metadata>;
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 for the site. */
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(siteId?: string): Promise<ResolvedTheme>;
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(siteId?: string): Promise<EffectiveThemeConfig>;
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 so a format-switch chain (e.g. markdown tiptap
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 the built-in minimal renderer. */
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 Markdown converter. Handles the tag set the
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>`). Anything else (tables, sections, divs) keeps its content
185
- * but loses structural meaning.
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 there are known limits like nested formatting
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, opts?: {
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(siteId?: string): Promise<EffectiveSiteSettings>;
206
- resolveActiveTheme(siteId?: string): Promise<ResolvedTheme>;
207
- loadThemeConfig(siteId?: string): Promise<EffectiveThemeConfig>;
208
- postMetadata(post: Post, siteId?: string): Promise<Metadata>;
209
- siteMetadata(siteId?: string): Promise<Metadata>;
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;