create-ampless 1.0.0-alpha.126 → 1.0.0-alpha.128

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.
@@ -0,0 +1,32 @@
1
+ 'use server'
2
+
3
+ // Phase 7 preview pipeline. <PostForm> / <PostHistoryPanel> calls this
4
+ // server action with the in-flight draft Post; we render the body +
5
+ // page-level scripts on the server (where async ReactNode +
6
+ // `contentFields` plugin renderers can run) and return a complete HTML
7
+ // string that the admin shows in an iframe (sandbox=`allow-scripts`).
8
+ //
9
+ // Server-side is the only place this can live: `ampless.renderBody`
10
+ // returns a Promise<ReactNode>, so client code can't call it inline.
11
+ // The action also has access to the `Ampless` instance via
12
+ // `admin.getAmpless()` which the client never sees.
13
+
14
+ import { renderToStaticMarkup } from 'react-dom/server'
15
+ import type { Post } from 'ampless'
16
+ import { admin } from '@/lib/admin'
17
+
18
+ export async function renderPreviewHtml(draft: Post): Promise<string> {
19
+ const ampless = await admin.getAmpless()
20
+ // IMPORTANT: include BOTH the body and the page-level scripts so
21
+ // widgets like x.com's `widgets.js` get a chance to hydrate in the
22
+ // iframe. Without `publicPostScriptsForPage([draft])` the embed
23
+ // blockquote would render but never load the widget JS, and authors
24
+ // wouldn't see what the public page actually looks like.
25
+ const node = (
26
+ <>
27
+ {await ampless.renderBody(draft)}
28
+ {await ampless.publicPostScriptsForPage([draft])}
29
+ </>
30
+ )
31
+ return renderToStaticMarkup(node)
32
+ }
@@ -0,0 +1,21 @@
1
+ 'use client'
2
+
3
+ // Phase 7 admin-editor extension slot. Templates wire first-party
4
+ // embed plugins' tiptap Node extensions here so the admin's
5
+ // <TiptapEditor> picks them up. Empty by default — uncomment the
6
+ // imports + the registration to enable YouTube / x.com embeds.
7
+ //
8
+ // Wired into the admin layout via:
9
+ // createAdminLayout(admin, { editorBootstrap: EditorBootstrap })
10
+
11
+ import { installAdminEditorExtensions } from '@ampless/admin/editor'
12
+ // import { youtubeEditor } from '@ampless/plugin-youtube/editor'
13
+ // import { tweetEditor } from '@ampless/plugin-x-embed/editor'
14
+
15
+ export function EditorBootstrap({ children }: { children: React.ReactNode }) {
16
+ installAdminEditorExtensions([
17
+ // youtubeEditor.extension,
18
+ // tweetEditor.extension,
19
+ ])
20
+ return <>{children}</>
21
+ }
@@ -1,4 +1,5 @@
1
1
  import { admin } from '@/lib/admin'
2
2
  import { createAdminLayout } from '@ampless/admin/pages'
3
+ import { EditorBootstrap } from './_editor-bootstrap'
3
4
 
4
- export default createAdminLayout(admin)
5
+ export default createAdminLayout(admin, { editorBootstrap: EditorBootstrap })
@@ -1,4 +1,5 @@
1
1
  import { admin } from '@/lib/admin'
2
2
  import { createEditPostPage } from '@ampless/admin/pages'
3
+ import { renderPreviewHtml } from '../../_actions/render-preview'
3
4
 
4
- export default createEditPostPage(admin)
5
+ export default createEditPostPage(admin, { renderPreviewAction: renderPreviewHtml })
@@ -1,4 +1,5 @@
1
1
  import { admin } from '@/lib/admin'
2
2
  import { createNewPostPage } from '@ampless/admin/pages'
3
+ import { renderPreviewHtml } from '../../_actions/render-preview'
3
4
 
4
- export default createNewPostPage(admin)
5
+ export default createNewPostPage(admin, { renderPreviewAction: renderPreviewHtml })
@@ -546,7 +546,7 @@ export default function readingTimePlugin() {
546
546
  ```tsx
547
547
  {postBody} {/* publicBodyForPost — JSON-LD */}
548
548
  {html.beforeContent} {/* publicHtmlForPost — beforeContent スロット */}
549
- <div className="prose" dangerouslySetInnerHTML={{ __html: renderBody(post) }} />
549
+ <div className="prose">{await ampless.renderBody(post)}</div>
550
550
  {html.afterContent} {/* publicHtmlForPost — afterContent スロット */}
551
551
  ```
552
552
 
@@ -684,6 +684,166 @@ npm 公開のスタンドアロンプラグインの場合は、`package.json#am
684
684
 
685
685
  ---
686
686
 
687
+ ## 6b. 投稿本文の差し替え: `contentFields` (Phase 7)
688
+
689
+ `contentFields` capability は、投稿本文の一部(tiptap ノード、または markdown の単独行 URL)を、プラグインが管理する React サブツリーで差し替える機能。`@ampless/plugin-youtube` / `@ampless/plugin-x-embed` が `https://youtu.be/...` URL を iframe プレーヤーに、`https://x.com/<handle>/status/...` URL を tweet blockquote に展開するために使う。
690
+
691
+ ### 形
692
+
693
+ ```ts
694
+ import { definePlugin, type ContentFieldRenderer } from 'ampless'
695
+
696
+ definePlugin({
697
+ // ...
698
+ capabilities: ['contentFields'],
699
+ contentFields: [
700
+ {
701
+ kind: 'tiptap',
702
+ nodeType: 'amplessYoutube',
703
+ render: (node, ctx) => <YouTubeEmbed videoId={String(node.attrs?.videoId)} />,
704
+ },
705
+ {
706
+ kind: 'markdown-url',
707
+ pattern: /^https:\/\/youtu\.be\/([\w-]{11})$/,
708
+ render: ({ match }, ctx) => <YouTubeEmbed videoId={match[1]!} />,
709
+ },
710
+ ],
711
+ })
712
+ ```
713
+
714
+ 各 renderer は `ampless.renderBody(post)` 内で本文を歩く runtime から server-side で呼び出される。戻り値は `ReactNode`。`PluginPublicRenderContext` (`ctx`) は `publicHead` / `publicBodyEnd` と同じものなので、`ctx.setting<T>(key)` も同じように使える。
715
+
716
+ ### 2 種類の kind
717
+
718
+ - **`tiptap`** — `nodeType` (例: `'amplessYoutube'`) でキー付け。runtime の tiptap walker が `type` 一致のノードを見つけたら renderer を呼ぶ。デフォルトの switch-case レンダリングはバイパスされ、プラグインがサブツリー全体を所有する。
719
+ - **`markdown-url`** — anchored な `RegExp` (`^...$`) でキー付け。runtime は `marked.lexer` で markdown をトークン化し、内容全体が単独 URL の paragraph トークン (autolink / bare URL / `[text](url)`) のみについて pattern を試す。最初にマッチしたものが勝ち。capture group は `match[1]`, `match[2]`, ... でアクセス可能。
720
+
721
+ ### 命名と一意性
722
+
723
+ - first-party プラグインは `ampless...` の camelCase プレフィックス(`amplessYoutube`, `amplessTweet` 等)を使い、コミュニティ製の `nodeType` と衝突しないようにしている。
724
+ - runtime は同じ `nodeType` / `pattern.source` の重複登録を起動時に throw で拒否する。先勝ち。v1 は multi-instance 非対応。
725
+
726
+ ### markdown URL pattern のルール
727
+
728
+ - **必ず `^...$` で anchor する**。anchor なしだと段落内の URL が誤マッチし、周辺テキストが壊れる。
729
+ - runtime はマッチ前に前後の whitespace を trim するので、pattern 側で `\s*` を書く必要はない。
730
+ - 段落の唯一のトークンが `[caption](url)` の markdown link なら受理する。`[caption with link](url)` のように前後にテキストが混在するケースは対象外(正しい挙動: prose は本文に残すべきで、video embed にすべきではない)。
731
+
732
+ ---
733
+
734
+ ## 6c. ページレベルスクリプト: `publicPostScript` (Phase 7)
735
+
736
+ `publicPostScript` capability は、ページ上の投稿が必要とする `<script>` タグをプラグインから出力するためのもの。runtime は安定 `id` で dedupe するので、同一ページ内の複数 embed は 1 つの script タグに集約される。`@ampless/plugin-x-embed` がページに tweet embed がある場合のみ `https://platform.twitter.com/widgets.js` を 1 度だけ注入するのに使われている。
737
+
738
+ ### 形
739
+
740
+ ```ts
741
+ definePlugin({
742
+ capabilities: ['contentFields', 'publicPostScript'],
743
+ publicPostScript(post, ctx) {
744
+ if (!hasTweetIn(post)) return []
745
+ return [
746
+ {
747
+ id: 'amplessTweet:widgets',
748
+ src: 'https://platform.twitter.com/widgets.js',
749
+ async: true,
750
+ },
751
+ ]
752
+ },
753
+ })
754
+ ```
755
+
756
+ ### テーマからの呼び出し
757
+
758
+ テーマは投稿本文の出力後に `{await ampless.publicPostScriptsForPage(posts)}` を呼ぶ。first-party テーマは post 詳細ページ / home ページ (featured 表示時) で自動的に呼び出す:
759
+
760
+ ```tsx
761
+ <div>{await ampless.renderBody(post)}</div>
762
+ {await ampless.publicPostScriptsForPage([post])}
763
+ ```
764
+
765
+ runtime は:
766
+
767
+ 1. プラグイン × post 組ごとに `publicPostScript(post, ctx)` を呼ぶ。
768
+ 2. `id` が空 / 非 string、`src` が http(s) でない、descriptor がオブジェクトでない、等を drop。
769
+ 3. `id` で dedupe(先勝ち)。
770
+ 4. `<Fragment>` で `<script src={src} async defer />` を出力。
771
+
772
+ ### CSP の扱い
773
+
774
+ runtime は `src` のホスト allowlist を強制しない。CSP はサイト側エンジニアの責任(`next.config.ts` / middleware で `script-src` に script ホスト(例: `platform.twitter.com`)を追加)。各プラグインの README に書いてある。
775
+
776
+ ---
777
+
778
+ ## 6d. Admin エディタ拡張の配線 (Phase 7)
779
+
780
+ admin エディタに tiptap Node 拡張を提供するプラグインは、別途 `./editor` subpath の client-side エントリを出荷する。テンプレート側で `_editor-bootstrap.tsx` から配線する:
781
+
782
+ ```tsx
783
+ // templates/_shared/app/(admin)/admin/_editor-bootstrap.tsx
784
+ 'use client'
785
+ import { installAdminEditorExtensions } from '@ampless/admin/editor'
786
+ import { youtubeEditor } from '@ampless/plugin-youtube/editor'
787
+ import { tweetEditor } from '@ampless/plugin-x-embed/editor'
788
+
789
+ export function EditorBootstrap({ children }: { children: React.ReactNode }) {
790
+ installAdminEditorExtensions([
791
+ youtubeEditor.extension,
792
+ tweetEditor.extension,
793
+ ])
794
+ return <>{children}</>
795
+ }
796
+ ```
797
+
798
+ その上で layout に渡す:
799
+
800
+ ```tsx
801
+ // templates/_shared/app/(admin)/admin/layout.tsx
802
+ import { createAdminLayout } from '@ampless/admin/pages'
803
+ import { EditorBootstrap } from './_editor-bootstrap'
804
+ export default createAdminLayout(admin, { editorBootstrap: EditorBootstrap })
805
+ ```
806
+
807
+ `installAdminEditorExtensions` は idempotent で、render 時に client component 内で呼ばれる。admin の `<TiptapEditor>` は毎回 render 時に登録済みリストを built-in extensions の末尾に spread する。
808
+
809
+ ### エディタプレビューのパイプライン
810
+
811
+ admin の edit / new post フォームは preview ペインを `<iframe sandbox="allow-scripts">` で表示する。`srcDoc` はテンプレート側 server action が返す HTML:
812
+
813
+ ```tsx
814
+ // templates/_shared/app/(admin)/admin/_actions/render-preview.tsx
815
+ 'use server'
816
+ import { renderToStaticMarkup } from 'react-dom/server'
817
+ import type { Post } from 'ampless'
818
+ import { admin } from '@/lib/admin'
819
+
820
+ export async function renderPreviewHtml(draft: Post): Promise<string> {
821
+ const ampless = await admin.getAmpless()
822
+ const node = (
823
+ <>
824
+ {await ampless.renderBody(draft)}
825
+ {await ampless.publicPostScriptsForPage([draft])}
826
+ </>
827
+ )
828
+ return renderToStaticMarkup(node)
829
+ }
830
+ ```
831
+
832
+ page factory は `renderPreviewAction` オプションでこの action を thread する:
833
+
834
+ ```tsx
835
+ // templates/_shared/app/(admin)/admin/posts/[postId]/page.tsx
836
+ import { admin } from '@/lib/admin'
837
+ import { createEditPostPage } from '@ampless/admin/pages'
838
+ import { renderPreviewHtml } from '../../_actions/render-preview'
839
+
840
+ export default createEditPostPage(admin, { renderPreviewAction: renderPreviewHtml })
841
+ ```
842
+
843
+ 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
+
845
+ ---
846
+
687
847
  ## 7. 非同期イベントフック
688
848
 
689
849
  `hooks` は SQS から到着したイベントを trust_level に対応する processor Lambda が受けて実行します。
@@ -678,7 +678,7 @@ the slots:
678
678
  ```tsx
679
679
  {postBody} {/* publicBodyForPost — JSON-LD */}
680
680
  {html.beforeContent} {/* publicHtmlForPost — beforeContent slot */}
681
- <div className="prose" dangerouslySetInnerHTML={{ __html: renderBody(post) }} />
681
+ <div className="prose">{await ampless.renderBody(post)}</div>
682
682
  {html.afterContent} {/* publicHtmlForPost — afterContent slot */}
683
683
  ```
684
684
 
@@ -894,6 +894,218 @@ manifest and the factory return value:
894
894
 
895
895
  ---
896
896
 
897
+ ## 6b. In-body content renderers: `contentFields` (Phase 7)
898
+
899
+ The `contentFields` capability lets a plugin replace specific fragments
900
+ of a post body — tiptap nodes or single-line markdown URLs — with a
901
+ React subtree it controls. Used by the first-party `@ampless/plugin-youtube`
902
+ and `@ampless/plugin-x-embed` packages to expand `https://youtu.be/...`
903
+ URLs into iframe players and `https://x.com/<handle>/status/...` URLs
904
+ into tweet blockquotes.
905
+
906
+ ### Shape
907
+
908
+ ```ts
909
+ import { definePlugin, type ContentFieldRenderer } from 'ampless'
910
+
911
+ definePlugin({
912
+ // ...
913
+ capabilities: ['contentFields'],
914
+ contentFields: [
915
+ {
916
+ kind: 'tiptap',
917
+ nodeType: 'amplessYoutube',
918
+ render: (node, ctx) => <YouTubeEmbed videoId={String(node.attrs?.videoId)} />,
919
+ },
920
+ {
921
+ kind: 'markdown-url',
922
+ pattern: /^https:\/\/youtu\.be\/([\w-]{11})$/,
923
+ render: ({ match }, ctx) => <YouTubeEmbed videoId={match[1]!} />,
924
+ },
925
+ ],
926
+ })
927
+ ```
928
+
929
+ Each renderer is called server-side by the runtime when it walks a post
930
+ body during `ampless.renderBody(post)`. The return value must be a
931
+ `ReactNode`. The plugin's `PluginPublicRenderContext` (`ctx`) is the
932
+ same context handed to `publicHead` / `publicBodyEnd`, so `ctx.setting<T>(key)`
933
+ works exactly the same way.
934
+
935
+ ### The two kinds
936
+
937
+ - **`tiptap`** — keyed by `nodeType` (a string, e.g. `'amplessYoutube'`).
938
+ The runtime's tiptap walker calls the renderer whenever it encounters
939
+ a node whose `type` matches `nodeType`. Default switch-case rendering
940
+ is bypassed; the plugin owns the subtree.
941
+ - **`markdown-url`** — keyed by an anchored `RegExp` (`^...$`). The
942
+ runtime tokenizes markdown with `marked.lexer` and, for any
943
+ `paragraph` token whose entire content is a single URL (autolink,
944
+ bare URL, or `[text](url)` markdown link), tests the URL against
945
+ each registered pattern. The first match wins; capture groups are
946
+ exposed via `match[1]`, `match[2]`, etc.
947
+
948
+ ### Naming and uniqueness
949
+
950
+ - First-party plugins use the `ampless...` camelCase prefix
951
+ (`amplessYoutube`, `amplessTweet`) so the namespace stays clear of
952
+ community-contributed `nodeType`s.
953
+ - The runtime rejects duplicate `nodeType` / `pattern.source` at
954
+ startup with a thrown error. The first plugin to register a given key
955
+ wins; the second one fails fast. Multi-instance v1 is not supported.
956
+
957
+ ### Markdown URL pattern rules
958
+
959
+ - **Always anchor with `^...$`** so the pattern only matches paragraphs
960
+ whose entire content is a single URL. A pattern without anchors would
961
+ match URLs embedded mid-paragraph and break the surrounding text.
962
+ - The runtime trims leading/trailing whitespace before matching, so
963
+ patterns don't need to account for `\s*` around the URL.
964
+ - Inline `[caption](url)` markdown links are accepted when the link is
965
+ the paragraph's only token. `[caption with link](url)` mixed with
966
+ surrounding text is NOT matched (correct behaviour: the prose
967
+ belongs in the post, not a video embed).
968
+
969
+ ---
970
+
971
+ ## 6c. Page-level scripts: `publicPostScript` (Phase 7)
972
+
973
+ The `publicPostScript` capability lets a plugin emit a `<script>` tag
974
+ that any post on the page needs. The runtime dedupes by stable `id`
975
+ so multiple embeds in one or several posts collapse to one script tag.
976
+ Used by `@ampless/plugin-x-embed` to inject
977
+ `https://platform.twitter.com/widgets.js` once per page that has any
978
+ tweet embed.
979
+
980
+ ### Shape
981
+
982
+ ```ts
983
+ definePlugin({
984
+ capabilities: ['contentFields', 'publicPostScript'],
985
+ publicPostScript(post, ctx) {
986
+ if (!hasTweetIn(post)) return []
987
+ return [
988
+ {
989
+ id: 'amplessTweet:widgets',
990
+ src: 'https://platform.twitter.com/widgets.js',
991
+ async: true,
992
+ },
993
+ ]
994
+ },
995
+ })
996
+ ```
997
+
998
+ ### Theme integration
999
+
1000
+ Themes call `{await ampless.publicPostScriptsForPage(posts)}` after
1001
+ rendering each post body. First-party themes do this automatically in
1002
+ their post detail page and home page (when a featured post is shown):
1003
+
1004
+ ```tsx
1005
+ <div>{await ampless.renderBody(post)}</div>
1006
+ {await ampless.publicPostScriptsForPage([post])}
1007
+ ```
1008
+
1009
+ The runtime:
1010
+
1011
+ 1. Calls `publicPostScript(post, ctx)` for each plugin × post pair.
1012
+ 2. Drops descriptors with empty/non-string `id`, with `src` that fails
1013
+ the http(s) allowlist, or that aren't objects.
1014
+ 3. Dedupes by `id` (first arrival wins).
1015
+ 4. Emits a `<Fragment>` of `<script src={src} async defer />` elements.
1016
+
1017
+ ### CSP considerations
1018
+
1019
+ The runtime does **not** enforce a host allowlist on `src`. CSP is the
1020
+ site engineer's responsibility — add the script host (e.g.
1021
+ `platform.twitter.com`) to `script-src` in `next.config.ts` /
1022
+ middleware. This is documented in each plugin's README.
1023
+
1024
+ ---
1025
+
1026
+ ## 6d. Admin editor extension wiring (Phase 7)
1027
+
1028
+ Plugins that contribute tiptap Node extensions to the admin editor
1029
+ ship a separate client-side entry under the `./editor` subpath. The
1030
+ template wires it up in `_editor-bootstrap.tsx`:
1031
+
1032
+ ```tsx
1033
+ // templates/_shared/app/(admin)/admin/_editor-bootstrap.tsx
1034
+ 'use client'
1035
+ import { installAdminEditorExtensions } from '@ampless/admin/editor'
1036
+ import { youtubeEditor } from '@ampless/plugin-youtube/editor'
1037
+ import { tweetEditor } from '@ampless/plugin-x-embed/editor'
1038
+
1039
+ export function EditorBootstrap({ children }: { children: React.ReactNode }) {
1040
+ installAdminEditorExtensions([
1041
+ youtubeEditor.extension,
1042
+ tweetEditor.extension,
1043
+ ])
1044
+ return <>{children}</>
1045
+ }
1046
+ ```
1047
+
1048
+ Then thread it into the layout:
1049
+
1050
+ ```tsx
1051
+ // templates/_shared/app/(admin)/admin/layout.tsx
1052
+ import { createAdminLayout } from '@ampless/admin/pages'
1053
+ import { EditorBootstrap } from './_editor-bootstrap'
1054
+ export default createAdminLayout(admin, { editorBootstrap: EditorBootstrap })
1055
+ ```
1056
+
1057
+ `installAdminEditorExtensions` is idempotent and runs at render time
1058
+ inside a client component. The admin's `<TiptapEditor>` spreads the
1059
+ registered list onto its built-in extensions on every render.
1060
+
1061
+ ### Editor preview pipeline
1062
+
1063
+ The admin's edit / new post forms render the preview pane in an
1064
+ `<iframe sandbox="allow-scripts">` whose `srcDoc` is the HTML returned
1065
+ by the template's server action:
1066
+
1067
+ ```tsx
1068
+ // templates/_shared/app/(admin)/admin/_actions/render-preview.tsx
1069
+ 'use server'
1070
+ import { renderToStaticMarkup } from 'react-dom/server'
1071
+ import type { Post } from 'ampless'
1072
+ import { admin } from '@/lib/admin'
1073
+
1074
+ export async function renderPreviewHtml(draft: Post): Promise<string> {
1075
+ const ampless = await admin.getAmpless()
1076
+ const node = (
1077
+ <>
1078
+ {await ampless.renderBody(draft)}
1079
+ {await ampless.publicPostScriptsForPage([draft])}
1080
+ </>
1081
+ )
1082
+ return renderToStaticMarkup(node)
1083
+ }
1084
+ ```
1085
+
1086
+ Page factories thread the action down via the `renderPreviewAction`
1087
+ option:
1088
+
1089
+ ```tsx
1090
+ // templates/_shared/app/(admin)/admin/posts/[postId]/page.tsx
1091
+ import { admin } from '@/lib/admin'
1092
+ import { createEditPostPage } from '@ampless/admin/pages'
1093
+ import { renderPreviewHtml } from '../../_actions/render-preview'
1094
+
1095
+ export default createEditPostPage(admin, { renderPreviewAction: renderPreviewHtml })
1096
+ ```
1097
+
1098
+ The iframe's `sandbox="allow-scripts"` (without `allow-same-origin`)
1099
+ keeps preview content in an opaque origin so widget scripts cannot
1100
+ reach the admin's DOM. The trade-off is that some widgets may misbehave
1101
+ when they can't access their own origin's storage; the YouTube iframe
1102
+ embed and x.com widgets.js both work under these constraints in
1103
+ dogfood as of Phase 7. If a future widget needs a non-opaque preview
1104
+ origin, the escape hatch is a separate preview route on a different
1105
+ subdomain with appropriate CSP — not relaxing the sandbox flag.
1106
+
1107
+ ---
1108
+
897
1109
  ## 7. Async event hooks
898
1110
 
899
1111
  `hooks` runs inside the trust_level-matched processor Lambda when an
@@ -25,21 +25,21 @@
25
25
  "@tiptap/pm": "^3.23.6",
26
26
  "@tiptap/react": "^3.23.6",
27
27
  "@tiptap/starter-kit": "^3.23.6",
28
- "@ampless/plugin-analytics-ga4": "^0.2.0-alpha.25",
29
- "@ampless/plugin-cookie-consent": "^0.1.0-alpha.16",
30
- "@ampless/plugin-gtm": "^0.2.0-alpha.24",
31
- "@ampless/plugin-og-image": "^0.2.0-alpha.41",
32
- "@ampless/plugin-plausible": "^0.2.0-alpha.24",
33
- "@ampless/plugin-reading-time": "^0.1.0-alpha.14",
34
- "@ampless/plugin-rss": "^0.2.0-alpha.41",
35
- "@ampless/plugin-schema-jsonld": "^0.1.1-alpha.20",
36
- "@ampless/plugin-seo": "^0.2.0-alpha.41",
37
- "@ampless/plugin-webhook": "^0.2.0-alpha.42",
38
- "@ampless/admin": "^1.0.0-alpha.74",
39
- "@ampless/backend": "^1.0.0-alpha.63",
40
- "@ampless/runtime": "^1.0.0-alpha.50",
28
+ "@ampless/plugin-analytics-ga4": "^0.2.0-alpha.26",
29
+ "@ampless/plugin-cookie-consent": "^0.1.0-alpha.17",
30
+ "@ampless/plugin-gtm": "^0.2.0-alpha.25",
31
+ "@ampless/plugin-og-image": "^0.2.0-alpha.42",
32
+ "@ampless/plugin-plausible": "^0.2.0-alpha.25",
33
+ "@ampless/plugin-reading-time": "^0.1.0-alpha.15",
34
+ "@ampless/plugin-rss": "^0.2.0-alpha.42",
35
+ "@ampless/plugin-schema-jsonld": "^0.1.1-alpha.21",
36
+ "@ampless/plugin-seo": "^0.2.0-alpha.42",
37
+ "@ampless/plugin-webhook": "^0.2.0-alpha.43",
38
+ "@ampless/admin": "^1.0.0-alpha.75",
39
+ "@ampless/backend": "^1.0.0-alpha.64",
40
+ "@ampless/runtime": "^1.0.0-alpha.51",
41
41
  "@digital-go-jp/tailwind-theme-plugin": "^0.3.4",
42
- "ampless": "^1.0.0-alpha.41",
42
+ "ampless": "^1.0.0-alpha.42",
43
43
  "aws-amplify": "^6.17.0",
44
44
  "class-variance-authority": "^0.7.1",
45
45
  "clsx": "^2.1.1",
@@ -1,6 +1,5 @@
1
1
  import Link from 'next/link'
2
2
  import { formatDate, parseLinkList, type ThemeRouteContext } from 'ampless'
3
- import { renderBody } from '@ampless/runtime'
4
3
  import { ampless } from '@/lib/ampless'
5
4
  import { admin } from '@/lib/admin'
6
5
  import { TagList } from '@/components/tag-list'
@@ -59,13 +58,14 @@ export default async function BlogHome(_: ThemeRouteContext) {
59
58
  </time>
60
59
  )}
61
60
  </Link>
62
- <div
63
- className="prose prose-neutral dark:prose-invert mt-4 max-w-none"
64
- dangerouslySetInnerHTML={{ __html: renderBody(featured) }}
65
- />
61
+ <div className="prose prose-neutral dark:prose-invert mt-4 max-w-none">
62
+ {await ampless.renderBody(featured)}
63
+ </div>
66
64
  </article>
67
65
  )}
68
66
 
67
+ {featured && (await ampless.publicPostScriptsForPage([featured]))}
68
+
69
69
  {posts.length === 0 ? (
70
70
  !featured && <p className="text-gray-500">{admin.t('public.noPosts')}</p>
71
71
  ) : (
@@ -2,7 +2,6 @@ import type { Metadata } from 'next'
2
2
  import Link from 'next/link'
3
3
  import { notFound } from 'next/navigation'
4
4
  import { formatDate, parseLinkList, type ThemeRouteContext } from 'ampless'
5
- import { renderBody } from '@ampless/runtime'
6
5
  import { ampless } from '@/lib/ampless'
7
6
  import { admin } from '@/lib/admin'
8
7
  import { LightboxBinder } from '@/components/lightbox-content'
@@ -86,11 +85,14 @@ export default async function BlogPost({ params }: PostCtx) {
86
85
  id="post-body"
87
86
  className="prose prose-neutral dark:prose-invert max-w-none [&_img]:max-w-[var(--ampless-img-max-width)] [&_img]:mx-auto"
88
87
  style={proseStyle}
89
- dangerouslySetInnerHTML={{ __html: renderBody(post) }}
90
- />
88
+ >
89
+ {await ampless.renderBody(post)}
90
+ </div>
91
91
 
92
92
  {html.afterContent}
93
93
 
94
+ {await ampless.publicPostScriptsForPage([post])}
95
+
94
96
  <TagList tags={post.tags} className="mt-8 border-t pt-6" />
95
97
 
96
98
  <footer className="mt-6 flex flex-wrap items-center gap-x-6 gap-y-2 text-sm text-gray-500">
@@ -1,6 +1,5 @@
1
1
  import Link from 'next/link'
2
2
  import { formatDate, type ThemeRouteContext } from 'ampless'
3
- import { renderBody } from '@ampless/runtime'
4
3
  import { ampless } from '@/lib/ampless'
5
4
  import { SiteHeader } from '@/components/site-chrome/site-header'
6
5
  import { SiteFooter } from '@/components/site-chrome/site-footer'
@@ -70,14 +69,15 @@ export default async function CorporateHome(_: ThemeRouteContext) {
70
69
  {formatDate(featured.publishedAt, settings.dateFormat, settings.timezone)}
71
70
  </time>
72
71
  )}
73
- <div
74
- className="prose prose-neutral dark:prose-invert mt-6 max-w-none"
75
- dangerouslySetInnerHTML={{ __html: renderBody(featured) }}
76
- />
72
+ <div className="prose prose-neutral dark:prose-invert mt-6 max-w-none">
73
+ {await ampless.renderBody(featured)}
74
+ </div>
77
75
  </article>
78
76
  </section>
79
77
  )}
80
78
 
79
+ {featured && (await ampless.publicPostScriptsForPage([featured]))}
80
+
81
81
  {posts.length > 0 && (
82
82
  <section className="mx-auto max-w-4xl px-6 py-16">
83
83
  <h2 className="mb-8 border-l-4 border-[var(--primary)] pl-4 text-2xl font-bold">
@@ -2,7 +2,6 @@ import type { Metadata } from 'next'
2
2
  import Link from 'next/link'
3
3
  import { notFound } from 'next/navigation'
4
4
  import { formatDate, type ThemeRouteContext } from 'ampless'
5
- import { renderBody } from '@ampless/runtime'
6
5
  import { ampless } from '@/lib/ampless'
7
6
  import { admin } from '@/lib/admin'
8
7
  import { LightboxBinder } from '@/components/lightbox-content'
@@ -75,11 +74,14 @@ export default async function CorporatePost({ params }: PostCtx) {
75
74
  id="post-body"
76
75
  className="prose prose-neutral dark:prose-invert max-w-none [&_img]:max-w-[var(--ampless-img-max-width)] [&_img]:mx-auto"
77
76
  style={proseStyle}
78
- dangerouslySetInnerHTML={{ __html: renderBody(post) }}
79
- />
77
+ >
78
+ {await ampless.renderBody(post)}
79
+ </div>
80
80
 
81
81
  {html.afterContent}
82
82
 
83
+ {await ampless.publicPostScriptsForPage([post])}
84
+
83
85
  <TagList tags={post.tags} className="mt-8 border-t pt-6" />
84
86
  </article>
85
87
 
@@ -1,6 +1,5 @@
1
1
  import Link from 'next/link'
2
2
  import { formatDate, type ThemeRouteContext } from 'ampless'
3
- import { renderBody } from '@ampless/runtime'
4
3
  import { ampless } from '@/lib/ampless'
5
4
  import { SiteHeader } from '@/components/site-chrome/site-header'
6
5
  import { SiteFooter } from '@/components/site-chrome/site-footer'
@@ -63,14 +62,15 @@ export default async function DadsHome(_: ThemeRouteContext) {
63
62
  {featured.title}
64
63
  </Link>
65
64
  </h2>
66
- <div
67
- className="prose prose-neutral dark:prose-invert mt-6 max-w-none"
68
- dangerouslySetInnerHTML={{ __html: renderBody(featured) }}
69
- />
65
+ <div className="prose prose-neutral dark:prose-invert mt-6 max-w-none">
66
+ {await ampless.renderBody(featured)}
67
+ </div>
70
68
  </div>
71
69
  </section>
72
70
  )}
73
71
 
72
+ {featured && (await ampless.publicPostScriptsForPage([featured]))}
73
+
74
74
  {posts.length > 0 && (
75
75
  <section className="px-6 py-12">
76
76
  <div className="mx-auto max-w-4xl">
@@ -2,7 +2,6 @@ import type { Metadata } from 'next'
2
2
  import Link from 'next/link'
3
3
  import { notFound } from 'next/navigation'
4
4
  import { formatDate, type ThemeRouteContext } from 'ampless'
5
- import { renderBody } from '@ampless/runtime'
6
5
  import { ampless } from '@/lib/ampless'
7
6
  import { admin } from '@/lib/admin'
8
7
  import { LightboxBinder } from '@/components/lightbox-content'
@@ -80,11 +79,14 @@ export default async function DadsPost({ params }: PostCtx) {
80
79
  id="post-body"
81
80
  className="prose prose-neutral dark:prose-invert max-w-none [&_a]:text-[var(--primary)] [&_a]:underline-offset-4 [&_img]:max-w-[var(--ampless-img-max-width)] [&_img]:mx-auto"
82
81
  style={proseStyle}
83
- dangerouslySetInnerHTML={{ __html: renderBody(post) }}
84
- />
82
+ >
83
+ {await ampless.renderBody(post)}
84
+ </div>
85
85
 
86
86
  {html.afterContent}
87
87
 
88
+ {await ampless.publicPostScriptsForPage([post])}
89
+
88
90
  <TagList tags={post.tags} className="mt-10 border-t pt-6" />
89
91
  </article>
90
92
 
@@ -2,7 +2,6 @@ import type { Metadata } from 'next'
2
2
  import Link from 'next/link'
3
3
  import { notFound } from 'next/navigation'
4
4
  import { formatDate, type ThemeRouteContext } from 'ampless'
5
- import { renderBody } from '@ampless/runtime'
6
5
  import { ampless } from '@/lib/ampless'
7
6
  import { LightboxBinder } from '@/components/lightbox-content'
8
7
  import { TagList } from '@/components/tag-list'
@@ -77,11 +76,14 @@ export default async function DocsPost({ params }: PostCtx) {
77
76
  id="post-body"
78
77
  className="prose prose-neutral dark:prose-invert max-w-none [&_img]:max-w-[var(--ampless-img-max-width)] [&_img]:mx-auto"
79
78
  style={proseStyle}
80
- dangerouslySetInnerHTML={{ __html: renderBody(post) }}
81
- />
79
+ >
80
+ {await ampless.renderBody(post)}
81
+ </div>
82
82
 
83
83
  {html.afterContent}
84
84
 
85
+ {await ampless.publicPostScriptsForPage([post])}
86
+
85
87
  <TagList tags={post.tags} className="mt-10 border-t pt-6" />
86
88
  </article>
87
89
  </main>
@@ -1,6 +1,5 @@
1
1
  import Link from 'next/link'
2
2
  import { formatDate, type ThemeRouteContext } from 'ampless'
3
- import { renderBody } from '@ampless/runtime'
4
3
  import { ampless } from '@/lib/ampless'
5
4
  import { SiteHeader } from '@/components/site-chrome/site-header'
6
5
  import { SiteFooter } from '@/components/site-chrome/site-footer'
@@ -63,14 +62,15 @@ export default async function LandingHome(_: ThemeRouteContext) {
63
62
  <section className="mx-auto max-w-3xl px-6 py-16">
64
63
  <article>
65
64
  <h2 className="text-3xl font-bold tracking-tight">{featured.title}</h2>
66
- <div
67
- className="prose prose-neutral dark:prose-invert mt-6 max-w-none"
68
- dangerouslySetInnerHTML={{ __html: renderBody(featured) }}
69
- />
65
+ <div className="prose prose-neutral dark:prose-invert mt-6 max-w-none">
66
+ {await ampless.renderBody(featured)}
67
+ </div>
70
68
  </article>
71
69
  </section>
72
70
  )}
73
71
 
72
+ {featured && (await ampless.publicPostScriptsForPage([featured]))}
73
+
74
74
  {posts.length > 0 && (
75
75
  <section className="mx-auto max-w-5xl px-6 py-16">
76
76
  <h2 className="mb-8 text-3xl font-bold">Latest</h2>
@@ -2,7 +2,6 @@ import type { Metadata } from 'next'
2
2
  import Link from 'next/link'
3
3
  import { notFound } from 'next/navigation'
4
4
  import { formatDate, type ThemeRouteContext } from 'ampless'
5
- import { renderBody } from '@ampless/runtime'
6
5
  import { ampless } from '@/lib/ampless'
7
6
  import { admin } from '@/lib/admin'
8
7
  import { LightboxBinder } from '@/components/lightbox-content'
@@ -77,11 +76,14 @@ export default async function LandingPost({ params }: PostCtx) {
77
76
  id="post-body"
78
77
  className="prose prose-neutral dark:prose-invert max-w-none [&_img]:max-w-[var(--ampless-img-max-width)] [&_img]:mx-auto"
79
78
  style={proseStyle}
80
- dangerouslySetInnerHTML={{ __html: renderBody(post) }}
81
- />
79
+ >
80
+ {await ampless.renderBody(post)}
81
+ </div>
82
82
 
83
83
  {html.afterContent}
84
84
 
85
+ {await ampless.publicPostScriptsForPage([post])}
86
+
85
87
  <TagList tags={post.tags} className="mt-10 border-t pt-6" />
86
88
  </article>
87
89
 
@@ -2,7 +2,6 @@ import type { Metadata } from 'next'
2
2
  import Link from 'next/link'
3
3
  import { notFound } from 'next/navigation'
4
4
  import { formatDate, type ThemeRouteContext } from 'ampless'
5
- import { renderBody } from '@ampless/runtime'
6
5
  import { ampless } from '@/lib/ampless'
7
6
  import { admin } from '@/lib/admin'
8
7
  import { LightboxBinder } from '@/components/lightbox-content'
@@ -58,11 +57,14 @@ export default async function MinimalPost({ params }: PostCtx) {
58
57
  id="post-body"
59
58
  className="prose prose-neutral dark:prose-invert max-w-none [&_img]:max-w-[var(--ampless-img-max-width)] [&_img]:mx-auto"
60
59
  style={proseStyle}
61
- dangerouslySetInnerHTML={{ __html: renderBody(post) }}
62
- />
60
+ >
61
+ {await ampless.renderBody(post)}
62
+ </div>
63
63
 
64
64
  {html.afterContent}
65
65
 
66
+ {await ampless.publicPostScriptsForPage([post])}
67
+
66
68
  <TagList tags={post.tags} className="mt-8 border-t pt-6" />
67
69
  </article>
68
70
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-ampless",
3
- "version": "1.0.0-alpha.126",
3
+ "version": "1.0.0-alpha.128",
4
4
  "description": "Create a new ampless project",
5
5
  "license": "MIT",
6
6
  "type": "module",