create-ampless 0.2.0-alpha.8 → 1.0.0-alpha.100
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 +77 -0
- package/README.md +6 -3
- package/dist/index.js +1282 -93
- package/dist/templates/_shared/AGENTS.ja.md +94 -0
- package/dist/templates/_shared/AGENTS.md +94 -0
- package/dist/templates/_shared/README.ja.md +239 -0
- package/dist/templates/_shared/README.md +239 -0
- package/dist/templates/_shared/RUNBOOK.ja.md +120 -0
- package/dist/templates/_shared/RUNBOOK.md +32 -58
- package/dist/templates/_shared/THEMES.ja.md +495 -0
- package/dist/templates/_shared/THEMES.md +523 -0
- package/dist/templates/_shared/amplify/auth/post-confirmation/resource.ts +7 -0
- package/dist/templates/_shared/amplify/backend.custom.ts +41 -0
- package/dist/templates/_shared/amplify/backend.ts +20 -2
- package/dist/templates/_shared/amplify/data/get-media-by-src.js +45 -0
- package/dist/templates/_shared/amplify/data/get-published-post.js +9 -12
- package/dist/templates/_shared/amplify/data/list-posts-by-tag.js +6 -9
- package/dist/templates/_shared/amplify/data/list-published-posts.js +10 -11
- package/dist/templates/_shared/amplify/data/resource.custom.ts +32 -0
- package/dist/templates/_shared/amplify/data/resource.ts +32 -15
- package/dist/templates/_shared/amplify/events/dispatcher/resource.ts +1 -0
- package/dist/templates/_shared/amplify/events/processor-trusted/resource.ts +1 -0
- package/dist/templates/_shared/amplify/events/processor-untrusted/resource.ts +5 -0
- package/dist/templates/_shared/amplify/functions/api-key-renewer/resource.ts +1 -0
- package/dist/templates/_shared/amplify/functions/mcp-handler/handler.ts +1 -0
- package/dist/templates/_shared/amplify/functions/mcp-handler/resource.ts +12 -0
- package/dist/templates/_shared/amplify/functions/plugin-secret-handler/handler.ts +11 -0
- package/dist/templates/_shared/amplify/functions/plugin-secret-handler/resource.ts +12 -0
- package/dist/templates/_shared/amplify/functions/user-admin/handler.ts +4 -0
- package/dist/templates/_shared/amplify/functions/user-admin/resource.ts +13 -0
- package/dist/templates/_shared/amplify/secrets/.gitkeep +0 -0
- package/dist/templates/_shared/amplify/secrets/encryption-key.ts +9 -0
- package/dist/templates/_shared/app/(admin)/admin/mcp-tokens/page.tsx +5 -0
- package/dist/templates/_shared/app/(admin)/admin/plugins/page.tsx +5 -0
- package/dist/templates/_shared/app/(admin)/admin/users/page.tsx +5 -0
- package/dist/templates/_shared/app/globals.css +55 -39
- package/dist/templates/_shared/app/layout.tsx +51 -16
- package/dist/templates/_shared/app/providers.tsx +0 -2
- package/dist/templates/_shared/app/raw/[slug]/route.ts +10 -0
- package/dist/templates/_shared/app/static/[slug]/[[...path]]/route.ts +14 -0
- package/dist/templates/_shared/cms.config.ts +64 -23
- package/dist/templates/_shared/components/site-chrome/site-sidebar.tsx +3 -4
- package/dist/templates/_shared/components/tag-list.tsx +9 -2
- package/dist/templates/_shared/components.json +1 -1
- package/dist/templates/_shared/docs/plugin-author-guide.ja.md +934 -0
- package/dist/templates/_shared/docs/plugin-author-guide.md +1336 -0
- package/dist/templates/_shared/lib/admin.ts +12 -12
- package/dist/templates/_shared/lib/ampless.ts +2 -8
- package/dist/templates/_shared/lib/amplify.ts +0 -5
- package/dist/templates/_shared/package.json +51 -40
- package/dist/templates/_shared/plugins/README.ja.md +139 -0
- package/dist/templates/_shared/plugins/README.md +143 -0
- package/dist/templates/_shared/proxy.ts +23 -8
- package/dist/templates/blog/README.ja.md +22 -0
- package/dist/templates/blog/README.md +17 -47
- package/dist/templates/blog/manifest.ts +1 -1
- package/dist/templates/blog/pages/feed.ts +4 -5
- package/dist/templates/blog/pages/home.tsx +10 -15
- package/dist/templates/blog/pages/post.tsx +42 -14
- package/dist/templates/blog/pages/sitemap.ts +4 -5
- package/dist/templates/blog/pages/tag.tsx +8 -10
- package/dist/templates/blog/tokens.css +26 -40
- package/dist/templates/corporate/README.ja.md +18 -0
- package/dist/templates/corporate/README.md +12 -14
- package/dist/templates/corporate/pages/feed.ts +3 -4
- package/dist/templates/corporate/pages/home.tsx +7 -12
- package/dist/templates/corporate/pages/post.tsx +20 -14
- package/dist/templates/corporate/pages/sitemap.ts +3 -4
- package/dist/templates/corporate/pages/tag.tsx +8 -10
- package/dist/templates/corporate/tokens.css +17 -39
- package/dist/templates/dads/README.ja.md +31 -0
- package/dist/templates/dads/README.md +13 -17
- package/dist/templates/dads/pages/feed.ts +3 -4
- package/dist/templates/dads/pages/home.tsx +7 -12
- package/dist/templates/dads/pages/post.tsx +20 -14
- package/dist/templates/dads/pages/sitemap.ts +3 -4
- package/dist/templates/dads/pages/tag.tsx +8 -10
- package/dist/templates/dads/tokens.css +22 -42
- package/dist/templates/docs/README.ja.md +24 -0
- package/dist/templates/docs/README.md +10 -13
- package/dist/templates/docs/pages/feed.ts +3 -4
- package/dist/templates/docs/pages/home.tsx +6 -9
- package/dist/templates/docs/pages/post.tsx +19 -13
- package/dist/templates/docs/pages/sitemap.ts +3 -4
- package/dist/templates/docs/pages/tag.tsx +8 -10
- package/dist/templates/docs/tokens.css +17 -39
- package/dist/templates/landing/README.ja.md +20 -0
- package/dist/templates/landing/README.md +14 -19
- package/dist/templates/landing/pages/feed.ts +4 -5
- package/dist/templates/landing/pages/home.tsx +7 -12
- package/dist/templates/landing/pages/post.tsx +20 -14
- package/dist/templates/landing/pages/sitemap.ts +3 -4
- package/dist/templates/landing/pages/tag.tsx +8 -10
- package/dist/templates/landing/tokens.css +17 -39
- package/dist/templates/minimal/README.ja.md +14 -0
- package/dist/templates/minimal/README.md +9 -47
- package/dist/templates/minimal/pages/feed.ts +4 -5
- package/dist/templates/minimal/pages/home.tsx +6 -8
- package/dist/templates/minimal/pages/post.tsx +19 -12
- package/dist/templates/minimal/pages/sitemap.ts +4 -5
- package/dist/templates/minimal/pages/tag.tsx +7 -8
- package/dist/templates/minimal/tokens.css +17 -39
- package/dist/templates/plugin-local/README.md +34 -0
- package/dist/templates/plugin-local/index.ts +39 -0
- package/dist/templates/plugin-standalone/CHANGELOG.md +1 -0
- package/dist/templates/plugin-standalone/README.ja.md +43 -0
- package/dist/templates/plugin-standalone/README.md +43 -0
- package/dist/templates/plugin-standalone/package.json +47 -0
- package/dist/templates/plugin-standalone/src/index.test.ts +16 -0
- package/dist/templates/plugin-standalone/src/index.ts +29 -0
- package/dist/templates/plugin-standalone/tsconfig.json +16 -0
- package/dist/templates/plugin-standalone/tsup.config.ts +8 -0
- package/package.json +1 -1
- package/dist/templates/_shared/app/(admin)/admin/sites/page.tsx +0 -5
- package/dist/templates/_shared/app/site/[siteId]/raw/[slug]/route.ts +0 -5
- package/dist/templates/_shared/components/i18n-provider.tsx +0 -15
- package/dist/templates/_shared/lib/admin-site-client.ts +0 -10
- package/dist/templates/_shared/lib/admin-site.ts +0 -12
- package/dist/templates/_shared/lib/amplify-server.ts +0 -7
- package/dist/templates/_shared/lib/auth-server.ts +0 -15
- package/dist/templates/_shared/lib/cn.ts +0 -5
- package/dist/templates/_shared/lib/i18n.ts +0 -35
- package/dist/templates/_shared/lib/kv-provider.ts +0 -7
- package/dist/templates/_shared/lib/media.ts +0 -6
- package/dist/templates/_shared/lib/posts-provider.ts +0 -7
- package/dist/templates/_shared/lib/posts-public.ts +0 -27
- package/dist/templates/_shared/lib/posts.ts +0 -12
- package/dist/templates/_shared/lib/seo.ts +0 -11
- package/dist/templates/_shared/lib/site-settings.ts +0 -11
- package/dist/templates/_shared/lib/storage.ts +0 -10
- package/dist/templates/_shared/lib/theme-actions.ts +0 -5
- package/dist/templates/_shared/lib/theme-active.ts +0 -10
- package/dist/templates/_shared/lib/theme-config.ts +0 -12
- package/dist/templates/_shared/lib/upload.ts +0 -6
- /package/dist/templates/_shared/app/{site/[siteId]/[slug] → [slug]}/page.tsx +0 -0
- /package/dist/templates/_shared/app/{site/[siteId]/feed.xml → feed.xml}/route.ts +0 -0
- /package/dist/templates/_shared/app/{site/[siteId]/og → og}/[slug]/route.ts +0 -0
- /package/dist/templates/_shared/app/{site/[siteId]/page.tsx → page.tsx} +0 -0
- /package/dist/templates/_shared/app/{site/[siteId]/sitemap.xml → sitemap.xml}/route.ts +0 -0
- /package/dist/templates/_shared/app/{site/[siteId]/tag → tag}/[tag]/page.tsx +0 -0
|
@@ -0,0 +1,934 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
Source of truth lives in packages/ampless/docs/plugin-author-guide.md.
|
|
3
|
+
Keep both copies in sync — the scaffold copy at
|
|
4
|
+
templates/_shared/docs/plugin-author-guide.ja.md must mirror this file
|
|
5
|
+
byte-for-byte until we add a CI check.
|
|
6
|
+
-->
|
|
7
|
+
|
|
8
|
+
> English: [plugin-author-guide.md](./plugin-author-guide.md)
|
|
9
|
+
|
|
10
|
+
# ampless プラグインの書き方
|
|
11
|
+
|
|
12
|
+
このガイドは、初めての `definePlugin()` 呼び出しから admin 編集可能な設定パネル、そして npm 公開に至るまで、ampless プラグインを ship するために必要な手順を一通りカバーします。Phase 1〜4 の機能を網羅 — descriptor ベースの `<head>` / `<body>` / 投稿単位 body 注入、非同期イベントフック、admin 管理の `settings.public` 値です。
|
|
13
|
+
|
|
14
|
+
設計の経緯と背景は [`docs/architecture/08-plugin-architecture.md`](https://github.com/heavymoons/ampless/blob/main/docs/architecture/08-plugin-architecture.md) に集約。本ページはその実装ハンドブック側です。
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## 0. テーマとプラグインの境界線
|
|
19
|
+
|
|
20
|
+
ampless はテーマとプラグインの両方を提供します。用途に合ったものを選ぶことで、未来の自分や他のサイト作者が迷わずコードを見つけられます。
|
|
21
|
+
|
|
22
|
+
| やりたいこと | テーマを使う | プラグインを使う |
|
|
23
|
+
|---|---|---|
|
|
24
|
+
| レイアウト・タイポグラフィ・配色・ルート単位の UI | ✓ | |
|
|
25
|
+
| home / post / tag ページのカスタムコンポーネント | ✓ | |
|
|
26
|
+
| 非開発者が admin から編集できる設定 | | ✓ (`adminSettings`) |
|
|
27
|
+
| コンテンツイベント後のバックグラウンド処理(RSS・検索インデックス・webhook) | | ✓ (`eventHooks`) |
|
|
28
|
+
| 信頼できる副作用(S3 書き込み・外部 API 送信) | | ✓ (`writePublicAsset` + `trusted`) |
|
|
29
|
+
| テーマに依存しない `<head>` / `<body>` 注入(アナリティクス・同意バナー) | | ✓ (`publicHead` / `publicBodyEnd`) |
|
|
30
|
+
| 投稿単位の機械可読メタデータ(JSON-LD 等) | | ✓ (`schema` via `publicBodyForPost`) |
|
|
31
|
+
| 投稿本文の周囲の可視 HTML(reading-time、breadcrumb、share など) | | ✓ (`publicHtmlForPost`) |
|
|
32
|
+
| 複数の ampless サイトで共有したいコード | | ✓ (npm パッケージとして公開) |
|
|
33
|
+
|
|
34
|
+
判断の目安:
|
|
35
|
+
|
|
36
|
+
- テーマ = **ページの見た目**。render 時は読み取り専用。
|
|
37
|
+
- プラグイン = **render を超えて起こること**: admin 編集可能な設定、バックグラウンド処理、テーマ非依存の注入、機械可読メタデータ、サイト間の再利用。
|
|
38
|
+
|
|
39
|
+
新しいプラグイン作者がよく踏む 2 つの境界線:
|
|
40
|
+
|
|
41
|
+
- **ストレージ / DynamoDB / 外部 API 書き込みはプラグインに置く**。テーマは読み取り専用です。
|
|
42
|
+
- **admin が `/admin/plugins` からオン・オフしたい機能はプラグインに置く**。表面上の効果が純粋に見た目だけであっても同様です。テーマも独自の設定を持てますが、それはテーマ表示設定であり、サイト運用設定ではありません。
|
|
43
|
+
|
|
44
|
+
境界線上に本当に乗っている機能については [`docs/architecture/08-plugin-architecture.md`](https://github.com/heavymoons/ampless/blob/main/docs/architecture/08-plugin-architecture.md) で詳しく議論しています。
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## 1. プラグインで何ができるか
|
|
49
|
+
|
|
50
|
+
ampless プラグインは 3 つのいずれかの場所に書きます — コードをどのくらい広く共有したいかに応じて選んでください:
|
|
51
|
+
|
|
52
|
+
| どこに置くか | 使い時 | 置き場所 |
|
|
53
|
+
|---|---|---|
|
|
54
|
+
| **ファーストパーティ** | ampless コアへの全員向け貢献 | ampless モノレポ内の `packages/plugin-*/` |
|
|
55
|
+
| **サイトローカル** | サイト固有のカスタマイズ、個別 publish 不要 | サイトリポジトリ内の `plugins/<name>/` |
|
|
56
|
+
| **外部 npm パッケージ** | 他のサイトと共有したい、`npm publish` 想定 | スタンドアロンリポジトリ (`@scope/ampless-plugin-foo`) |
|
|
57
|
+
|
|
58
|
+
3 つの形式はすべて同じ `definePlugin({...})` ファクトリを呼び出し、同じサーフェスを使います。違いはパッケージング・配布方法、および静的 `package.json#amplessPlugin` マニフェストが有効にするインストール時バリデーションのオプトインです(§3 参照)。
|
|
59
|
+
|
|
60
|
+
§14 には一行のスキャフォールドコマンド (`npx create-ampless plugin <name>`) があり、後者 2 つのどちらにも即使えるボイラープレートを生成します。
|
|
61
|
+
|
|
62
|
+
ampless プラグインは `AmplessPlugin` オブジェクトを返す TypeScript モジュールです。以下のうち 1 つ以上のサーフェスにフックします:
|
|
63
|
+
|
|
64
|
+
| サーフェス | 実行場所 | 同期 / 非同期 | Phase |
|
|
65
|
+
|---|---|---|---|
|
|
66
|
+
| `metadata(post, site)` | 投稿の `generateMetadata()` | 同期 | 既存 |
|
|
67
|
+
| `siteMetadata(site)` | root layout の `generateMetadata()` | 同期 | 既存 |
|
|
68
|
+
| `publicHead(ctx)` | root layout の `<head>` | 同期 (async layout から呼ばれる) | 1 |
|
|
69
|
+
| `publicBodyEnd(ctx)` | root layout の `<body>` 末尾 | 同期 | 1 |
|
|
70
|
+
| `publicBodyForPost(post, ctx)` | テーマの post ページテンプレート(投稿単位) | 同期 | 4 |
|
|
71
|
+
| `publicHtmlForPost(post, ctx)` | テーマの post ページテンプレート(投稿単位、可視 HTML) | 同期 | 6d |
|
|
72
|
+
| `ogImage` | `/og/[slug]` ルート | リクエスト時、公開 Lambda 内 | 既存 |
|
|
73
|
+
| `hooks` | trust_level に応じた processor Lambda | 非同期、SQS イベントで起動 | 既存 |
|
|
74
|
+
| `settings.public` | `/admin/plugins` フォーム | 宣言的なマニフェスト | 2 |
|
|
75
|
+
|
|
76
|
+
後続フェーズに残してある surface もいくつかあって、現状の `definePlugin`
|
|
77
|
+
では形にできません:
|
|
78
|
+
|
|
79
|
+
- **任意の `ReactNode` のページ注入**。同期描画 surface (`publicHead` /
|
|
80
|
+
`publicBodyEnd` / `publicBodyForPost`) は descriptor 変種を返すだけ。
|
|
81
|
+
descriptor の validator は **runtime が描画する HTML 出力の安全境界**
|
|
82
|
+
であって、プラグイン本体のコードを縛る JS sandbox ではない (プラグインは
|
|
83
|
+
普通の TypeScript としてサイトと同一の Node プロセス内で動く)
|
|
84
|
+
- **同期描画 surface 内でのネットワーク**。これらの surface は宣言的な
|
|
85
|
+
出力を返す設計で、Promise を受け取らないし async result path も無い。
|
|
86
|
+
`publicHead` の中で `await fetch(...)` を書くと SSR が無期限ブロックする
|
|
87
|
+
(デッドラインを返す手段がない)。外向き HTTP が必要な処理は trusted
|
|
88
|
+
Lambda (`hooks`) でやる
|
|
89
|
+
- **admin ルート / server ルート / コンテンツフィールドの追加** —
|
|
90
|
+
Phase 6b 予約
|
|
91
|
+
- **Admin routes / server routes / content fields.** Phase 6b 予約。
|
|
92
|
+
`settings.public` に credential を置かないこと
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
## 2. 最小ファイル構成
|
|
97
|
+
|
|
98
|
+
プラグインを作る最速の方法はスキャフォールドです:
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
# サイトローカル (現在の ampless サイトに plugins/<name>/index.ts を生成)
|
|
102
|
+
npx create-ampless@latest plugin my-thing
|
|
103
|
+
|
|
104
|
+
# スタンドアロン npm パッケージ (`npm publish` 向けの ./<name>/ を生成)
|
|
105
|
+
npx create-ampless@latest plugin @myscope/ampless-plugin-my-thing --standalone
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
全体の手順は §14 を参照してください。このセクションの残りでは生成されるファイルの意味を説明します — 手書きしたい場合はここを読めば把握できます。
|
|
109
|
+
|
|
110
|
+
### サイトローカル
|
|
111
|
+
|
|
112
|
+
```
|
|
113
|
+
plugins/
|
|
114
|
+
my-thing/
|
|
115
|
+
index.ts # ファクトリ関数のみ。これがプラグインの全体
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
サイトの `package.json` / `tsconfig.json` がコンパイルを担うため、追加で ship するものはありません。`cms.config.ts` から相対 import で登録します。
|
|
119
|
+
|
|
120
|
+
### スタンドアロン npm パッケージ
|
|
121
|
+
|
|
122
|
+
```
|
|
123
|
+
ampless-plugin-my-thing/
|
|
124
|
+
package.json
|
|
125
|
+
tsconfig.json
|
|
126
|
+
tsup.config.ts
|
|
127
|
+
README.md
|
|
128
|
+
CHANGELOG.md
|
|
129
|
+
src/
|
|
130
|
+
index.ts
|
|
131
|
+
index.test.ts
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
本レポ内の `packages/plugin-rss/` と `packages/plugin-analytics-ga4/` が動作するファーストパーティの参考実装です — スタンドアロンスキャフォールドはこれらのレイアウトを踏襲します。
|
|
135
|
+
|
|
136
|
+
最小の `src/index.ts`:
|
|
137
|
+
|
|
138
|
+
```ts
|
|
139
|
+
import { definePlugin, type AmplessPlugin } from 'ampless'
|
|
140
|
+
|
|
141
|
+
export default function myPlugin(): AmplessPlugin {
|
|
142
|
+
return definePlugin({
|
|
143
|
+
name: 'my-plugin',
|
|
144
|
+
apiVersion: 1,
|
|
145
|
+
trust_level: 'untrusted',
|
|
146
|
+
capabilities: ['publicHead'],
|
|
147
|
+
publicHead() {
|
|
148
|
+
return [{ type: 'meta', name: 'x-plugin', content: 'hi' }]
|
|
149
|
+
},
|
|
150
|
+
})
|
|
151
|
+
}
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
`cms.config.ts` に差し込み:
|
|
155
|
+
|
|
156
|
+
```ts
|
|
157
|
+
import myPlugin from 'my-plugin'
|
|
158
|
+
|
|
159
|
+
export default defineConfig({
|
|
160
|
+
site: { name: 'My Blog', url: 'https://example.com' },
|
|
161
|
+
plugins: [myPlugin()],
|
|
162
|
+
})
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
これで完了。`npm run dev` を再起動して任意のページのソースを表示すると `<meta name="x-plugin" content="hi" />` が `<head>` に出ています。
|
|
166
|
+
|
|
167
|
+
---
|
|
168
|
+
|
|
169
|
+
## 3. `AmplessPlugin` マニフェスト
|
|
170
|
+
|
|
171
|
+
```ts
|
|
172
|
+
interface AmplessPlugin {
|
|
173
|
+
name: string // パッケージ風の識別子。例: 'analytics-ga4'
|
|
174
|
+
packageName?: string // インストール時のクロスチェック用 npm パッケージ名
|
|
175
|
+
apiVersion: 1 // 契約が変わるときだけ bump
|
|
176
|
+
trust_level: 'untrusted' | 'trusted' | 'privileged'
|
|
177
|
+
instanceId?: string // 複数インストール時の namespace
|
|
178
|
+
displayName?: LocalizedString // admin UI ラベル
|
|
179
|
+
capabilities?: readonly PluginCapability[]
|
|
180
|
+
hooks?: { ... } // 非同期イベント
|
|
181
|
+
metadata?(post, site): PluginMetadata
|
|
182
|
+
siteMetadata?(site): PluginMetadata
|
|
183
|
+
publicHead?(ctx): readonly PublicHeadDescriptor[]
|
|
184
|
+
publicBodyEnd?(ctx): readonly PublicBodyDescriptor[]
|
|
185
|
+
publicBodyForPost?(post: Post, ctx): readonly PublicPostBodyDescriptor[]
|
|
186
|
+
publicHtmlForPost?(post: Post, ctx): readonly PublicPostHtmlDescriptor[]
|
|
187
|
+
ogImage?: OgImageConfig
|
|
188
|
+
settings?: { public?: readonly PluginSettingField[] }
|
|
189
|
+
}
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
### `name`
|
|
193
|
+
|
|
194
|
+
短い識別子 (`'analytics-ga4'`、`'rss'`、`'webhook'` など)。デフォルトの `instanceId` および trusted processor が S3 に書き出す `public/plugins/<name>/` のプレフィックスに使われます。`/^[a-zA-Z0-9_-]+$/` 必須 — 「命名規則」セクション参照。
|
|
195
|
+
|
|
196
|
+
### `apiVersion: 1`
|
|
197
|
+
|
|
198
|
+
今日は 1 のみ。将来の互換性破壊バージョンが出たらこの数字が bump され、runtime は未知の値を黙って bind せず拒否します。
|
|
199
|
+
|
|
200
|
+
### `instanceId`
|
|
201
|
+
|
|
202
|
+
optional、デフォルトは `name`。同じプラグインを 1 サイトで複数 instance 動かせる作り (例: 2 つの GA4 measurement ID、チャットプラットフォーム毎の webhook) では、ホストに `instanceId` を指定させて各々独立した namespace を持たせる:
|
|
203
|
+
|
|
204
|
+
```ts
|
|
205
|
+
analyticsGa4Plugin({ instanceId: 'marketing' })
|
|
206
|
+
analyticsGa4Plugin({ instanceId: 'product' })
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
`instanceId` も `name` も `/^[a-zA-Z0-9_-]+$/` を満たす必要があります。`.` は `pk='siteconfig', sk='plugins.<id>.<key>'` の区切りを壊します。scope (`@foo/bar`) やスラッシュは予約済みです。
|
|
210
|
+
|
|
211
|
+
### `displayName`
|
|
212
|
+
|
|
213
|
+
`/admin/plugins` のパネル見出し。単一ロケールのプラグインなら平文の文字列で十分。`{ en: 'GA4', ja: 'GA4' }` 形式の per-locale map にすると admin のアクティブロケールに応じて読み分けられます。
|
|
214
|
+
|
|
215
|
+
### `packageName`
|
|
216
|
+
|
|
217
|
+
省略可能。設定すると、runtime は起動時に `<packageName>/package.json` を解決し、そこにある静的な `amplessPlugin` ブロックをファクトリの戻り値とクロスチェックします。これにより、runtime で初めて気づく(あるいは永遠に気づかない)インストール時のミスを検出できます — capability の不一致はクラッシュせず、該当サーフェスが静かにスキップされるだけです。
|
|
218
|
+
|
|
219
|
+
スタンドアロンプラグインでは、`package.json#name` で宣言している npm パッケージ名をここに設定します:
|
|
220
|
+
|
|
221
|
+
```ts
|
|
222
|
+
return definePlugin({
|
|
223
|
+
name: 'site-verification',
|
|
224
|
+
packageName: '@ishinao/ampless-plugin-site-verification',
|
|
225
|
+
apiVersion: 1,
|
|
226
|
+
// ...
|
|
227
|
+
})
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
サイトローカルプラグインは不要です — 未設定のままにするとクロスチェックはスキップされます(Phase 5 より前のプラグインとの後方互換)。
|
|
231
|
+
|
|
232
|
+
### `package.json` の静的マニフェスト(スタンドアロンプラグインのみ)
|
|
233
|
+
|
|
234
|
+
クロスチェックが静的マニフェストを見つけるには、公開パッケージに 2 つの条件が必要です:
|
|
235
|
+
|
|
236
|
+
1. `package.json#amplessPlugin` がファクトリの戻り値と同じフィールドを宣言している:
|
|
237
|
+
|
|
238
|
+
```json
|
|
239
|
+
"amplessPlugin": {
|
|
240
|
+
"apiVersion": 1,
|
|
241
|
+
"name": "site-verification",
|
|
242
|
+
"trustLevel": "untrusted",
|
|
243
|
+
"capabilities": ["publicHead", "adminSettings"],
|
|
244
|
+
"displayName": { "en": "Site verification", "ja": "サイト所有権確認" }
|
|
245
|
+
}
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
2. `package.json#exports` が `./package.json` を明示的に公開している:
|
|
249
|
+
|
|
250
|
+
```json
|
|
251
|
+
"exports": {
|
|
252
|
+
".": {
|
|
253
|
+
"import": "./dist/index.js",
|
|
254
|
+
"types": "./dist/index.d.ts"
|
|
255
|
+
},
|
|
256
|
+
"./package.json": "./package.json"
|
|
257
|
+
}
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
これがないと、Node のパッケージエクスポートの制約により `import.meta.resolve('<pkg>/package.json')` が `ERR_PACKAGE_PATH_NOT_EXPORTED` で拒否され、runtime はクロスチェックを静かにスキップします(プラグインは動きますが、インストール時ガードは機能しません)。
|
|
261
|
+
|
|
262
|
+
`create-ampless plugin --standalone` スキャフォールドは両方を正しく生成します。また `package.json#keywords` に `"ampless-plugin"` を加えておくことをお勧めします — npm 検索で ampless プラグインを探す際の慣例です。
|
|
263
|
+
|
|
264
|
+
runtime がチェックする内容:
|
|
265
|
+
|
|
266
|
+
| フィールド | 不一致時の動作 |
|
|
267
|
+
|---|---|
|
|
268
|
+
| `apiVersion`(ファクトリ vs マニフェスト) | 起動時に **throws** |
|
|
269
|
+
| `apiVersion`(runtime がサポートするバージョンより新しい) | 起動時に **throws** |
|
|
270
|
+
| `name` | dev で warn |
|
|
271
|
+
| `trustLevel` | dev で warn |
|
|
272
|
+
| `capabilities`(集合比較) | dev で warn |
|
|
273
|
+
|
|
274
|
+
起動を中断するのは 2 つの `apiVersion` ケースのみです — これは runtime が対応していない ampless API でビルドされたプラグインのロードを防ぎます。その他はすべて開発者向けの警告であり、runtime のブロックではありません。
|
|
275
|
+
|
|
276
|
+
---
|
|
277
|
+
|
|
278
|
+
## 4. `trust_level` の選び方
|
|
279
|
+
|
|
280
|
+
3 階層、選択基準は **イベントフック (hooks) が何を必要とするか** で決まります (sync サーフェスは IAM に触れない):
|
|
281
|
+
|
|
282
|
+
| 階層 | IAM | 用途 |
|
|
283
|
+
|---|---|---|
|
|
284
|
+
| `untrusted` | なし (SQS consume のみ) | head/body descriptor、webhook 配送、コンテンツ変換 |
|
|
285
|
+
| `trusted` | 投稿読み出し、`public/plugins/<instanceId ?? name>/...` への書き込み | RSS フィード、sitemap、計算済み JSON インデックス |
|
|
286
|
+
| `privileged` | 予約 | 将来: SES、secret、private S3 |
|
|
287
|
+
|
|
288
|
+
決め方の目安:
|
|
289
|
+
|
|
290
|
+
- **`publicHead` / `publicBodyEnd` / `metadata` だけ必要** → `untrusted`
|
|
291
|
+
- **hooks から投稿を読みたい (publish 時にフィードを再生成等)** → `trusted`
|
|
292
|
+
- **`public/plugins/*` 以外への S3 PutObject や他の AWS API が必要** → 今はプラグインで ship せず、privileged 層を待つかプラグイン外で実装
|
|
293
|
+
|
|
294
|
+
trust level がズレているプラグインは「権限不足で sliently fail」または「不要に強い権限を持つ」のどちらか。階層を切り替えて再デプロイすれば直ります。
|
|
295
|
+
|
|
296
|
+
---
|
|
297
|
+
|
|
298
|
+
## 5. 同期サーフェス
|
|
299
|
+
|
|
300
|
+
**公開 Next.js プロセス** (サイト訪問者のリクエストスレッド) 内で同期実行
|
|
301
|
+
されます。これらの surface はネットワーク I/O を意図して設計していません。
|
|
302
|
+
async result path が無いので `publicHead` 内で `await fetch(...)` すると
|
|
303
|
+
SSR がデッドラインなしでブロックします。ネットワーク呼び出しが必要な副作用
|
|
304
|
+
は `hooks` (trusted Lambda、async) でやってください。公開プロセス内の
|
|
305
|
+
プラグインコードは公開ページ用の IAM ロールで動きます — 特別な AWS 権限は
|
|
306
|
+
ありません。
|
|
307
|
+
|
|
308
|
+
| サーフェス | 戻り値 | 用途 |
|
|
309
|
+
|---|---|---|
|
|
310
|
+
| `metadata(post, site)` | `PluginMetadata` (Next.js `Metadata` 形) | 投稿単位の `<title>` / OGP / Twitter / canonical |
|
|
311
|
+
| `siteMetadata(site)` | `PluginMetadata` | サイト全体の `<title>` / favicon / RSS `<link rel="alternate">` |
|
|
312
|
+
| `publicHead(ctx)` | `PublicHeadDescriptor[]` | 解析ローダー、フォント、jsonld、hreflang |
|
|
313
|
+
| `publicBodyEnd(ctx)` | `PublicBodyDescriptor[]` | GTM no-script フレーム、チャットウィジェット、末尾スニペット |
|
|
314
|
+
| `publicBodyForPost(post, ctx)` | `PublicPostBodyDescriptor[]` | 投稿単位の body 注入 — JSON-LD 構造化データ。テーマの post ページテンプレートが render する |
|
|
315
|
+
| `publicHtmlForPost(post, ctx)` | `PublicPostHtmlDescriptor[]` | 投稿単位の可視 HTML を `beforeContent` / `afterContent` に注入 — reading-time バッジ、breadcrumb、share リンク等。body は runtime が `sanitize-html` の厳格 allowlist で sanitize |
|
|
316
|
+
|
|
317
|
+
`ctx` オブジェクトの中身:
|
|
318
|
+
|
|
319
|
+
```ts
|
|
320
|
+
{
|
|
321
|
+
site: Config['site'] // name / url / description
|
|
322
|
+
setting<T>(key: string): T | undefined
|
|
323
|
+
}
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
`ctx.setting()` は Phase 2 で追加された admin 管理値アクセッサ — §8 参照。
|
|
327
|
+
|
|
328
|
+
---
|
|
329
|
+
|
|
330
|
+
## 6. Descriptor リファレンス
|
|
331
|
+
|
|
332
|
+
`publicHead` と `publicBodyEnd` は **descriptor オブジェクト** を返します。
|
|
333
|
+
`ReactNode` ではありません。runtime が validation (URL scheme denylist /
|
|
334
|
+
attrs allowlist / id dedup) してから React 要素を組み立てます。これは
|
|
335
|
+
プラグインが寄与できる **HTML 出力** の安全境界であって、プラグイン本体の
|
|
336
|
+
コード実行を縛るものではない、という点に注意 — プラグインは普通の
|
|
337
|
+
TypeScript としてサイトと同一の Node プロセスで動きます。descriptor
|
|
338
|
+
パイプラインは、JS sandbox に頼らずに公開ページの面を狭く / 監査可能に
|
|
339
|
+
保つための仕組みです。
|
|
340
|
+
|
|
341
|
+
### 共通の variant
|
|
342
|
+
|
|
343
|
+
```ts
|
|
344
|
+
// 外部 script
|
|
345
|
+
{
|
|
346
|
+
type: 'script',
|
|
347
|
+
id: 'ga4-loader-analytics-ga4',
|
|
348
|
+
src: 'https://www.googletagmanager.com/gtag/js?id=G-XXX',
|
|
349
|
+
strategy: 'afterInteractive', // または 'lazyOnload'
|
|
350
|
+
async: true, // optional; strategy が暗黙的に付ける
|
|
351
|
+
defer: false,
|
|
352
|
+
attrs: { crossorigin: 'anonymous' },
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// inline script — id は必須 (重複検知に使う)
|
|
356
|
+
{
|
|
357
|
+
type: 'inlineScript',
|
|
358
|
+
id: 'ga4-init-analytics-ga4',
|
|
359
|
+
body: "/* 一行ブートストラップ */",
|
|
360
|
+
strategy: 'afterInteractive',
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// inline script — JSON-LD variant (publicHead / publicBodyEnd / publicBodyForPost で使用可能)
|
|
364
|
+
// runtime が body を自動 escape するので、生の JSON 文字列を返せばよい
|
|
365
|
+
{
|
|
366
|
+
type: 'inlineScript',
|
|
367
|
+
id: 'schema-article',
|
|
368
|
+
scriptType: 'application/ld+json',
|
|
369
|
+
body: JSON.stringify({ '@context': 'https://schema.org', '@type': 'Article', ... }),
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// Meta / link / noscript
|
|
373
|
+
{ type: 'meta', name: 'theme-color', content: '#fff' }
|
|
374
|
+
{ type: 'meta', property: 'og:image', content: 'https://…' }
|
|
375
|
+
{ type: 'link', rel: 'preconnect', href: 'https://cdn.example.com' }
|
|
376
|
+
{ type: 'noscript', id: 'gtm-fallback-msg', html: '<p>JS required</p>' }
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
### body 専用の variant
|
|
380
|
+
|
|
381
|
+
```ts
|
|
382
|
+
// iframe — GTM の no-script フォールバック、チャットウィジェット等
|
|
383
|
+
{
|
|
384
|
+
type: 'iframe',
|
|
385
|
+
id: 'gtm-fallback',
|
|
386
|
+
src: 'https://www.googletagmanager.com/ns.html?id=GTM-XYZ',
|
|
387
|
+
height: 0,
|
|
388
|
+
width: 0,
|
|
389
|
+
attrs: { sandbox: 'allow-scripts' },
|
|
390
|
+
}
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
### `PublicPostBodyDescriptor`(Phase 4)
|
|
394
|
+
|
|
395
|
+
`publicBodyForPost` は `PublicPostBodyDescriptor[]` を返します。これは `inlineScript` の制限サブセットで、`scriptType` が必須かつ `'application/ld+json'` のみ有効です:
|
|
396
|
+
|
|
397
|
+
```ts
|
|
398
|
+
// publicBodyForPost で返せる唯一の形:
|
|
399
|
+
{
|
|
400
|
+
type: 'inlineScript',
|
|
401
|
+
id: 'schema-article',
|
|
402
|
+
scriptType: 'application/ld+json', // 必須 — これ以外は drop + warn
|
|
403
|
+
body: JSON.stringify({ '@context': 'https://schema.org', '@type': 'Article', ... }),
|
|
404
|
+
}
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
`meta` / `link` を除いている理由:投稿単位のメタデータは Next.js `generateMetadata()` 経由の `metadata()` サーフェスが担い、フレームワークの deduplication・streaming と統合されている。`publicBodyForPost` は `generateMetadata` が生成できない構造化データ(`<script type="application/ld+json">`)のためだけに存在する。
|
|
408
|
+
|
|
409
|
+
### `PublicPostHtmlDescriptor`(Phase 6d)
|
|
410
|
+
|
|
411
|
+
`publicHtmlForPost` は `PublicPostHtmlDescriptor[]` を返します:
|
|
412
|
+
|
|
413
|
+
```ts
|
|
414
|
+
{
|
|
415
|
+
type: 'html',
|
|
416
|
+
id: 'display', // plugin-local 短識別子(≤ 64 文字、制御文字不可)
|
|
417
|
+
position: 'beforeContent' | 'afterContent',
|
|
418
|
+
body: '<p class="reading-time">約 3 分で読めます</p>',
|
|
419
|
+
}
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
runtime は `body` を `sanitize-html` の厳格 allowlist で sanitize し(詳しい allowlist と drop 対象は上の `publicHtmlForPost` 例を参照)、結果を `<div data-ampless-plugin="${namespace}" data-ampless-position="${position}">` で wrap します。テーマは `pages/post.tsx` で `{html.beforeContent}` / `{html.afterContent}` を embed するだけで、plugin の出力に対して `dangerouslySetInnerHTML` を書きません。
|
|
423
|
+
|
|
424
|
+
### JSON-LD 自動 escape
|
|
425
|
+
|
|
426
|
+
`scriptType === 'application/ld+json'` のとき、runtime は描画前に **`body` 文字列を自動 escape** する — `<` → `<`、`>` → `>`、`&` → `&`、U+2028 → ` `、U+2029 → ` `。この処理は `inlineScript` を受け付ける 3 つのサーフェス(`publicHead` / `publicBodyEnd` / `publicBodyForPost`)すべてで行われる。プラグイン作者は生の JSON 文字列を返せばよく、自前で escape しなくてよい。
|
|
427
|
+
|
|
428
|
+
サポート外の `scriptType` を持つ descriptor は **console warning 付きで drop** される。
|
|
429
|
+
|
|
430
|
+
### サーフェス別 scriptType ルール
|
|
431
|
+
|
|
432
|
+
| サーフェス | `scriptType` |
|
|
433
|
+
|---|---|
|
|
434
|
+
| `publicHead` | `undefined`(デフォルト JS)または `'application/ld+json'` |
|
|
435
|
+
| `publicBodyEnd` | `publicHead` と同じ |
|
|
436
|
+
| `publicBodyForPost` | `'application/ld+json'` **必須**。他の値(省略含む)は drop + warn |
|
|
437
|
+
|
|
438
|
+
### Validation ルール
|
|
439
|
+
|
|
440
|
+
- **URL scheme allowlist**: `http`、`https`、または相対パス。`javascript:`、`data:`、`vbscript:`、`blob:`、`file:` は要素描画前に拒否されます
|
|
441
|
+
- **`attrs` allowlist**: `data-*`、`crossorigin`、`referrerpolicy`、`integrity`、`fetchpriority`、`loading`、`sandbox`、`allow`、`allowfullscreen`。それ以外は dev warn 付きで drop
|
|
442
|
+
- **`inlineScript.id` は必須**。無いとプラグイン同士が似たスニペットを emit したときに dedup できず、dev warning も index 番号を指すだけで原因プラグインを特定できません
|
|
443
|
+
- **id 重複**: 最後の出現が勝ち。dev warning でどの key が重複したか表示されます
|
|
444
|
+
- **CSP nonce**: Phase 1 では伝搬しません。`nonce` attr は型上は宣言してありますが今のところ無視されます
|
|
445
|
+
- **strategy**: `afterInteractive` は外部 script に `async` を付ける。`lazyOnload` は `defer`。明示的な `async` / `defer` が常に勝ち。`beforeInteractive` は非対応
|
|
446
|
+
|
|
447
|
+
### runtime が描画する形
|
|
448
|
+
|
|
449
|
+
各プラグインについて runtime が `publicHead(ctx)` (resp. `publicBodyEnd`) を呼び、descriptor を validate して reject を drop、残ったものを `<Fragment>` でラップ。root layout はその Fragment を直接埋め込みます:
|
|
450
|
+
|
|
451
|
+
```tsx
|
|
452
|
+
<head>{pluginHead}</head>
|
|
453
|
+
{/* … */}
|
|
454
|
+
<body>… {pluginBodyEnd}</body>
|
|
455
|
+
```
|
|
456
|
+
|
|
457
|
+
`cms.config.plugins` の順序は集約後も保たれます。
|
|
458
|
+
|
|
459
|
+
### `publicBodyForPost` の使用例(Phase 4)
|
|
460
|
+
|
|
461
|
+
`schema` capability を宣言してサーフェスを実装します:
|
|
462
|
+
|
|
463
|
+
```typescript
|
|
464
|
+
import { definePlugin } from 'ampless'
|
|
465
|
+
|
|
466
|
+
export default function schemaJsonldPlugin() {
|
|
467
|
+
return definePlugin({
|
|
468
|
+
name: 'schema-jsonld',
|
|
469
|
+
apiVersion: 1,
|
|
470
|
+
trust_level: 'untrusted',
|
|
471
|
+
capabilities: ['schema'],
|
|
472
|
+
publicBodyForPost(post, ctx) {
|
|
473
|
+
return [{
|
|
474
|
+
type: 'inlineScript',
|
|
475
|
+
id: 'schema-article',
|
|
476
|
+
scriptType: 'application/ld+json',
|
|
477
|
+
body: JSON.stringify({
|
|
478
|
+
'@context': 'https://schema.org',
|
|
479
|
+
'@type': 'Article',
|
|
480
|
+
headline: post.title,
|
|
481
|
+
url: `${ctx.site.url}/${post.slug}`,
|
|
482
|
+
datePublished: post.publishedAt,
|
|
483
|
+
}),
|
|
484
|
+
}]
|
|
485
|
+
},
|
|
486
|
+
})
|
|
487
|
+
}
|
|
488
|
+
```
|
|
489
|
+
|
|
490
|
+
テーマの `pages/post.tsx` が `ampless.publicBodyForPost(post)` を呼び、返された descriptor を描画します。runtime は自動 escape した body を持つ `<script type="application/ld+json">` 要素をページに挿入します。
|
|
491
|
+
|
|
492
|
+
### `publicHtmlForPost` 例(Phase 6d)
|
|
493
|
+
|
|
494
|
+
**可視 HTML** を post の周囲に出したいとき(reading-time バッジ、breadcrumb、share リンク、micro-format 注釈など)は `publicHtmlForPost` を使います。runtime が body を sanitize したうえで `beforeContent` / `afterContent` スロットに embed するので、テーマ側で `dangerouslySetInnerHTML` を書く必要はありません。
|
|
495
|
+
|
|
496
|
+
```typescript
|
|
497
|
+
import { definePlugin } from 'ampless'
|
|
498
|
+
|
|
499
|
+
export default function readingTimePlugin() {
|
|
500
|
+
return definePlugin({
|
|
501
|
+
name: 'reading-time',
|
|
502
|
+
apiVersion: 1,
|
|
503
|
+
trust_level: 'untrusted',
|
|
504
|
+
capabilities: ['publicHtmlForPost'],
|
|
505
|
+
publicHtmlForPost(post, _ctx) {
|
|
506
|
+
const words = countWords(post)
|
|
507
|
+
const minutes = Math.max(1, Math.round(words / 200))
|
|
508
|
+
return [{
|
|
509
|
+
type: 'html',
|
|
510
|
+
id: 'display',
|
|
511
|
+
position: 'beforeContent',
|
|
512
|
+
body: `<p class="reading-time" data-words="${words}" data-minutes="${minutes}">約 ${minutes} 分で読めます</p>`,
|
|
513
|
+
}]
|
|
514
|
+
},
|
|
515
|
+
})
|
|
516
|
+
}
|
|
517
|
+
```
|
|
518
|
+
|
|
519
|
+
テーマの `pages/post.tsx` は `const html = await ampless.publicHtmlForPost(post)` を 1 回呼び、スロットを embed します:
|
|
520
|
+
|
|
521
|
+
```tsx
|
|
522
|
+
{postBody} {/* publicBodyForPost — JSON-LD */}
|
|
523
|
+
{html.beforeContent} {/* publicHtmlForPost — beforeContent スロット */}
|
|
524
|
+
<div className="prose" dangerouslySetInnerHTML={{ __html: renderBody(post) }} />
|
|
525
|
+
{html.afterContent} {/* publicHtmlForPost — afterContent スロット */}
|
|
526
|
+
```
|
|
527
|
+
|
|
528
|
+
**スロット位置**(v1): `'beforeContent'` / `'afterContent'` の 2 つ。
|
|
529
|
+
|
|
530
|
+
**Sanitizer(厳格、trust level に関わらず同一):**
|
|
531
|
+
|
|
532
|
+
- 許可タグ: `p` · `span` · `strong` · `em` · `a` · `code` · `br` · `ul` · `ol` · `li`
|
|
533
|
+
- 許可グローバル属性: `class` · `data-words` · `data-minutes` · `data-ampless-*`
|
|
534
|
+
- 許可 `<a>` 属性: `href` · `rel` · `target`。`target="_blank"` のとき sanitizer が `rel="noopener noreferrer"` を自動付与
|
|
535
|
+
- `href` で許可するスキーム: `http` / `https`。相対 URL (`./path` / `../path` / `/path` / `#anchor`) は素通り。`javascript:` / `data:` / `mailto:` / `tel:` / `vbscript:` は drop
|
|
536
|
+
- drop されるタグ・属性: `<img>` · `<iframe>` · `<video>` · `<audio>` · `<object>` · `<embed>` · `<form>` · `<style>` · インライン `style` · 全 event handler (`on*`)
|
|
537
|
+
|
|
538
|
+
allowlist 外のタグが必要になった場合は issue を立ててください。allowlist は設計上拡張するものであって、escape hatch ではありません。
|
|
539
|
+
|
|
540
|
+
**`id` は plugin-local。** 短い識別子(例: `'display'`)を使います。runtime が React `key` および wrapper `<div>` の `data-ampless-plugin` / `data-ampless-position` 属性を組むときに `${instanceId ?? name}:${id}` で resolve するので、plugin 作者が自前で namespace を埋め込む必要はありません。validator は `id` が空、制御文字を含む、64 文字超のいずれかなら descriptor を drop します。
|
|
541
|
+
|
|
542
|
+
**dedupe は position ごと。** 1 つの plugin instance が `beforeContent` と `afterContent` の両方に同じ `id` を返すのは OK(dedupe スコープが独立)。同じ position に同じ `id` を 2 回返すと最初の 1 件を残して 2 件目を warn 付きで drop します。
|
|
543
|
+
|
|
544
|
+
**複数 instance。** distinct な `instanceId` を持つ 2 つの `reading-time` instance(例: `reading-time-en` / `reading-time-jp`)は、同じ position に `id: 'display'` を返しても両方残ります(namespace が違うため)。
|
|
545
|
+
|
|
546
|
+
### クライアントサイドの DOM 操作はしない
|
|
547
|
+
|
|
548
|
+
`publicHead` または `publicBodyEnd` から返したインラインスクリプトは、React がページを hydrate する前、HTML のパース中に実行されます。**React が管理するサブツリー内の見える DOM を操作してはいけません** — hydration が走ると React は仮想 DOM と合わないツリーを検出し、`Hydration failed because the server rendered HTML didn't match the client` エラーを投げてサブツリーをゼロから再生成します。挿入したノードは消えてしまいます。
|
|
549
|
+
|
|
550
|
+
React 19 はさらに、クライアントコンポーネントのレンダー中に出会った `<script>` タグの実行を拒否するため、`document.body.append(myNewElement)` のようなスクリプトはそもそも発火しないことがあります。
|
|
551
|
+
|
|
552
|
+
**安全なパターン**:
|
|
553
|
+
|
|
554
|
+
- **グローバル状態 / 非 DOM の副作用**: `window.dataLayer` への push、設定オブジェクトのセット、アナリティクス SDK のインスタンス化。`@ampless/plugin-analytics-ga4`・`@ampless/plugin-gtm`・`@ampless/plugin-plausible` はこの方法を使っています。
|
|
555
|
+
- **外部ウィジェットローダー**: 自前の独立したコンテナを管理するサードパーティスクリプトの読み込み(Crisp・Intercom・Drift など)。ウィジェットの shadow DOM / fixed-position オーバーレイは React のツリーの外にあり、hydration と競合しません。
|
|
556
|
+
- **SSR 専用の descriptor**: `meta` / `link` / `noscript`(`publicBodyEnd` では `iframe` も)を返す — runtime がサーバーサイドで描画するため、最初から React の仮想 DOM の一部になります。
|
|
557
|
+
|
|
558
|
+
**避けるべきパターン**:
|
|
559
|
+
|
|
560
|
+
- `document.createElement('div')` + `document.body.append(...)`
|
|
561
|
+
- テーマが描画した要素のクラス / 属性 / テキストコンテンツの変更
|
|
562
|
+
- クライアントサイドで `#post-body` のような要素を読み取って投稿単位の HTML を挿入する — 現在 `publicHead`-for-post に相当するサーフェスはなく、サーバーレンダリング済みのサブツリーをクライアントサイドで書き換えると hydration と競合します
|
|
563
|
+
|
|
564
|
+
投稿単位の見える出力には `publicHtmlForPost` を使ってください(Phase 6d — 上の例と §6 の `PublicPostHtmlDescriptor` 参照)。runtime が post 本文の周囲の固定スロットにサーバーサイド HTML を出すので、hydration と競合しません。
|
|
565
|
+
|
|
566
|
+
---
|
|
567
|
+
|
|
568
|
+
## 7. 非同期イベントフック
|
|
569
|
+
|
|
570
|
+
`hooks` は SQS から到着したイベントを trust_level に対応する processor Lambda が受けて実行します。runtime context (`ctx`) の中身:
|
|
571
|
+
|
|
572
|
+
```ts
|
|
573
|
+
interface PluginRuntimeContext {
|
|
574
|
+
site: Config['site']
|
|
575
|
+
listPublishedPosts(): Promise<Post[]> // trusted のみ
|
|
576
|
+
writePublicAsset(key: string, body, contentType): Promise<string> // trusted のみ
|
|
577
|
+
}
|
|
578
|
+
```
|
|
579
|
+
|
|
580
|
+
例: RSS プラグイン ([`packages/plugin-rss/src/index.ts`](https://github.com/heavymoons/ampless/blob/main/packages/plugin-rss/src/index.ts) 参照):
|
|
581
|
+
|
|
582
|
+
```ts
|
|
583
|
+
hooks: {
|
|
584
|
+
'content.published': async (_event, ctx) => {
|
|
585
|
+
const posts = await ctx.listPublishedPosts()
|
|
586
|
+
const xml = buildRssFeed(posts, ctx.site)
|
|
587
|
+
await ctx.writePublicAsset('feed.xml', xml, 'application/rss+xml')
|
|
588
|
+
},
|
|
589
|
+
'content.unpublished': /* 同じ */,
|
|
590
|
+
'content.deleted': /* 同じ */,
|
|
591
|
+
'content.updated': /* 同じ */,
|
|
592
|
+
}
|
|
593
|
+
```
|
|
594
|
+
|
|
595
|
+
### `writePublicAsset`
|
|
596
|
+
|
|
597
|
+
公開生成ファイルを書き出す trusted plugin は capability を宣言してください:
|
|
598
|
+
|
|
599
|
+
```ts
|
|
600
|
+
capabilities: ['eventHooks', 'writePublicAsset']
|
|
601
|
+
```
|
|
602
|
+
|
|
603
|
+
同じ plugin が `metadata()` または `siteMetadata()` も実装する場合は、`metadata` も宣言します。この capability 名は両方の metadata 関数をまとめて表し、別個の `siteMetadata` capability はありません。
|
|
604
|
+
|
|
605
|
+
trusted processor は次の場所に書き込みます:
|
|
606
|
+
|
|
607
|
+
```txt
|
|
608
|
+
public/plugins/<instanceId ?? name>/<key>
|
|
609
|
+
```
|
|
610
|
+
|
|
611
|
+
`key` は allowlist `[A-Za-z0-9._/-]+` に一致する必要があります。それ以外(スペース、URL 予約文字 `#` `?` `&` `=` `+`、非 ASCII 文字 (`日本語.xml` 等)、空文字、絶対パス(`/` 始まり)、`.` / `..` path segment、backslash、制御文字、256 文字超)は S3 呼び出し前に拒否されます。`indexes/posts.json` のような nested path や `feed.v2.xml` のような複数 dot は許可されます。allowlist を厳しく絞っているのは、返却 URL と実 S3 key を byte 等しい文字列に保つため — URL 予約文字は S3 では生バイトとして通るが、URL を consumer が parse すると別 object を指す状態になる。user 由来の文字を key に入れたい場合は事前に sanitize (hash、slugify 等) してから `ctx.writePublicAsset()` を呼んでください。戻り値は書き込まれた object の public URL です。
|
|
612
|
+
|
|
613
|
+
移行期間中、`capabilities` フィールドが無い plugin はそのまま動きます。`capabilities` を宣言しているのに `writePublicAsset` を省いた plugin は、実際に `ctx.writePublicAsset()` を呼んだ時に 1 回だけ warn します。
|
|
614
|
+
|
|
615
|
+
### ベストプラクティス
|
|
616
|
+
|
|
617
|
+
- **冪等にする**。SQS は at-least-once 配送 — 同じイベントが 2 回 fire する可能性があります。同じ入力で同じ出力 (決定論的なフィード等) を生成するようにしてください
|
|
618
|
+
- **宣言していない `event.payload.*` を読まない**。形は [`docs/architecture/05-event-system.md`](https://github.com/heavymoons/ampless/blob/main/docs/architecture/05-event-system.md) に文書化されています。形がドリフトすると暗黙の読み出しが silent に壊れます
|
|
619
|
+
- **エラーは DLQ に行く**。フック内で throw すると最終的にメッセージは dead-letter queue に届きます。失敗のサーフェスは通常の CloudWatch ダッシュボードで
|
|
620
|
+
|
|
621
|
+
---
|
|
622
|
+
|
|
623
|
+
## 8. `settings.public` — admin 管理の値 (Phase 2)
|
|
624
|
+
|
|
625
|
+
`settings.public` マニフェストを宣言すると、ホストには `/admin/plugins` の編集 UI が自動で生えます。
|
|
626
|
+
|
|
627
|
+
```ts
|
|
628
|
+
settings: {
|
|
629
|
+
public: [
|
|
630
|
+
{
|
|
631
|
+
type: 'text',
|
|
632
|
+
key: 'measurementId',
|
|
633
|
+
label: { en: 'Measurement ID', ja: '測定 ID' },
|
|
634
|
+
description: { en: 'GA4 ID, blank to disable', ja: '空で無効化' },
|
|
635
|
+
pattern: '^$|^G-[A-Z0-9]+$',
|
|
636
|
+
placeholder: 'G-XXXXXXXX',
|
|
637
|
+
default: 'G-XXXXXXXX',
|
|
638
|
+
},
|
|
639
|
+
],
|
|
640
|
+
}
|
|
641
|
+
```
|
|
642
|
+
|
|
643
|
+
### 利用可能な field タイプ
|
|
644
|
+
|
|
645
|
+
| Type | 保存形 | 補足 |
|
|
646
|
+
|---|---|---|
|
|
647
|
+
| `text` | string | `pattern`、`maxLength`、`placeholder` |
|
|
648
|
+
| `textarea` | string | `rows`、`maxLength` |
|
|
649
|
+
| `url` | string | save 時に scheme チェック、`allowRelative` |
|
|
650
|
+
| `code` | string | `language` ラベル (表示用)、Phase 2.5 で専用エディタに差し替え予定 |
|
|
651
|
+
| `boolean` | boolean | チェックボックスで描画 |
|
|
652
|
+
| `number` | number | `min` / `max` / `step` |
|
|
653
|
+
| `select` | string (`options[i].value` のいずれかと一致必須) | `options` 必須 |
|
|
654
|
+
| `json` | decoded value (object / array / number / boolean) | admin form は save 前に `JSON.parse` |
|
|
655
|
+
|
|
656
|
+
### 保存形
|
|
657
|
+
|
|
658
|
+
保存される値は DynamoDB の以下に landing:
|
|
659
|
+
|
|
660
|
+
```
|
|
661
|
+
pk = 'siteconfig'
|
|
662
|
+
sk = 'plugins.<instanceId>.<fieldKey>'
|
|
663
|
+
```
|
|
664
|
+
|
|
665
|
+
trusted processor がこの行を S3 の `public/site-settings.json` にミラー。`publicHead` / `publicBodyEnd` 実行時に公開 runtime がこの file を fetch (60s `revalidate`、`site-settings` cache tag) し、描画パス内から同期読み出しできるようになります。
|
|
666
|
+
|
|
667
|
+
### required / 無効化 / 未設定 の違い
|
|
668
|
+
|
|
669
|
+
- `required: true` は save 時に empty / undefined を reject、admin form にエラー表示
|
|
670
|
+
- **string 系 field** (`text` / `textarea` / `url` / `code`) は、`required` が falsy のとき **空文字保存が valid**。これが「無効化」シグナル — 例えば GA4 では `measurementId` を空文字保存することで、プラグインを削除せず解析を停止できます
|
|
671
|
+
- **非 string 系 field** (`number` / `boolean` / `json` / `select`) は常に空文字 reject。保存値をクリアしたい場合はユーザが **デフォルトに戻す** を押す — DDB 行が削除され、次のリクエストから `manifest.default` にフォールバックします
|
|
672
|
+
|
|
673
|
+
---
|
|
674
|
+
|
|
675
|
+
## 9. 設定値の読み出し: `ctx.setting<T>(key)`
|
|
676
|
+
|
|
677
|
+
`publicHead` / `publicBodyEnd` 内で解決済みの値を読み出すには `ctx.setting`:
|
|
678
|
+
|
|
679
|
+
```ts
|
|
680
|
+
publicHead(ctx) {
|
|
681
|
+
const id = ctx.setting<string>('measurementId') ?? ''
|
|
682
|
+
if (!id) return []
|
|
683
|
+
return [/* id を使った descriptor */]
|
|
684
|
+
}
|
|
685
|
+
```
|
|
686
|
+
|
|
687
|
+
リクエスト毎の解決順序:
|
|
688
|
+
|
|
689
|
+
```
|
|
690
|
+
stored 値 (validated)
|
|
691
|
+
↳ manifest.default (これも validated)
|
|
692
|
+
↳ undefined
|
|
693
|
+
```
|
|
694
|
+
|
|
695
|
+
両側で validation を通すので、手で DDB 行を編集した結果 out-of-range になった値 (あるいは constructor 引数として渡された壊れた default) がページに漏れません。renderer は invalid 値を「存在しない」かのように扱い、次の valid 層が引き継ぎます。
|
|
696
|
+
|
|
697
|
+
スナップショットがいつ更新されるか: trusted processor が `public/site-settings.json` の再生成を完了した後、次の Next.js fetch cache TTL (60s) を過ぎたリクエストから新値を読みます。admin form は cache invalidation を ~8s 遅延発火するので、processor の S3 rebuild が完了する前に公開側が古い JSON を fetch して詰めてしまう race を避けています。
|
|
698
|
+
|
|
699
|
+
### 複数インスタンス
|
|
700
|
+
|
|
701
|
+
各プラグインインスタンスは `instanceId` でスコープされた独立 namespace を持ちます。`cms.config.ts` 内の 2 回の `analyticsGa4Plugin({ instanceId: 'a' })` と `analyticsGa4Plugin({ instanceId: 'b' })` は別々の DDB 行を見ます。`ctx.setting()` は自プラグインの `instanceId` に自動スコープします。
|
|
702
|
+
|
|
703
|
+
---
|
|
704
|
+
|
|
705
|
+
## 9a. Secret settings: `ctx.secret<T>(key)` (Phase 6a)
|
|
706
|
+
|
|
707
|
+
Secret settings を使うと、trusted プラグインが認証情報 (Webhook 署名 secret・SMTP パスワード・外部 API トークン等) を admin UI 経由で保存・ローテーションできます。**公開サイトやブラウザ側コードに値が流れることはありません**。
|
|
708
|
+
|
|
709
|
+
詳しいリファレンスは [`packages/ampless/docs/plugin-author-guide.md`](../../packages/ampless/docs/plugin-author-guide.md) §9a を参照してください。概要:
|
|
710
|
+
|
|
711
|
+
- `settings.secret` は `trust_level: 'trusted'` + `'secretSettings'` capability 必須。鍵の初回セットアップ: `npx create-ampless setup-encryption-key` を実行して `amplify/secrets/encryption-key.ts` を生成し、`defineAmplessBackend({ pluginSecretEncryptionKey })` に渡してデプロイ(AWS 認証情報不要)。
|
|
712
|
+
- フィールド型は `PluginSecretField` = `Omit<PluginTextField, 'default'> | Omit<PluginTextareaField, 'default'>`。`default` は型レベルで禁止。fallback は closure-private 変数で保持。
|
|
713
|
+
- admin ブラウザは AppSync mutation (`setPluginSecret`) 経由で書き込む → `plugin-secret-handler` Lambda が env var から鍵取得・AES-256-GCM 暗号化・DDB PutItem。平文は DDB に保存されずブラウザにも返らない。`ctx.secret<T>(key)` は trusted Lambda が復号した平文を返す(`process.env.PLUGIN_SECRET_ENCRYPTION_KEY` を使用; DDB に鍵は保存しない)。per-invocation でキャッシュ。
|
|
714
|
+
- admin UI が「保存済み」表示 (`••••••••`) + Replace + Clear を提供。値は取得・表示されない。
|
|
715
|
+
- **Dual-write 整合性**: set/clear は 2 テーブルに連続して書く。2 回目失敗時 — set パス部分失敗は「secret 機能する・indicator 不在(UI: 未保存誤表示)」; clear パス部分失敗は「secret 削除済・indicator stale(UI: 保存済み誤表示、secret は発火しない安全側)」。
|
|
716
|
+
- **field manifest 検証の範囲**: admin クライアントは UX フィードバック用に `pattern` / `maxLength` / `required` を検証するが、Lambda は **汎用 10,000 文字キャップと安全文字サニタイザのみ**を強制。admin/editor が AppSync mutation を直接呼ぶと field 単位の制約は迂回可能 — admin/editor は信頼されたオペレータと扱う設計のため、manifest チェックはセキュリティ境界ではなく UX ガイダンス。
|
|
717
|
+
|
|
718
|
+
---
|
|
719
|
+
|
|
720
|
+
## 10. ウォークスルー: GA4 を Phase 1 から Phase 2 に移行する
|
|
721
|
+
|
|
722
|
+
Phase 1 の GA4 プラグインは measurement ID を constructor 引数で受けていました。Phase 2 では後方互換のためにその引数を残しつつ、値は `ctx.setting()` 経由で読みます。
|
|
723
|
+
|
|
724
|
+
**Before** (Phase 1):
|
|
725
|
+
|
|
726
|
+
```ts
|
|
727
|
+
export default function analyticsGa4Plugin(opts: { measurementId: string }) {
|
|
728
|
+
const { measurementId } = opts
|
|
729
|
+
return definePlugin({
|
|
730
|
+
name: 'analytics-ga4',
|
|
731
|
+
apiVersion: 1,
|
|
732
|
+
trust_level: 'untrusted',
|
|
733
|
+
capabilities: ['publicHead'],
|
|
734
|
+
publicHead() {
|
|
735
|
+
if (!measurementId) return []
|
|
736
|
+
return [/* measurementId を使う descriptor */]
|
|
737
|
+
},
|
|
738
|
+
})
|
|
739
|
+
}
|
|
740
|
+
```
|
|
741
|
+
|
|
742
|
+
**After** (Phase 2):
|
|
743
|
+
|
|
744
|
+
```ts
|
|
745
|
+
export default function analyticsGa4Plugin(opts: { measurementId?: string } = {}) {
|
|
746
|
+
const { measurementId = '', instanceId = 'analytics-ga4' } = opts
|
|
747
|
+
return definePlugin({
|
|
748
|
+
name: 'analytics-ga4',
|
|
749
|
+
instanceId,
|
|
750
|
+
apiVersion: 1,
|
|
751
|
+
trust_level: 'untrusted',
|
|
752
|
+
capabilities: ['publicHead', 'adminSettings'],
|
|
753
|
+
settings: {
|
|
754
|
+
public: [{
|
|
755
|
+
type: 'text',
|
|
756
|
+
key: 'measurementId',
|
|
757
|
+
label: { en: 'Measurement ID', ja: '測定 ID' },
|
|
758
|
+
pattern: '^$|^G-[A-Z0-9]+$',
|
|
759
|
+
default: measurementId,
|
|
760
|
+
}],
|
|
761
|
+
},
|
|
762
|
+
publicHead(ctx) {
|
|
763
|
+
const id = ctx.setting<string>('measurementId') ?? ''
|
|
764
|
+
if (!id) return []
|
|
765
|
+
return [/* id を使う descriptor */]
|
|
766
|
+
},
|
|
767
|
+
})
|
|
768
|
+
}
|
|
769
|
+
```
|
|
770
|
+
|
|
771
|
+
constructor 引数は `manifest.default` の seed になります。`cms.config.ts` で既に `analyticsGa4Plugin({ measurementId: 'G-X' })` を渡している運用者は挙動変化なし。新規デプロイは空にして admin UI 側で設定する運用が推奨です。
|
|
772
|
+
|
|
773
|
+
---
|
|
774
|
+
|
|
775
|
+
## 11. テスト
|
|
776
|
+
|
|
777
|
+
ampless は vitest を使っています。典型的なプラグインテストはこんな形:
|
|
778
|
+
|
|
779
|
+
```ts
|
|
780
|
+
import { describe, it, expect } from 'vitest'
|
|
781
|
+
import type { PluginPublicRenderContext, AmplessPlugin } from 'ampless'
|
|
782
|
+
import { resolvePluginSettings } from 'ampless'
|
|
783
|
+
import myPlugin from './index.js'
|
|
784
|
+
|
|
785
|
+
function makeCtx(plugin: AmplessPlugin, stored: Record<string, unknown> = {}): PluginPublicRenderContext {
|
|
786
|
+
const resolved = resolvePluginSettings(plugin.settings, stored)
|
|
787
|
+
return {
|
|
788
|
+
site: { name: 'Test', url: 'https://example.com/' },
|
|
789
|
+
setting: (k) => resolved[k],
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
it('measurementId 設定時に descriptor を吐く', () => {
|
|
794
|
+
const plugin = myPlugin({ measurementId: 'G-XXX' })
|
|
795
|
+
const descriptors = plugin.publicHead?.(makeCtx(plugin)) ?? []
|
|
796
|
+
expect(descriptors).toHaveLength(2)
|
|
797
|
+
})
|
|
798
|
+
|
|
799
|
+
it('admin が空文字保存した場合は空配列', () => {
|
|
800
|
+
const plugin = myPlugin({ measurementId: 'G-XXX' })
|
|
801
|
+
const descriptors = plugin.publicHead?.(makeCtx(plugin, { measurementId: '' })) ?? []
|
|
802
|
+
expect(descriptors).toEqual([])
|
|
803
|
+
})
|
|
804
|
+
```
|
|
805
|
+
|
|
806
|
+
マニフェスト + 描画挙動をテストすればよいです。runtime の descriptor validator は `@ampless/runtime` 側でテストされているので、プラグインテストは「何の状態でどの descriptor を返すか」に集中してください。
|
|
807
|
+
|
|
808
|
+
イベントフックのテストは `ctx.listPublishedPosts` と `ctx.writePublicAsset` を単純なスタブ関数で差し替えます。
|
|
809
|
+
|
|
810
|
+
---
|
|
811
|
+
|
|
812
|
+
## 12. npm publish
|
|
813
|
+
|
|
814
|
+
ファーストパーティ / モノレポ内部のプラグインは本レポの既存 changeset フローに従ってください。外部プラグインは通常の npm パッケージ:
|
|
815
|
+
|
|
816
|
+
- **パッケージ名**: `@your-scope/plugin-foo`。`@ampless/plugin-*` スコープは本モノレポから ship する公式プラグイン用に予約
|
|
817
|
+
- **エントリ**: ESM のみ、default export (factory) + 設定インターフェイス (ユーザの `cms.config.ts` から型付きで引数を渡せるように) を export
|
|
818
|
+
- **`apiVersion`**: 契約が変わるときだけ bump。既存フィールドの型が変わったら major、新フィールド追加なら minor (既存インストールは動き続ける)
|
|
819
|
+
- **Dist-tag**: ampless 自体が alpha のうちは `@alpha`。`@latest` は ampless v1.0 まで予約
|
|
820
|
+
|
|
821
|
+
参考実装:
|
|
822
|
+
|
|
823
|
+
- [`packages/plugin-analytics-ga4`](https://github.com/heavymoons/ampless/tree/main/packages/plugin-analytics-ga4) — descriptor ベース、Phase 2 settings
|
|
824
|
+
- [`packages/plugin-gtm`](https://github.com/heavymoons/ampless/tree/main/packages/plugin-gtm) — `publicHead`(ローダーインラインスクリプト)+ `publicBodyEnd`(`<noscript>` iframe フォールバック)を両方使用、コンテナ ID は admin 編集可能
|
|
825
|
+
- [`packages/plugin-plausible`](https://github.com/heavymoons/ampless/tree/main/packages/plugin-plausible) — `data-*` attrs 付きの単一 `<script>` descriptor、`required` な URL field(self-hosted Plausible 上書き対応)
|
|
826
|
+
- [`packages/plugin-rss`](https://github.com/heavymoons/ampless/tree/main/packages/plugin-rss) — trusted、非同期 hooks + `writePublicAsset`
|
|
827
|
+
- [`packages/plugin-seo`](https://github.com/heavymoons/ampless/tree/main/packages/plugin-seo) — `metadata()` + `siteMetadata()`
|
|
828
|
+
- [`packages/plugin-webhook`](https://github.com/heavymoons/ampless/tree/main/packages/plugin-webhook) — untrusted hook + 外向き HTTP
|
|
829
|
+
- [`packages/plugin-og-image`](https://github.com/heavymoons/ampless/tree/main/packages/plugin-og-image) — `ogImage` ルートレンダラ
|
|
830
|
+
- [`packages/plugin-schema-jsonld`](https://github.com/heavymoons/ampless/tree/main/packages/plugin-schema-jsonld) — `publicBodyForPost` + `schema` capability、投稿単位 Article JSON-LD。(Phase 4)
|
|
831
|
+
|
|
832
|
+
---
|
|
833
|
+
|
|
834
|
+
## 13. 命名規則とよくある落とし穴
|
|
835
|
+
|
|
836
|
+
### 命名規則
|
|
837
|
+
|
|
838
|
+
- `name`、`instanceId`、`settings.public.key` のすべて: `/^[a-zA-Z0-9_-]+$/` 必須。違反した plugin / field は dev console warning 付きで runtime が drop します
|
|
839
|
+
- `plugins.<instanceId>.<fieldKey>` のドット区切りが保存形式の唯一の構造です。key 側にネストしたドットを入れて凝らないでください
|
|
840
|
+
|
|
841
|
+
### よくある落とし穴
|
|
842
|
+
|
|
843
|
+
- **capability と実装の不一致**。`capabilities: ['publicHead']` を宣言したのに `publicHead` を未定義 (またはその逆) にすると、起動時に console warning が出ます。capability を外すか関数を追加してください
|
|
844
|
+
- **`instanceId` 重複**。同じ namespace を共有する 2 つのインスタンスは起動時に警告が出ます。後者の保存設定は前者と衝突します
|
|
845
|
+
- **`inlineScript` の `id` 忘れ**。production では silent に drop、dev では warn。inline script の dedup は id 無しではできません
|
|
846
|
+
- **`publicHead` から `ReactNode` を返す**。TypeScript で弾かれます — `publicHead` の戻り値型は descriptor のみ。任意 `ReactNode` が必要なら、それは Phase 6b の `developer.headElements` capability 待ち
|
|
847
|
+
- **admin form から `manifest.default` を保存してしまう**。resolved default を「明示値」として書き戻さないでください — admin form が touched フィールドのみ書き込む設計はまさにこのため。default 値を保存すると future のパッケージ更新で default が変わってもそのフィールドだけ反映されなくなります
|
|
848
|
+
- **`publicBodyForPost` で `scriptType: 'application/ld+json'` を省略**。`publicBodyForPost` が返す descriptor で `scriptType` を省略するか他の値を指定すると、production では silent に drop、dev では warn されます。このサーフェスで有効なのは `'application/ld+json'` だけです
|
|
849
|
+
- **`schema` capability と `publicBodyForPost` の不一致**。`capabilities: ['schema']` を宣言して `publicBodyForPost` を未実装(またはその逆)にすると起動時に warning が出ます。宣言と実装は同期させてください
|
|
850
|
+
|
|
851
|
+
---
|
|
852
|
+
|
|
853
|
+
## 14. クイックスタート: `create-ampless` でスキャフォールド
|
|
854
|
+
|
|
855
|
+
アイデアから動くプラグインへの最速ルートとして、`create-ampless` CLI には `plugin <name>` サブコマンドが用意されています:
|
|
856
|
+
|
|
857
|
+
```bash
|
|
858
|
+
# サイトローカル: 現在の ampless サイトのルートで実行
|
|
859
|
+
# plugins/<name>/index.ts を生成する
|
|
860
|
+
npx create-ampless@latest plugin my-thing \
|
|
861
|
+
--trust-level untrusted \
|
|
862
|
+
--capabilities publicHead,adminSettings
|
|
863
|
+
|
|
864
|
+
# スタンドアロン npm パッケージ: ./<dir>/ を生成
|
|
865
|
+
# package.json / tsconfig.json / tsup.config.ts / README + .ja /
|
|
866
|
+
# CHANGELOG / .gitignore / src/index.ts + src/index.test.ts を含む
|
|
867
|
+
# 新しいパッケージディレクトリを置きたい場所で実行する
|
|
868
|
+
npx create-ampless@latest plugin @myscope/ampless-plugin-thing \
|
|
869
|
+
--standalone \
|
|
870
|
+
--trust-level untrusted \
|
|
871
|
+
--capabilities publicHead,adminSettings \
|
|
872
|
+
--description "このプラグインが何をするか"
|
|
873
|
+
```
|
|
874
|
+
|
|
875
|
+
スタンドアロンスキャフォールドには Phase 5 のクロスチェックに必要なものがすべて含まれます: `package.json#amplessPlugin`、`./package.json` サブパスエクスポート、`packageName` ファクトリフィールド、`ampless-plugin` 検索キーワード、そして `pnpm install && pnpm test && pnpm build` が生成直後にクリーンに通る最小の vitest サンプル。
|
|
876
|
+
|
|
877
|
+
どちらのモードも、フラグなしの位置引数呼び出し (`npx create-ampless@latest plugin`) で @clack のプロンプト UI を使ったインタラクティブモードに切り替えられます。
|
|
878
|
+
|
|
879
|
+
### スタンドアロンプラグインの公開
|
|
880
|
+
|
|
881
|
+
```bash
|
|
882
|
+
cd ampless-plugin-thing
|
|
883
|
+
pnpm install
|
|
884
|
+
pnpm test
|
|
885
|
+
pnpm build
|
|
886
|
+
pnpm publish --access public --tag alpha
|
|
887
|
+
```
|
|
888
|
+
|
|
889
|
+
スコープ付き名前 (`@scope/...`) には `--access public` が必須です。`--tag alpha` は現在の ampless プレリリースサイクルに合わせています — 安定 major に達したら外してください。
|
|
890
|
+
|
|
891
|
+
`npm publish` が返った直後に `npm install <pkg>@alpha` で 404 が出ることがあります(CDN とレジストリレプリカの伝播遅延)。その場合は 1〜2 分待ってリトライしてください — `npm view <pkg>@alpha version` がレジストリで見えていることは必要条件ですが十分条件ではありません。
|
|
892
|
+
|
|
893
|
+
### パッケージの命名
|
|
894
|
+
|
|
895
|
+
npm の慣例として、スコープと `ampless-plugin-` プレフィックスを除いた短い識別子が `AmplessPlugin.name` になります:
|
|
896
|
+
|
|
897
|
+
| npm パッケージ | `AmplessPlugin.name` |
|
|
898
|
+
|---|---|
|
|
899
|
+
| `@ampless/plugin-gtm` | `gtm` |
|
|
900
|
+
| `@scope/ampless-plugin-clarity` | `clarity` |
|
|
901
|
+
| `ampless-plugin-readme-toc` | `readme-toc` |
|
|
902
|
+
| `weird-name-no-prefix` | `weird-name-no-prefix` |
|
|
903
|
+
|
|
904
|
+
スキャフォールドはこのストリッピングを自動で行います。スキャフォールドを使わない場合も同じマッピングで手書きしてください。パッケージの静的マニフェストとファクトリで `name` が一致しない場合はインストール時クロスチェックが警告します。
|
|
905
|
+
|
|
906
|
+
### サイトローカルの後作業
|
|
907
|
+
|
|
908
|
+
サイトローカルのスキャフォールド後:
|
|
909
|
+
|
|
910
|
+
```ts
|
|
911
|
+
// cms.config.ts
|
|
912
|
+
import myThingPlugin from './plugins/my-thing'
|
|
913
|
+
|
|
914
|
+
export default defineConfig({
|
|
915
|
+
// ...
|
|
916
|
+
plugins: [
|
|
917
|
+
myThingPlugin(),
|
|
918
|
+
],
|
|
919
|
+
})
|
|
920
|
+
```
|
|
921
|
+
|
|
922
|
+
スキャフォールドの最後にこのスニペットが表示されます — `cms.config.ts` にコピーしてプラグインを有効化してください。
|
|
923
|
+
|
|
924
|
+
`update-ampless` は `plugins/` ディレクトリを決して変更しません(PROTECTED 扱い)。ampless のアップグレードをまたいでも安全に残ります。
|
|
925
|
+
|
|
926
|
+
---
|
|
927
|
+
|
|
928
|
+
## 15. 質問先
|
|
929
|
+
|
|
930
|
+
- アーキテクチャ / 設計の質問 → [`docs/architecture/08-plugin-architecture.ja.md`](https://github.com/heavymoons/ampless/blob/main/docs/architecture/08-plugin-architecture.ja.md)
|
|
931
|
+
- ファーストパーティプラグインの bug → `heavymoons/ampless` にプラグインの package 名つきで issue
|
|
932
|
+
- プラグインランタイム / admin form の bug → 同じレポ、ラベル `area:plugins`
|
|
933
|
+
|
|
934
|
+
ampless レポは v1.0 RC まで非公開なので、上記のリンクは現時点では package tarball 内の `node_modules/ampless/docs/` をローカルで見るための参照です。public 化後は実 GitHub URL に解決されます。
|