article
テーマとプラグインの使い分け
ampless でカスタムテーマとプラグインをどう使い分けるか。公開ページの見た目を変えるテーマ、投稿イベントや計測タグを扱うプラグイン、という境界から紹介します。
ampless でサイトを拡張するとき、最初に迷いやすいのは「これはカスタムテーマで直す話か、プラグインとして足す話か」だと思います。
ampless の設計では、テーマは公開ページを組み立てる層です。トップページにどのセクションを置くか。記事一覧をカードにするか、リストにするか。本文の余白、見出し、サイドバー、タグ一覧をどう見せるか。ページを開いた人が直接見るものは、まずテーマ側で考えます。
<head> に meta や script を追加するだけなら、テーマでもできます。たとえば themes/my-docs/pages/post.tsx の metadata 生成や、共通レイアウトに直接タグを書く方法でも、特定サイトの特定テーマには反映できます。
それでもプラグインに分ける利点があります。GA4 の測定 ID、GTM のコンテナ ID、Plausible のドメインのように、テーマを切り替えても同じ設定を使いたいもの。複数サイトへ同じ仕組みを持っていきたいもの。あるいは /admin/plugins から値だけ変えたいもの。そういう処理は、テーマファイルに直接書くより、cms.config.ts の plugins 配列に登録しておくほうが管理しやすくなります。
RSS や sitemap の再生成、Webhook 配送のように、投稿イベントのあとで非同期に処理したいものもプラグイン向きです。これはテーマだけではできません。テーマはリクエストされたページを描画しますが、プラグインの hook は投稿が変わったあとに processor Lambda で起動し、公開済み投稿を読み直したり、S3 に生成ファイルを書き出したりできます。
この記事では、この境界から始めて、ampless のプラグイン機構を 2026 年 6 月 14 日時点 の実装ベースで整理します。publicHtmlForPost による本文前後の可視 HTML、contentFields による本文内 embed、admin で管理できる secret settings、package manifest のクロスチェックまで入っています。一方で、マーケットプレイス型のインストールや privileged プラグインはまだ v1.0 の必須条件には入れていません。ここで書く内容は、いま使える範囲を説明するためのものとして読んでください。
テーマとプラグインの違い
テーマは、公開サイトの画面を決めます。themes/my-* の中にページコンポーネント、CSS トークン、記事ページ、タグページ、ホーム画面を置きます。管理画面でテーマを切り替えると、同じ投稿データでも見え方が変わります。
プラグインは、テーマを替えても残したい処理を登録します。たとえば @ampless/plugin-rss は、投稿が公開されたあとに公開済み投稿を読み直し、RSS XML を public/plugins/rss/feed.xml に書き出します。この処理は、ブログテーマでも docs テーマでも同じです。だからテーマの中ではなく、プラグインとして分けています。
| やりたいこと | 置く場所 |
|---|---|
| トップページの構成を変える | カスタムテーマ |
| 記事ページの余白、見出し、フォントを変える | カスタムテーマ |
| サイドバーや記事カードの並べ方を変える | カスタムテーマ |
| 本文の前後に読了時間や注意書きのような可視 UI を置く | プラグイン + 対応テーマ |
| 本文中の任意位置に複雑な UI を組み込む | カスタムテーマ |
| 特定テーマだけで使う meta / script を固定で入れる | カスタムテーマ |
| GA4 / GTM / Plausible の設定をテーマ変更後も同じ画面で管理する | プラグイン |
| sitemap / RSS / JSON index を生成する | プラグイン |
| 投稿公開時に外部サービスへ通知する | プラグイン |
/admin/plugins から測定 ID やドメインだけ変えたい |
プラグイン |
| 同じ機能を別サイトにも持っていきたい | プラグイン |
判断に迷ったら、次の順番で見るとだいたい整理できます。
- 画面の配置や見た目を変えるだけなら、テーマ。
- そのテーマだけに固定で入れる head タグなら、テーマでも十分。
- 投稿公開後に S3 へファイルを書き出す、外部 API を呼ぶ、Webhook を送るなら、プラグイン。
- 管理画面から設定値だけ変えたいなら、プラグイン。
- その機能を別テーマや別サイトでも使いたいなら、プラグイン。
ampless は、本文中の任意位置へプラグインが自由に割り込む汎用 slot は持っていません。本文の細かい配置や複雑な UI は、いまもテーマコンポーネントが決めます。
ただし、投稿本文の直前・直後に限れば publicHtmlForPost() が使えます。プラグインは beforeContent / afterContent のどちらに出したいかを descriptor で返し、対応テーマが ampless.publicHtmlForPost(post) を呼んで描画します。HTML は runtime が sanitize-html の allowlist を通してからテーマへ渡すので、読了時間バッジ、breadcrumb、関連記事リンクのような軽い可視要素を、テーマをまたいで共有できます。
また、inline script で React 管理下の可視 DOM をあとから差し込む書き方は避けます。Next.js の hydration と衝突し、挿入した要素が消えたり、ページの一部が作り直されたりします。プラグインの script は、window.dataLayer に値を積む処理や、外部ウィジェット自身が持つ独立したコンテナの初期化に留めるほうが安全です。
プラグインが使う入口
ampless のプラグインは、definePlugin() が返す plain object です。プラグインごとに、どの入口を使うかを宣言します。
| 入口 | 何をするか | 実行される場所 |
|---|---|---|
metadata(post, site) |
投稿ごとの title、OGP、canonical などを返す | 公開 Next.js プロセス |
siteMetadata(site) |
サイト全体の RSS link や metadata を返す | 公開 Next.js プロセス |
publicHead(ctx) |
<head> に meta、link、script などを追加する |
公開 Next.js プロセス |
publicBodyEnd(ctx) |
<body> 末尾に script、noscript、iframe を追加する |
公開 Next.js プロセス |
publicBodyForPost(post, ctx) |
投稿ページに JSON-LD script を追加する | テーマの post ページ |
publicHtmlForPost(post, ctx) |
投稿本文の前後に、サニタイズ済みの可視 HTML を追加する | テーマの post ページ |
contentFields |
投稿本文の一部を、プラグイン提供の React 要素に差し替える | ampless.renderBody(post) |
publicPostScript(post, ctx) |
投稿本文が必要とする page-level script を追加する | テーマの post ページ |
hooks |
投稿イベントを受けて RSS 再生成や Webhook 配送を行う | processor Lambda |
ogImage |
/og/<slug> の画像を描画する |
公開 Next.js ルート |
settings.public |
/admin/plugins に公開設定フォームを出す |
管理画面 + 公開設定キャッシュ |
settings.secret |
/admin/plugins に secret 設定フォームを出し、trusted hook から読む |
管理画面 + trusted processor Lambda |
publicHead() や publicBodyEnd() は、テーマでも書ける head/body 追加を、プラグインとして共通管理するための入口です。プラグインは descriptor を返し、runtime が URL や属性を検証してから HTML に変換します。測定 ID やドメインを /admin/plugins から変えたい場合や、複数テーマで同じタグを使いたい場合に効いてきます。
publicHtmlForPost() は、本文の直前または直後に見える HTML を足す入口です。テーマは {html.beforeContent} / {html.afterContent} を置く場所を決め、runtime は descriptor の検証、namespace ごとの dedupe、HTML のサニタイズを担当します。@ampless/plugin-reading-time はこの surface を使っています。
contentFields は、本文の中にある特定の fragment をプラグイン側の renderer へ渡す入口です。@ampless/plugin-youtube は YouTube URL を iframe に、@ampless/plugin-x-embed は x.com / twitter.com の投稿 URL を tweet blockquote に変換します。publicPostScript() は x.com の widgets.js のように、ページ上に該当する埋め込みがあるときだけ読みたい script を 1 回だけ出すために使います。
RSS や sitemap のように、公開済み投稿を読み直して S3 にファイルを書き出す場合は、hooks を使います。ここがカスタムテーマとの大きな違いです。テーマはリクエストが来たページを描画しますが、trusted プラグインの hook は投稿更新のあとに非同期で起動し、必要なデータを読み直して、公開サイトがあとで読むファイルを保存します。
イベントで起動するプラグイン
SEO と RSS は、どちらも投稿イベントを購読します。購読しているのは次の四つです。
| イベント | いつ出るか | RSS / sitemap で必要な理由 |
|---|---|---|
content.published |
下書きが公開になった、または公開状態の投稿が作られた | 新しい URL を feed / sitemap に入れる |
content.unpublished |
公開済み投稿が下書きに戻った、または削除前に公開状態から外れた | feed / sitemap から外す |
content.deleted |
投稿行が削除された | 残っている公開投稿だけで作り直す |
content.updated |
投稿が更新された | 公開済み投稿の title、slug、excerpt、publishedAt などが変わった場合に反映する |
流れは次のようになります。
flowchart TD
Write["管理 UI / MCP<br/>Post を作成・更新・削除"] --> Stream["DynamoDB Stream"]
Stream --> Dispatcher["event-dispatcher Lambda<br/>イベント種別を判定"]
Dispatcher --> TrustedQueue["SQS: TrustedEventsQueue"]
Dispatcher --> UntrustedQueue["SQS: UntrustedEventsQueue"]
TrustedQueue --> TrustedProcessor["processor-trusted Lambda"]
UntrustedQueue --> UntrustedProcessor["processor-untrusted Lambda"]
TrustedProcessor --> TrustedHooks["trusted hooks を呼ぶ"]
UntrustedProcessor --> UntrustedHooks["untrusted hooks を呼ぶ"]
イベント payload には、本文全体ではなく小さな投稿情報だけが入ります。postId、slug、title、status、publishedAt、tags などです。Webhook のように「何が起きたか」を外部へ知らせるだけなら、この payload をそのまま送れます。
RSS や sitemap は、それだけでは足りません。feed には最新の公開投稿をまとめて入れる必要があり、sitemap も公開中の URL 一覧として作り直す必要があります。そのため trusted hook の中で ctx.listPublishedPosts() を呼びます。これは Post テーブルの byStatus GSI を status = published で Query し、公開済み投稿だけを新しい順に返します。戻ってくる投稿には postId、slug、title、excerpt、format、body、publishedAt、tags が含まれます。
生成した結果は ctx.writePublicAsset(key, body, contentType) で保存します。保存先は必ず、プラグインごとの namespace の下です。
| プラグイン | 呼び出し | 実際の保存先 |
|---|---|---|
| SEO | ctx.writePublicAsset('sitemap.xml', xml, 'application/xml; charset=utf-8') |
public/plugins/seo/sitemap.xml |
| RSS | ctx.writePublicAsset('feed.xml', xml, 'application/rss+xml; charset=utf-8') |
public/plugins/rss/feed.xml |
| custom index | ctx.writePublicAsset('indexes/posts.json', json, 'application/json; charset=utf-8') |
public/plugins/<plugin-name>/indexes/posts.json |
writePublicAsset() は S3 の PutObject を行います。key には絶対パス、. / .. segment、backslash、制御文字、長すぎる文字列を使えません。runtime が S3 へ送る前に検証します。返り値は、保存された object の public URL です。
この仕組みにすると、公開ページのリクエスト中に毎回 RSS を組み立てる必要がありません。投稿が変わったタイミングで XML を作り直し、/feed.xml や /sitemap.xml の route handler は、その保存済みファイルを読むだけで済みます。
trust_level
プラグインには trust_level があります。これは「そのプラグインの hooks が、どの Lambda で実行されるか」を決めるための値です。公開ページで publicHead() を呼ぶときの権限ではありません。
| trust_level | いまの用途 |
|---|---|
untrusted |
head/body 注入、metadata、publicHtmlForPost など。CMS データへ直接触る AWS 権限は渡さない |
trusted |
公開済み投稿を読む、public/plugins/<name>/... に生成ファイルを書く、admin 管理の secret を読む |
privileged |
予約済み。SES、private S3、独自テーブル、server route 追加のような強い権限が必要な拡張の候補 |
サンドボックスは V8 isolate や vm.Script ではありません。ampless は Lambda の IAM 実行ロールで境界を作ります。untrusted Lambda には CMS データへ触る権限を渡さず、trusted Lambda には公開ファイル生成に必要な権限を渡します。
privileged は型としては存在しますが、現時点では専用 Lambda がありません。署名 secret を持つ Webhook のような用途は、trusted プラグインの secretSettings で扱えるようになりました。一方で、ユーザー投稿フォームを独自テーブルに保存する、SES でメール送信する、private S3 を触る、といった処理は、まだプラグインとしての本番ルートを切っていません。必要な場合は、通常の Next.js / Amplify 実装としてサイト側に置くほうが読みやすいです。
公式プラグイン
いま用意している公式プラグインは、次のような役割です。
| パッケージ | trust_level | 何をするか |
|---|---|---|
@ampless/plugin-seo |
trusted | 投稿・サイトの SEO metadata を返し、公開イベント時に sitemap.xml を再生成する |
@ampless/plugin-rss |
trusted | 公開済み投稿から RSS フィードを作り、public/plugins/rss/feed.xml に書き出す |
@ampless/plugin-og-image |
untrusted | /og/<slug> で投稿ごとの OGP 画像を描画する |
@ampless/plugin-webhook |
trusted | content.published などのイベントを外部 URL へ POST する。HMAC 署名 secret は /admin/plugins で管理できる |
@ampless/plugin-analytics-ga4 |
untrusted | GA4 の loader と init script を <head> に追加する。測定 ID は /admin/plugins で編集できる |
@ampless/plugin-gtm |
untrusted | Google Tag Manager の loader と noscript fallback を追加する。コンテナ ID は admin から変更できる |
@ampless/plugin-plausible |
untrusted | Plausible Analytics の script を追加する。self-hosted の script URL も設定できる |
@ampless/plugin-schema-jsonld |
untrusted | 投稿ページに Article / BlogPosting などの JSON-LD を追加する |
@ampless/plugin-cookie-consent |
untrusted | Cookie 同意バナーと window.amplessConsent 規約を追加する。GA4 / GTM / Plausible の consent gating と組み合わせられる |
@ampless/plugin-reading-time |
untrusted | publicHtmlForPost で読了時間バッジを本文の前後に挿入する。ラベルや位置は admin から変更できる |
@ampless/plugin-highlight |
untrusted | language-* のコードブロックを highlight.js でハイライトする。対象ブロックがあるページだけ CDN から読み込む |
@ampless/plugin-mermaid |
untrusted | language-mermaid のコードブロックを Mermaid 図として描画する。highlight とは順序非依存で共存する |
@ampless/plugin-youtube |
trusted | YouTube URL を youtube-nocookie.com の iframe として投稿本文内に描画する |
@ampless/plugin-x-embed |
trusted | x.com / twitter.com の投稿 URL を tweet embed として描画し、必要なページで widgets.js を読み込む |
標準プラグインの用途を横断して見たい場合は、標準プラグインでできること に一覧を分けました。
scaffold 直後の cms.config.ts では、SEO と RSS が有効です。GA4、GTM、Plausible、Schema JSON-LD、Cookie consent、Reading time、Webhook、OG image はコメント付きの例として並びます。必要になったら import のコメントを外し、plugins 配列に追加して再デプロイします。Highlight と Mermaid は publicHead だけで動くため、cms.config.ts に登録すれば Markdown のコードフェンスがそのまま対象になります。YouTube と x.com の埋め込みは editor extension も関わるため、パッケージ追加と editor bootstrap の配線を別途行います。
import { defineConfig } from 'ampless'
import seoPlugin from '@ampless/plugin-seo'
import rssPlugin from '@ampless/plugin-rss'
import analyticsGa4Plugin from '@ampless/plugin-analytics-ga4'
export default defineConfig({
site: {
name: 'My Site',
url: 'https://example.com',
},
plugins: [
seoPlugin(),
rssPlugin({ limit: 20 }),
analyticsGa4Plugin({
measurementId: '', // 後から /admin/plugins で設定する
}),
],
})
GA4、GTM、Plausible、Schema JSON-LD、Cookie consent、Reading time は settings.public を持っています。一度 cms.config.ts に登録してデプロイすれば、測定 ID やドメイン、schema.org の種類、同意カテゴリ、読了時間の表示位置などは /admin/plugins から変更できます。Webhook は settings.secret を持っていて、HMAC 署名 secret を再デプロイなしでローテーションできます。Highlight と Mermaid は constructor option で CDN から読むバージョンやテーマを選ぶタイプです。YouTube と x.com の埋め込みは本文 renderer と editor extension の拡張なので、管理画面設定よりもコード配線が中心です。
サイトローカルなプラグイン
公開 npm パッケージにするほどではない拡張なら、サイトのリポジトリ内に置きます。たとえば plugins/local-meta.ts を作り、cms.config.ts から import します。
// plugins/local-meta.ts
import { definePlugin, type AmplessPlugin } from 'ampless'
export default function localMetaPlugin(): AmplessPlugin {
return definePlugin({
name: 'local-meta',
apiVersion: 1,
trust_level: 'untrusted',
capabilities: ['publicHead'],
publicHead() {
return [
{
type: 'meta',
name: 'x-site',
content: 'heavymoons',
},
]
},
})
}
// cms.config.ts
import localMetaPlugin from './plugins/local-meta'
export default defineConfig({
site: { name: 'My Site', url: 'https://example.com' },
plugins: [localMetaPlugin()],
})
このくらいの小さなプラグインなら、サイト固有の meta/link、軽い JSON-LD、特定サービス向けのタグ追加に向いています。公開 runtime は descriptor を検証するため、javascript: URL や許可されていない属性は HTML に出ません。
admin で変えられる設定
測定 ID や公開ドメインのように、デプロイ後も値だけ変えたい場合は settings.public を宣言します。すると /admin/plugins にフォームが出ます。settings.public は公開サイトから読める値なので、API キーや署名 secret は置きません。小さな例として、ブラウザ UI に渡す theme-color を管理画面から変えられるプラグインを書くと、次のようになります。
import { definePlugin, type AmplessPlugin } from 'ampless'
export default function themeColorPlugin(defaultColor = '#0f172a'): AmplessPlugin {
return definePlugin({
name: 'theme-color',
apiVersion: 1,
trust_level: 'untrusted',
displayName: { ja: 'テーマカラー', en: 'Theme color' },
capabilities: ['publicHead', 'adminSettings'],
settings: {
public: [
{
type: 'text',
key: 'color',
label: { ja: 'テーマカラー', en: 'Theme color' },
description: {
ja: 'ブラウザ UI に渡す theme-color。例: #0f172a',
en: 'Theme color passed to browser UI. Example: #0f172a',
},
pattern: '^#[0-9a-fA-F]{6}$',
default: defaultColor,
},
],
},
publicHead(ctx) {
const color = (ctx.setting<string>('color') ?? '').trim()
if (!color) return []
return [{ type: 'meta', name: 'theme-color', content: color }]
},
})
}
署名 secret や外部 API の秘密値が必要な場合は、trusted プラグインで settings.secret と secretSettings capability を使います。secret は admin UI から保存しますが、平文は公開設定キャッシュには出ません。trusted processor Lambda だけが ctx.secret<T>(key) で読み取れます。
@ampless/plugin-webhook はこの仕組みを使い、HMAC 署名 secret を /admin/plugins から再デプロイなしでローテーションできるようにしています。untrusted プラグインで settings.secret を宣言すると起動時に弾かれるため、公開リクエスト中に動く head/body 注入系のプラグインへ secret を渡す設計にはしていません。
イベントフック
投稿公開後に外部へ通知する。RSS のような生成ファイルを作る。独自の JSON インデックスを更新する。こうした処理は hooks に書きます。
import { definePlugin, type AmplessPlugin } from 'ampless'
export default function simpleIndexPlugin(): AmplessPlugin {
return definePlugin({
name: 'simple-index',
apiVersion: 1,
trust_level: 'trusted',
capabilities: ['eventHooks', 'writePublicAsset'],
hooks: {
'content.published': rebuild,
'content.updated': rebuild,
'content.unpublished': rebuild,
'content.deleted': rebuild,
},
})
}
async function rebuild(_event: unknown, ctx: import('ampless').PluginRuntimeContext) {
const posts = await ctx.listPublishedPosts()
const body = JSON.stringify(
posts.map((post) => ({
title: post.title,
slug: post.slug,
excerpt: post.excerpt,
})),
null,
2
)
await ctx.writePublicAsset('posts.json', body, 'application/json; charset=utf-8')
}
writePublicAsset() で書いたファイルは public/plugins/<plugin-name>/<key> に保存されます。キーには絶対パスや .. を使えません。runtime が書き込み前に検証します。
SQS は at-least-once です。同じイベントが複数回届いても壊れないように、hooks は冪等に書きます。RSS や index のように毎回すべて作り直して同じキーへ上書きする処理は、この性質と相性がいいです。
公開プラグインとして配る
ほかのサイトでも使うなら、通常の npm パッケージにします。現行モデルでは、管理画面からワンクリックでインストールするのではありません。サイト運営者がリポジトリに依存を追加し、cms.config.ts に登録して、Amplify Hosting で再デプロイします。
最小の構成は次のようなものです。
my-ampless-plugin/
package.json
tsconfig.json
tsup.config.ts
src/
index.ts
package.json は ESM の default export と、インストール時のクロスチェックに使う ./package.json export を出します。
{
"name": "ampless-plugin-example",
"type": "module",
"keywords": ["ampless-plugin"],
"exports": {
".": {
"import": "./dist/index.js",
"types": "./dist/index.d.ts"
},
"./package.json": "./package.json"
},
"files": ["dist", "README.md"],
"dependencies": {
"ampless": "^1.0.0-beta.0"
},
"amplessPlugin": {
"apiVersion": 1,
"name": "example",
"trustLevel": "untrusted",
"capabilities": ["publicHead"]
}
}
外部プラグインでも、基本は公式プラグインと同じです。
- factory を default export する
apiVersion: 1を返す- npm 配布するなら
packageNameを plugin descriptor に入れる package.json#amplessPluginと factory のname/trust_level/capabilitiesを一致させる./package.jsonを export し、検索用にampless-pluginkeyword を付けるtrust_levelとcapabilitiesを実装に合わせる- module top-level でネットワーク呼び出しやファイル書き込みをしない
- hooks は冪等にする
name/instanceId/ setting key は/^[a-zA-Z0-9_-]+$/に収める
standalone のひな形は npx create-ampless@beta plugin <name> で作れます。生成されるパッケージには、package.json#amplessPlugin、./package.json export、packageName、ampless-plugin keyword、最小の test / build 設定が入ります。
マーケットプレイス型の配布はまだありません。任意 JS を実行時にロードするには、trusted 相当の権限をどう分けるか、capability から IAM をどう作るか、失敗したプラグインをどう隔離するかを決める必要があります。v1.0 の必須条件には入れていません。
いま向いている拡張
現状のプラグイン機構で書きやすいのは、次のようなものです。
- analytics / tag manager / cookie consent のような head/body 注入
- reading-time badge や breadcrumb のような本文前後の軽い可視 HTML
- syntax highlight や Mermaid diagram のような、Markdown 記事の表現補助
- YouTube / x.com のような本文中の URL 埋め込み
- SEO metadata、canonical、JSON-LD
- RSS、sitemap、公開 JSON index の生成
- 投稿公開・更新イベントを外部へ投げる Webhook
- HMAC 署名 secret のように、trusted hook 側だけで読む秘密値を持つ連携
- サイト固有の小さな meta/link 追加
逆に、次のものはまだ慎重に見たほうがよさそうです。
- admin の新規画面や server route の追加
- 独自 DynamoDB テーブルへの書き込み
- ユーザー投稿フォームや決済のように、CMS とは別の権限境界が必要な処理
ここは privileged 層や per-plugin Lambda 分離、admin page / server route capability が入ってから整理する領域です。いま必要なら、プラグインに押し込まず、サイト側の通常の Next.js / Amplify 実装として切り出すほうが読みやすく保てます。
関連記事
- ampless の全体像 → what-is-ampless
- 標準プラグインでできること → standard-plugins
- サーバレス構成 → serverless-stack
- MCP 経由で記事を書く → mcp-http-transport