ampless 1.0.0-alpha.41 → 1.0.0-alpha.42
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/dist/index.d.ts +112 -2
- package/docs/plugin-author-guide.ja.md +161 -1
- package/docs/plugin-author-guide.md +213 -1
- package/package.json +12 -1
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { ReactNode } from 'react';
|
|
2
|
+
|
|
1
3
|
type ContentEventType = 'content.created' | 'content.updated' | 'content.published' | 'content.unpublished' | 'content.deleted';
|
|
2
4
|
type MediaEventType = 'media.uploaded' | 'media.deleted';
|
|
3
5
|
type SiteSettingsEventType = 'site.settings.updated';
|
|
@@ -302,7 +304,7 @@ type TrustLevel = 'untrusted' | 'trusted' | 'privileged';
|
|
|
302
304
|
* capability today is harmless, but the runtime won't expose any new
|
|
303
305
|
* surface for it until the matching phase ships.
|
|
304
306
|
*/
|
|
305
|
-
type PluginCapability = 'publicHead' | 'publicBody' | 'metadata' | 'eventHooks' | 'adminSettings' | 'writePublicAsset' | 'schema' | 'publicHtmlForPost' | 'secretSettings' | 'contentFields' | 'adminPage' | 'serverRoute' | 'network' | 'scheduler' | 'storageWrite' | 'privilegedSystem' | 'cspReady';
|
|
307
|
+
type PluginCapability = 'publicHead' | 'publicBody' | 'metadata' | 'eventHooks' | 'adminSettings' | 'writePublicAsset' | 'schema' | 'publicHtmlForPost' | 'secretSettings' | 'contentFields' | 'publicPostScript' | 'adminPage' | 'serverRoute' | 'network' | 'scheduler' | 'storageWrite' | 'privilegedSystem' | 'cspReady';
|
|
306
308
|
/**
|
|
307
309
|
* Loading strategy for `script` / `inlineScript` descriptors.
|
|
308
310
|
*
|
|
@@ -971,6 +973,80 @@ interface PluginSettingsManifest {
|
|
|
971
973
|
*/
|
|
972
974
|
version?: number;
|
|
973
975
|
}
|
|
976
|
+
/**
|
|
977
|
+
* Minimal tiptap node shape passed to a `contentFields` `tiptap` renderer.
|
|
978
|
+
* Mirrors the structural fields plugins typically read (`type`, `attrs`,
|
|
979
|
+
* `content`, `marks`, `text`) without coupling to a specific tiptap
|
|
980
|
+
* version. Plugins should treat unknown fields conservatively.
|
|
981
|
+
*/
|
|
982
|
+
interface TiptapRenderNode {
|
|
983
|
+
type: string;
|
|
984
|
+
attrs?: Record<string, unknown>;
|
|
985
|
+
content?: readonly TiptapRenderNode[];
|
|
986
|
+
marks?: readonly {
|
|
987
|
+
type: string;
|
|
988
|
+
attrs?: Record<string, unknown>;
|
|
989
|
+
}[];
|
|
990
|
+
text?: string;
|
|
991
|
+
}
|
|
992
|
+
/**
|
|
993
|
+
* Match object passed to a `contentFields` `markdown-url` renderer. The
|
|
994
|
+
* runtime walks the markdown body via `marked.lexer`, tests the trimmed
|
|
995
|
+
* single-line URL of each `paragraph` token against the registered
|
|
996
|
+
* pattern, and on match calls `render({ match, raw }, ctx)` so the
|
|
997
|
+
* plugin can extract capture groups (e.g. the YouTube video id) and
|
|
998
|
+
* return a ReactNode for that paragraph.
|
|
999
|
+
*/
|
|
1000
|
+
interface MarkdownEmbedMatch {
|
|
1001
|
+
match: RegExpMatchArray;
|
|
1002
|
+
raw: string;
|
|
1003
|
+
}
|
|
1004
|
+
/**
|
|
1005
|
+
* Renderer registered via `AmplessPlugin.contentFields`. Two variants:
|
|
1006
|
+
*
|
|
1007
|
+
* - `tiptap`: keyed by `nodeType` (e.g. `'amplessYoutube'`). The runtime
|
|
1008
|
+
* walks tiptap docs and, when the walker encounters a node whose
|
|
1009
|
+
* `type` matches `nodeType`, calls `render(node, ctx)` to produce a
|
|
1010
|
+
* ReactNode in place of the default switch-based HTML emission.
|
|
1011
|
+
*
|
|
1012
|
+
* - `markdown-url`: keyed by a fully-anchored `RegExp` (the pattern
|
|
1013
|
+
* should use `^...$`). The runtime tokenizes markdown via
|
|
1014
|
+
* `marked.lexer`, looks for `paragraph` tokens whose entire content
|
|
1015
|
+
* is a single URL (autolink / bare URL / `[text](url)` with matching
|
|
1016
|
+
* text), tests the URL against `pattern`, and on match calls
|
|
1017
|
+
* `render({ match, raw }, ctx)` for that paragraph.
|
|
1018
|
+
*
|
|
1019
|
+
* Each `nodeType` / `pattern.source` may be registered by at most one
|
|
1020
|
+
* plugin — duplicate registration throws at `createPluginHead` time.
|
|
1021
|
+
*/
|
|
1022
|
+
type ContentFieldRenderer = {
|
|
1023
|
+
kind: 'tiptap';
|
|
1024
|
+
nodeType: string;
|
|
1025
|
+
render(node: TiptapRenderNode, ctx: PluginPublicRenderContext): ReactNode;
|
|
1026
|
+
} | {
|
|
1027
|
+
kind: 'markdown-url';
|
|
1028
|
+
pattern: RegExp;
|
|
1029
|
+
render(m: MarkdownEmbedMatch, ctx: PluginPublicRenderContext): ReactNode;
|
|
1030
|
+
};
|
|
1031
|
+
/**
|
|
1032
|
+
* Descriptor returned by `AmplessPlugin.publicPostScript(post, ctx)`.
|
|
1033
|
+
* The runtime aggregates descriptors across every post passed to
|
|
1034
|
+
* `ampless.publicPostScriptsForPage(posts)` and emits each unique `id`
|
|
1035
|
+
* once as `<script src={src} async defer />`.
|
|
1036
|
+
*
|
|
1037
|
+
* `id` must be a stable, plugin-defined identifier (e.g.
|
|
1038
|
+
* `'amplessTweet:widgets'`). Multiple posts on the same page that all
|
|
1039
|
+
* need the same script collapse into a single tag.
|
|
1040
|
+
*
|
|
1041
|
+
* `src` must be an absolute `http://` or `https://` URL — relative
|
|
1042
|
+
* paths and other schemes are dropped with a warning.
|
|
1043
|
+
*/
|
|
1044
|
+
interface PublicPostScriptDescriptor {
|
|
1045
|
+
id: string;
|
|
1046
|
+
src: string;
|
|
1047
|
+
async?: boolean;
|
|
1048
|
+
defer?: boolean;
|
|
1049
|
+
}
|
|
974
1050
|
interface AmplessPlugin {
|
|
975
1051
|
name: string;
|
|
976
1052
|
/**
|
|
@@ -1152,6 +1228,40 @@ interface AmplessPlugin {
|
|
|
1152
1228
|
* capability.
|
|
1153
1229
|
*/
|
|
1154
1230
|
publicHtmlForPost?(post: Post, ctx: PluginPublicRenderContext): readonly PublicPostHtmlDescriptor[];
|
|
1231
|
+
/**
|
|
1232
|
+
* In-body content renderers (Phase 7 `contentFields` capability).
|
|
1233
|
+
* Plugins register tiptap node renderers (keyed by `nodeType`) and/or
|
|
1234
|
+
* markdown URL renderers (keyed by `RegExp`) here. The runtime walks
|
|
1235
|
+
* post bodies during `ampless.renderBody(post)` and, on match, calls
|
|
1236
|
+
* the renderer to obtain a `ReactNode` for that fragment in place of
|
|
1237
|
+
* the default HTML output.
|
|
1238
|
+
*
|
|
1239
|
+
* Duplicate `nodeType` / `pattern.source` across plugins throws at
|
|
1240
|
+
* `createPluginHead` construction time (eager config-time error). v1
|
|
1241
|
+
* does not support multi-instance plugins registering the same
|
|
1242
|
+
* renderer twice.
|
|
1243
|
+
*
|
|
1244
|
+
* Plugins implementing this should declare the `'contentFields'`
|
|
1245
|
+
* capability.
|
|
1246
|
+
*/
|
|
1247
|
+
contentFields?: readonly ContentFieldRenderer[];
|
|
1248
|
+
/**
|
|
1249
|
+
* Page-level script descriptors (Phase 7 `publicPostScript`
|
|
1250
|
+
* capability). Themes call `ampless.publicPostScriptsForPage(posts)`
|
|
1251
|
+
* after rendering post body / featured body; the runtime invokes
|
|
1252
|
+
* `publicPostScript(post, ctx)` for each plugin × post pair, collects
|
|
1253
|
+
* the descriptors, dedupes by `id`, and emits each unique entry as
|
|
1254
|
+
* `<script src={src} async defer />`.
|
|
1255
|
+
*
|
|
1256
|
+
* Primary use case: x.com (Twitter) `widgets.js` to hydrate
|
|
1257
|
+
* `<blockquote class="twitter-tweet">` blocks emitted by
|
|
1258
|
+
* `contentFields`. YouTube embeds don't need this (the iframe loads
|
|
1259
|
+
* its own script).
|
|
1260
|
+
*
|
|
1261
|
+
* Plugins implementing this should declare the `'publicPostScript'`
|
|
1262
|
+
* capability.
|
|
1263
|
+
*/
|
|
1264
|
+
publicPostScript?(post: Post, ctx: PluginPublicRenderContext): readonly PublicPostScriptDescriptor[];
|
|
1155
1265
|
/**
|
|
1156
1266
|
* Dynamic OG image renderer. The dispatcher route (e.g.
|
|
1157
1267
|
* `app/og/[slug]/route.ts`) reads this and feeds the element into
|
|
@@ -1766,4 +1876,4 @@ declare function pickDefaultEntrypoint(files: readonly {
|
|
|
1766
1876
|
|
|
1767
1877
|
declare const VERSION = "0.0.1";
|
|
1768
1878
|
|
|
1769
|
-
export { type AmplessEvent, type AmplessPlugin, type AuthContext, type BundleExtractResult, type CacheConfig, type CacheStrategy, type Config, type ContentEventPayload, type ContentEventType, type ContentFormat, type ContentTypeDefinition, type CreatePostInput, DEFAULT_ENTRYPOINT, type DateFormat, type EventPayloadOf, type EventType, type ExtractedFile, type FieldDefinition, type FieldType, type HistoryConfig, type ImageDisplay, type KvItem, type KvStore, type LinkListItem, type ListOptions, type ListPostHistoryOptions, type LocalizedString, MAX_BUNDLE_BYTES, type Media, type MediaEventPayload, type MediaEventType, type MediaMetadata, type MediaProcessingDefaults, type OgImageConfig, type OgImageFont, type OgImageRenderContext, PLUGIN_KEY_PATTERN, type Page, type PluginBooleanField, type PluginCapability, type PluginCodeField, type PluginEventHandler, type PluginHookResult, type PluginJsonField, type PluginMetadata, type PluginNumberField, type PluginPackageManifest, type PluginPublicRenderContext, type PluginRepeatableField, type PluginRepeatableSubField, type PluginRuntimeContext, type PluginSecretField, type PluginSelectField, type PluginSettingField, type PluginSettingsManifest, type PluginTextField, type PluginTextareaField, type PluginUninstallContext, type PluginUrlField, type Post, type PostIndexEventPayload, type PostIndexEventType, type PostMetadata, type PostRevision, type PostRevisionConnection, type PostStatus, type PostsProvider, type PublicBodyDescriptor, type PublicHeadDescriptor, type PublicPostBodyDescriptor, type PublicPostHtmlDescriptor, type PublicPostHtmlPosition, type Role, SITE_CONFIG_PK, type ScriptStrategy, type SiteSettingsEventPayload, type SiteSettingsEventType, type StaticPostBody, type StaticPostFileMeta, type StreamEventName, TEXT_EXTENSIONS, type ThemeColorField, type ThemeField, type ThemeFieldType, type ThemeFontFamilyField, type ThemeImageField, type ThemeLengthField, type ThemeLinkListField, type ThemeManifest, type ThemeModule, type ThemeRouteContext, type ThemeSelectField, type ThemeTextField, type TrustLevel, type TrustedPluginRuntimeContext, VERSION, type ValidationIssue, bundlePrefix, createPost, decodeAwsJson, defineConfig, definePlugin, defineSchema, defineTheme, defineThemeModule, deletePost, deleteSiteSetting, detectContentEvents, encodeAwsJson, escapeXml, extractFirstImageUrl, findAbsolutePathRefs, flattenSettings, formatColorPair, formatDate, formatPublicAssetUrl, getKvStore, getPost, getPostById, getSiteSetting, hasKvStore, hasPostsProvider, isTagListUrl, isValidPluginKey, listPostHistory, listPosts, listSiteSettings, mimeTypeFor, parseColorPair, parseLinkList, pickDefaultEntrypoint, resolveLocalized, resolvePluginSettings, resolveThemeValues, setKvStore, setPostsProvider, setSiteSetting, stringifyLinkList, stripCommonPrefix, themeSettingKey, unflattenSettings, updatePost, validateBundle, validateBundlePath, validatePluginSettingValue, validatePublicAssetKey, validateThemeValue };
|
|
1879
|
+
export { type AmplessEvent, type AmplessPlugin, type AuthContext, type BundleExtractResult, type CacheConfig, type CacheStrategy, type Config, type ContentEventPayload, type ContentEventType, type ContentFieldRenderer, type ContentFormat, type ContentTypeDefinition, type CreatePostInput, DEFAULT_ENTRYPOINT, type DateFormat, type EventPayloadOf, type EventType, type ExtractedFile, type FieldDefinition, type FieldType, type HistoryConfig, type ImageDisplay, type KvItem, type KvStore, type LinkListItem, type ListOptions, type ListPostHistoryOptions, type LocalizedString, MAX_BUNDLE_BYTES, type MarkdownEmbedMatch, type Media, type MediaEventPayload, type MediaEventType, type MediaMetadata, type MediaProcessingDefaults, type OgImageConfig, type OgImageFont, type OgImageRenderContext, PLUGIN_KEY_PATTERN, type Page, type PluginBooleanField, type PluginCapability, type PluginCodeField, type PluginEventHandler, type PluginHookResult, type PluginJsonField, type PluginMetadata, type PluginNumberField, type PluginPackageManifest, type PluginPublicRenderContext, type PluginRepeatableField, type PluginRepeatableSubField, type PluginRuntimeContext, type PluginSecretField, type PluginSelectField, type PluginSettingField, type PluginSettingsManifest, type PluginTextField, type PluginTextareaField, type PluginUninstallContext, type PluginUrlField, type Post, type PostIndexEventPayload, type PostIndexEventType, type PostMetadata, type PostRevision, type PostRevisionConnection, type PostStatus, type PostsProvider, type PublicBodyDescriptor, type PublicHeadDescriptor, type PublicPostBodyDescriptor, type PublicPostHtmlDescriptor, type PublicPostHtmlPosition, type PublicPostScriptDescriptor, type Role, SITE_CONFIG_PK, type ScriptStrategy, type SiteSettingsEventPayload, type SiteSettingsEventType, type StaticPostBody, type StaticPostFileMeta, type StreamEventName, TEXT_EXTENSIONS, type ThemeColorField, type ThemeField, type ThemeFieldType, type ThemeFontFamilyField, type ThemeImageField, type ThemeLengthField, type ThemeLinkListField, type ThemeManifest, type ThemeModule, type ThemeRouteContext, type ThemeSelectField, type ThemeTextField, type TiptapRenderNode, type TrustLevel, type TrustedPluginRuntimeContext, VERSION, type ValidationIssue, bundlePrefix, createPost, decodeAwsJson, defineConfig, definePlugin, defineSchema, defineTheme, defineThemeModule, deletePost, deleteSiteSetting, detectContentEvents, encodeAwsJson, escapeXml, extractFirstImageUrl, findAbsolutePathRefs, flattenSettings, formatColorPair, formatDate, formatPublicAssetUrl, getKvStore, getPost, getPostById, getSiteSetting, hasKvStore, hasPostsProvider, isTagListUrl, isValidPluginKey, listPostHistory, listPosts, listSiteSettings, mimeTypeFor, parseColorPair, parseLinkList, pickDefaultEntrypoint, resolveLocalized, resolvePluginSettings, resolveThemeValues, setKvStore, setPostsProvider, setSiteSetting, stringifyLinkList, stripCommonPrefix, themeSettingKey, unflattenSettings, updatePost, validateBundle, validateBundlePath, validatePluginSettingValue, validatePublicAssetKey, validateThemeValue };
|
|
@@ -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"
|
|
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"
|
|
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
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ampless",
|
|
3
|
-
"version": "1.0.0-alpha.
|
|
3
|
+
"version": "1.0.0-alpha.42",
|
|
4
4
|
"description": "Serverless CMS for AWS Amplify",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -32,6 +32,17 @@
|
|
|
32
32
|
"dependencies": {
|
|
33
33
|
"@jsquash/webp": "^1.5.0"
|
|
34
34
|
},
|
|
35
|
+
"peerDependencies": {
|
|
36
|
+
"@types/react": "^19"
|
|
37
|
+
},
|
|
38
|
+
"peerDependenciesMeta": {
|
|
39
|
+
"@types/react": {
|
|
40
|
+
"optional": true
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
"devDependencies": {
|
|
44
|
+
"@types/react": "^19.2.15"
|
|
45
|
+
},
|
|
35
46
|
"keywords": [
|
|
36
47
|
"cms",
|
|
37
48
|
"aws",
|