ampless 1.0.0-alpha.42 → 1.0.0-alpha.43

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.
@@ -808,16 +808,24 @@ export default createAdminLayout(admin, { editorBootstrap: EditorBootstrap })
808
808
 
809
809
  ### エディタプレビューのパイプライン
810
810
 
811
- admin の edit / new post フォームは preview ペインを `<iframe sandbox="allow-scripts">` で表示する。`srcDoc` はテンプレート側 server action が返す HTML:
811
+ admin の edit / new post フォームは preview ペインを `<iframe sandbox="allow-scripts">` で表示する。`srcDoc` はテンプレート側 preview Route Handler が返す HTML。テンプレート scaffold は handler を `app/(admin)/admin/preview/route.tsx` に同梱する:
812
812
 
813
813
  ```tsx
814
- // templates/_shared/app/(admin)/admin/_actions/render-preview.tsx
815
- 'use server'
816
- import { renderToStaticMarkup } from 'react-dom/server'
814
+ // templates/_shared/app/(admin)/admin/preview/route.tsx
817
815
  import type { Post } from 'ampless'
818
816
  import { admin } from '@/lib/admin'
819
817
 
820
- export async function renderPreviewHtml(draft: Post): Promise<string> {
818
+ export async function POST(req: Request): Promise<Response> {
819
+ const session = await admin.getServerSession()
820
+ if (!admin.isEditor(session)) {
821
+ return new Response('Forbidden', { status: 403 })
822
+ }
823
+ let draft: Post
824
+ try {
825
+ draft = (await req.json()) as Post
826
+ } catch {
827
+ return new Response('Bad Request', { status: 400 })
828
+ }
821
829
  const ampless = await admin.getAmpless()
822
830
  const node = (
823
831
  <>
@@ -825,21 +833,35 @@ export async function renderPreviewHtml(draft: Post): Promise<string> {
825
833
  {await ampless.publicPostScriptsForPage([draft])}
826
834
  </>
827
835
  )
828
- return renderToStaticMarkup(node)
836
+ // dynamic import: 下の「Server Action ではなく Route Handler を採用した理由」参照。
837
+ const { renderToStaticMarkup } = await import('react-dom/server')
838
+ return new Response(renderToStaticMarkup(node), {
839
+ headers: {
840
+ 'Content-Type': 'text/html; charset=utf-8',
841
+ 'Cache-Control': 'no-store',
842
+ },
843
+ })
829
844
  }
830
845
  ```
831
846
 
832
- page factory `renderPreviewAction` オプションでこの actionthread する:
847
+ `<PostForm>` / `<PostHistoryPanel>` は素のままで `/admin/preview` draftPOST する — call site で追加の wiring は不要:
833
848
 
834
849
  ```tsx
835
850
  // templates/_shared/app/(admin)/admin/posts/[postId]/page.tsx
836
851
  import { admin } from '@/lib/admin'
837
852
  import { createEditPostPage } from '@ampless/admin/pages'
838
- import { renderPreviewHtml } from '../../_actions/render-preview'
839
853
 
840
- export default createEditPostPage(admin, { renderPreviewAction: renderPreviewHtml })
854
+ export default createEditPostPage(admin)
841
855
  ```
842
856
 
857
+ admin が非デフォルトパス(Next.js `basePath` やカスタム prefix)にマウントされている場合は、page factory の `previewEndpoint` option で endpoint を上書きできる:
858
+
859
+ ```tsx
860
+ export default createEditPostPage(admin, { previewEndpoint: '/cms/admin/preview' })
861
+ ```
862
+
863
+ Server Action ではなく Route Handler を採用した理由: `'use server'` モジュール内部で `react-dom/server` 由来の rendering を行うと、Next.js 15+ が edit-post page の compile に失敗する。build 時に Client Component から Server Action モジュール経由で import graph を辿るため、その経路上で `react-dom/server` に到達した時点で「You're importing a component that imports react-dom/server」チェックが発火する。Route Handler に切り出すことで rendering を import graph から完全に切り離せる — `<PostForm>` は plain な HTTP endpoint を fetch するだけで、bundler は form から handler 側へ踏み込まない。さらに handler 自身が `admin.isEditor()` を明示的にチェックすることで、将来 `(admin)` route-group gate が誤設定された場合の安全側のフェールセーフになる。`react-dom/server` の import 自体を dynamic にしているのは Next.js 16 の Turbopack が、Route Handler を含む app router build 経路で reach できる top-level の `react-dom/server` static import を一律で flag するため。request 時に解決することで build-time import-graph walker から外しつつ、runtime では同じ Node.js subpath からロードする。
864
+
843
865
  iframe の `sandbox="allow-scripts"` は `allow-same-origin` を含まないので、preview の中身は opaque origin で動作する → widget script は admin の DOM に触れない。トレードオフ: widget が自身の origin の storage にアクセスできない場合に正しく動かない可能性がある。Phase 7 時点の dogfood では YouTube iframe / x.com widgets.js とも opaque origin で動作することを確認済み。将来 non-opaque origin が必要な widget が出てきた場合、escape hatch は別 subdomain の preview route + 適切な CSP — sandbox flag を緩めるのは取らない。
844
866
 
845
867
  ---
@@ -1062,16 +1062,25 @@ registered list onto its built-in extensions on every render.
1062
1062
 
1063
1063
  The admin's edit / new post forms render the preview pane in an
1064
1064
  `<iframe sandbox="allow-scripts">` whose `srcDoc` is the HTML returned
1065
- by the template's server action:
1065
+ by the template's preview Route Handler. The template scaffold ships
1066
+ the handler at `app/(admin)/admin/preview/route.tsx`:
1066
1067
 
1067
1068
  ```tsx
1068
- // templates/_shared/app/(admin)/admin/_actions/render-preview.tsx
1069
- 'use server'
1070
- import { renderToStaticMarkup } from 'react-dom/server'
1069
+ // templates/_shared/app/(admin)/admin/preview/route.tsx
1071
1070
  import type { Post } from 'ampless'
1072
1071
  import { admin } from '@/lib/admin'
1073
1072
 
1074
- export async function renderPreviewHtml(draft: Post): Promise<string> {
1073
+ export async function POST(req: Request): Promise<Response> {
1074
+ const session = await admin.getServerSession()
1075
+ if (!admin.isEditor(session)) {
1076
+ return new Response('Forbidden', { status: 403 })
1077
+ }
1078
+ let draft: Post
1079
+ try {
1080
+ draft = (await req.json()) as Post
1081
+ } catch {
1082
+ return new Response('Bad Request', { status: 400 })
1083
+ }
1075
1084
  const ampless = await admin.getAmpless()
1076
1085
  const node = (
1077
1086
  <>
@@ -1079,22 +1088,54 @@ export async function renderPreviewHtml(draft: Post): Promise<string> {
1079
1088
  {await ampless.publicPostScriptsForPage([draft])}
1080
1089
  </>
1081
1090
  )
1082
- return renderToStaticMarkup(node)
1091
+ // Dynamic import: see "Why a Route Handler" below.
1092
+ const { renderToStaticMarkup } = await import('react-dom/server')
1093
+ return new Response(renderToStaticMarkup(node), {
1094
+ headers: {
1095
+ 'Content-Type': 'text/html; charset=utf-8',
1096
+ 'Cache-Control': 'no-store',
1097
+ },
1098
+ })
1083
1099
  }
1084
1100
  ```
1085
1101
 
1086
- Page factories thread the action down via the `renderPreviewAction`
1087
- option:
1102
+ `<PostForm>` / `<PostHistoryPanel>` POST the draft to `/admin/preview`
1103
+ out of the box — no extra wiring at the call site:
1088
1104
 
1089
1105
  ```tsx
1090
1106
  // templates/_shared/app/(admin)/admin/posts/[postId]/page.tsx
1091
1107
  import { admin } from '@/lib/admin'
1092
1108
  import { createEditPostPage } from '@ampless/admin/pages'
1093
- import { renderPreviewHtml } from '../../_actions/render-preview'
1094
1109
 
1095
- export default createEditPostPage(admin, { renderPreviewAction: renderPreviewHtml })
1110
+ export default createEditPostPage(admin)
1096
1111
  ```
1097
1112
 
1113
+ If the admin is mounted at a non-default path (Next.js `basePath` or
1114
+ a custom prefix), override the endpoint via the page factory's
1115
+ `previewEndpoint` option:
1116
+
1117
+ ```tsx
1118
+ export default createEditPostPage(admin, { previewEndpoint: '/cms/admin/preview' })
1119
+ ```
1120
+
1121
+ Why a Route Handler rather than a Server Action: putting
1122
+ `react-dom/server`-driven rendering inside a `'use server'` module
1123
+ makes Next.js 15+ refuse to compile the edit-post page, because the
1124
+ build traces the import graph from Client Components through Server
1125
+ Action modules and any reach to `react-dom/server` along that path
1126
+ trips the "You're importing a component that imports
1127
+ react-dom/server" check. A Route Handler decouples the rendering
1128
+ from that graph entirely — `<PostForm>` fetches a plain HTTP
1129
+ endpoint and the bundler never walks from the form into here. The
1130
+ handler's explicit `admin.isEditor()` check also defends against
1131
+ accidental exposure if the `(admin)` route-group gate is ever
1132
+ misconfigured. The `react-dom/server` import itself is dynamic
1133
+ because Next.js 16's Turbopack flags any top-level static import of
1134
+ `react-dom/server` reached from the app router build, Route Handlers
1135
+ included; deferring it to request time keeps the module outside the
1136
+ build-time import-graph walker while still loading it from the same
1137
+ Node.js subpath at runtime.
1138
+
1098
1139
  The iframe's `sandbox="allow-scripts"` (without `allow-same-origin`)
1099
1140
  keeps preview content in an opaque origin so widget scripts cannot
1100
1141
  reach the admin's DOM. The trade-off is that some widgets may misbehave
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ampless",
3
- "version": "1.0.0-alpha.42",
3
+ "version": "1.0.0-alpha.43",
4
4
  "description": "Serverless CMS for AWS Amplify",
5
5
  "license": "MIT",
6
6
  "type": "module",