ampless 1.0.0-alpha.20 → 1.0.0-alpha.21
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 +71 -1
- package/docs/plugin-author-guide.ja.md +79 -1
- package/docs/plugin-author-guide.md +102 -2
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -288,6 +288,10 @@ type TrustLevel = 'untrusted' | 'trusted' | 'privileged';
|
|
|
288
288
|
* - `metadata` / `eventHooks`: name-only declaration for existing surfaces.
|
|
289
289
|
* - `adminSettings`: admin-managed public settings manifest.
|
|
290
290
|
* - `writePublicAsset`: trusted hook context can write namespaced public assets.
|
|
291
|
+
* - `schema`: per-post body injection via `publicBodyForPost`,
|
|
292
|
+
* scoped to JSON-LD `<script type="application/ld+json">`. Themes
|
|
293
|
+
* render the descriptors by calling `ampless.publicBodyForPost(post)`
|
|
294
|
+
* in their post template.
|
|
291
295
|
*
|
|
292
296
|
* Reserved capabilities are accepted by the type so that plugins can
|
|
293
297
|
* declare future intent, but the runtime does nothing with them yet —
|
|
@@ -363,6 +367,26 @@ type PublicHeadDescriptor = {
|
|
|
363
367
|
id: string;
|
|
364
368
|
body: string;
|
|
365
369
|
strategy?: ScriptStrategy;
|
|
370
|
+
/**
|
|
371
|
+
* Optional MIME-like script type. Phase 4 allows
|
|
372
|
+
* `'application/ld+json'` only — when set, the runtime emits
|
|
373
|
+
* `<script type="application/ld+json">` and **auto-escapes** the
|
|
374
|
+
* `body` (`<`, `>`, `&`, U+2028, U+2029 → `\uXXXX`) so plugin
|
|
375
|
+
* authors cannot accidentally let a value break out of the
|
|
376
|
+
* script tag. This invariant is applied across all three
|
|
377
|
+
* surfaces (`publicHead` / `publicBodyEnd` / `publicBodyForPost`).
|
|
378
|
+
*
|
|
379
|
+
* Unsupported values are dropped (descriptor and warning).
|
|
380
|
+
* Surface-dependent strictness:
|
|
381
|
+
* - `publicHead` / `publicBodyEnd`: `undefined` (= default JS,
|
|
382
|
+
* backwards compatible) or `'application/ld+json'`.
|
|
383
|
+
* - `publicBodyForPost`: `'application/ld+json'` REQUIRED —
|
|
384
|
+
* the per-post body surface is scoped to JSON-LD only so
|
|
385
|
+
* the schema capability does not become a per-post arbitrary
|
|
386
|
+
* inline-JS channel. A future capability would open that
|
|
387
|
+
* explicitly.
|
|
388
|
+
*/
|
|
389
|
+
scriptType?: 'application/ld+json';
|
|
366
390
|
/**
|
|
367
391
|
* Type-only reservation. CSP nonce resolution (including the
|
|
368
392
|
* planned `'auto'` mode) is deferred to a future RFP; Phase 1
|
|
@@ -404,6 +428,27 @@ type PublicBodyDescriptor = Extract<PublicHeadDescriptor, {
|
|
|
404
428
|
height?: number;
|
|
405
429
|
attrs?: Record<string, string | boolean>;
|
|
406
430
|
};
|
|
431
|
+
/**
|
|
432
|
+
* Descriptor returned by `publicBodyForPost()` (Phase 4). Limited to
|
|
433
|
+
* the `inlineScript` variant with `scriptType: 'application/ld+json'`
|
|
434
|
+
* REQUIRED. This narrowing is deliberate:
|
|
435
|
+
* - The per-post body surface is for JSON-LD / structured data only.
|
|
436
|
+
* We do not want the `schema` capability to become a per-post
|
|
437
|
+
* arbitrary inline-JS channel.
|
|
438
|
+
* - `meta` / `link` belong in `<head>`; theme post pages render
|
|
439
|
+
* these descriptors inside `<body>`, so meta/link are excluded.
|
|
440
|
+
*
|
|
441
|
+
* If a future use case needs per-post arbitrary inline JS (e.g.
|
|
442
|
+
* Microsoft Clarity per-page tagging), open it through a new
|
|
443
|
+
* capability such as `publicPostScript` rather than relaxing this.
|
|
444
|
+
*/
|
|
445
|
+
type PublicPostBodyDescriptor = Extract<PublicHeadDescriptor, {
|
|
446
|
+
type: 'inlineScript';
|
|
447
|
+
}> & {
|
|
448
|
+
/** Required for `publicBodyForPost`; the runtime drops any descriptor
|
|
449
|
+
* whose scriptType is not 'application/ld+json'. */
|
|
450
|
+
scriptType: 'application/ld+json';
|
|
451
|
+
};
|
|
407
452
|
/**
|
|
408
453
|
* Metadata-like object that maps cleanly onto Next.js `Metadata`. We keep
|
|
409
454
|
* the shape framework-agnostic so the core type doesn't depend on Next.js.
|
|
@@ -642,6 +687,31 @@ interface AmplessPlugin {
|
|
|
642
687
|
* (GTM no-script fallback frame, chat widgets, ...).
|
|
643
688
|
*/
|
|
644
689
|
publicBodyEnd?(ctx: PluginPublicRenderContext): readonly PublicBodyDescriptor[];
|
|
690
|
+
/**
|
|
691
|
+
* Per-post body descriptors (Phase 4). Themes render the result by
|
|
692
|
+
* calling `ampless.publicBodyForPost(post)` in their post template;
|
|
693
|
+
* the descriptors emit inside `<body>` (not `<head>`, which the
|
|
694
|
+
* Next.js Metadata API cannot do for `<script>` tags). Primary use
|
|
695
|
+
* case: JSON-LD `<script type="application/ld+json">` Article
|
|
696
|
+
* schema. Restricted to inline-script descriptors with
|
|
697
|
+
* `scriptType: 'application/ld+json'` — see
|
|
698
|
+
* `PublicPostBodyDescriptor` for the rationale.
|
|
699
|
+
*
|
|
700
|
+
* The runtime auto-escapes `<`, `>`, `&`, U+2028, and U+2029 in the
|
|
701
|
+
* body so plugin authors cannot accidentally let a value break out
|
|
702
|
+
* of the script tag. The same escape is applied to any
|
|
703
|
+
* `'application/ld+json'` descriptor returned from `publicHead` or
|
|
704
|
+
* `publicBodyEnd`.
|
|
705
|
+
*
|
|
706
|
+
* Theme integration: first-party themes (blog / corporate / dads /
|
|
707
|
+
* docs / landing / minimal) all render the result automatically.
|
|
708
|
+
* Plugin authors who target custom themes should document the
|
|
709
|
+
* theme-side render call in their plugin's README — when the theme
|
|
710
|
+
* does not call `publicBodyForPost`, the plugin silently no-ops.
|
|
711
|
+
*
|
|
712
|
+
* Plugins implementing this should declare the `schema` capability.
|
|
713
|
+
*/
|
|
714
|
+
publicBodyForPost?(post: Post, ctx: PluginPublicRenderContext): readonly PublicPostBodyDescriptor[];
|
|
645
715
|
/**
|
|
646
716
|
* Dynamic OG image renderer. The dispatcher route (e.g.
|
|
647
717
|
* `app/og/[slug]/route.ts`) reads this and feeds the element into
|
|
@@ -1189,4 +1259,4 @@ declare function pickDefaultEntrypoint(files: readonly {
|
|
|
1189
1259
|
|
|
1190
1260
|
declare const VERSION = "0.0.1";
|
|
1191
1261
|
|
|
1192
|
-
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 ImageDisplay, type KvItem, type KvStore, type LinkListItem, type ListOptions, 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 PluginJsonField, type PluginMetadata, type PluginNumberField, type PluginPublicRenderContext, type PluginRuntimeContext, type PluginSelectField, type PluginSettingField, type PluginSettingsManifest, type PluginTextField, type PluginTextareaField, type PluginUrlField, type Post, type PostIndexEventPayload, type PostIndexEventType, type PostMetadata, type PostStatus, type PostsProvider, type PublicBodyDescriptor, type PublicHeadDescriptor, 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, 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, listPosts, listSiteSettings, mimeTypeFor, parseColorPair, parseLinkList, pickDefaultEntrypoint, resolveLocalized, resolvePluginSettings, resolveThemeValues, setKvStore, setPostsProvider, setSiteSetting, stringifyLinkList, stripCommonPrefix, themeSettingKey, unflattenSettings, updatePost, validateBundle, validateBundlePath, validatePluginSettingValue, validatePublicAssetKey, validateThemeValue };
|
|
1262
|
+
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 ImageDisplay, type KvItem, type KvStore, type LinkListItem, type ListOptions, 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 PluginJsonField, type PluginMetadata, type PluginNumberField, type PluginPublicRenderContext, type PluginRuntimeContext, type PluginSelectField, type PluginSettingField, type PluginSettingsManifest, type PluginTextField, type PluginTextareaField, type PluginUrlField, type Post, type PostIndexEventPayload, type PostIndexEventType, type PostMetadata, type PostStatus, type PostsProvider, type PublicBodyDescriptor, type PublicHeadDescriptor, type PublicPostBodyDescriptor, 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, 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, listPosts, listSiteSettings, mimeTypeFor, parseColorPair, parseLinkList, pickDefaultEntrypoint, resolveLocalized, resolvePluginSettings, resolveThemeValues, setKvStore, setPostsProvider, setSiteSetting, stringifyLinkList, stripCommonPrefix, themeSettingKey, unflattenSettings, updatePost, validateBundle, validateBundlePath, validatePluginSettingValue, validatePublicAssetKey, validateThemeValue };
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
# ampless プラグインの書き方
|
|
11
11
|
|
|
12
|
-
このガイドは、初めての `definePlugin()` 呼び出しから admin 編集可能な設定パネル、そして npm 公開に至るまで、ampless プラグインを ship するために必要な手順を一通りカバーします。Phase 1
|
|
12
|
+
このガイドは、初めての `definePlugin()` 呼び出しから admin 編集可能な設定パネル、そして npm 公開に至るまで、ampless プラグインを ship するために必要な手順を一通りカバーします。Phase 1〜4 の機能を網羅 — descriptor ベースの `<head>` / `<body>` / 投稿単位 body 注入、非同期イベントフック、admin 管理の `settings.public` 値です。
|
|
13
13
|
|
|
14
14
|
設計の経緯と背景は [`docs/architecture/08-plugin-architecture.md`](https://github.com/heavymoons/ampless/blob/main/docs/architecture/08-plugin-architecture.md) に集約。本ページはその実装ハンドブック側です。
|
|
15
15
|
|
|
@@ -25,6 +25,7 @@ ampless プラグインは `AmplessPlugin` オブジェクトを返す TypeScrip
|
|
|
25
25
|
| `siteMetadata(site)` | root layout の `generateMetadata()` | 同期 | 既存 |
|
|
26
26
|
| `publicHead(ctx)` | root layout の `<head>` | 同期 (async layout から呼ばれる) | 1 |
|
|
27
27
|
| `publicBodyEnd(ctx)` | root layout の `<body>` 末尾 | 同期 | 1 |
|
|
28
|
+
| `publicBodyForPost(post, ctx)` | テーマの post ページテンプレート(投稿単位) | 同期 | 4 |
|
|
28
29
|
| `ogImage` | `/og/[slug]` ルート | リクエスト時、公開 Lambda 内 | 既存 |
|
|
29
30
|
| `hooks` | trust_level に応じた processor Lambda | 非同期、SQS イベントで起動 | 既存 |
|
|
30
31
|
| `settings.public` | `/admin/plugins` フォーム | 宣言的なマニフェスト | 2 |
|
|
@@ -102,6 +103,7 @@ interface AmplessPlugin {
|
|
|
102
103
|
siteMetadata?(site): PluginMetadata
|
|
103
104
|
publicHead?(ctx): readonly PublicHeadDescriptor[]
|
|
104
105
|
publicBodyEnd?(ctx): readonly PublicBodyDescriptor[]
|
|
106
|
+
publicBodyForPost?(post: Post, ctx): readonly PublicPostBodyDescriptor[]
|
|
105
107
|
ogImage?: OgImageConfig
|
|
106
108
|
settings?: { public?: readonly PluginSettingField[] }
|
|
107
109
|
}
|
|
@@ -162,6 +164,7 @@ trust level がズレているプラグインは「権限不足で sliently fail
|
|
|
162
164
|
| `siteMetadata(site)` | `PluginMetadata` | サイト全体の `<title>` / favicon / RSS `<link rel="alternate">` |
|
|
163
165
|
| `publicHead(ctx)` | `PublicHeadDescriptor[]` | 解析ローダー、フォント、jsonld、hreflang |
|
|
164
166
|
| `publicBodyEnd(ctx)` | `PublicBodyDescriptor[]` | GTM no-script フレーム、チャットウィジェット、末尾スニペット |
|
|
167
|
+
| `publicBodyForPost(post, ctx)` | `PublicPostBodyDescriptor[]` | 投稿単位の body 注入 — JSON-LD 構造化データ。テーマの post ページテンプレートが render する |
|
|
165
168
|
|
|
166
169
|
`ctx` オブジェクトの中身:
|
|
167
170
|
|
|
@@ -202,6 +205,15 @@ trust level がズレているプラグインは「権限不足で sliently fail
|
|
|
202
205
|
strategy: 'afterInteractive',
|
|
203
206
|
}
|
|
204
207
|
|
|
208
|
+
// inline script — JSON-LD variant (publicHead / publicBodyEnd / publicBodyForPost で使用可能)
|
|
209
|
+
// runtime が body を自動 escape するので、生の JSON 文字列を返せばよい
|
|
210
|
+
{
|
|
211
|
+
type: 'inlineScript',
|
|
212
|
+
id: 'schema-article',
|
|
213
|
+
scriptType: 'application/ld+json',
|
|
214
|
+
body: JSON.stringify({ '@context': 'https://schema.org', '@type': 'Article', ... }),
|
|
215
|
+
}
|
|
216
|
+
|
|
205
217
|
// Meta / link / noscript
|
|
206
218
|
{ type: 'meta', name: 'theme-color', content: '#fff' }
|
|
207
219
|
{ type: 'meta', property: 'og:image', content: 'https://…' }
|
|
@@ -223,6 +235,36 @@ trust level がズレているプラグインは「権限不足で sliently fail
|
|
|
223
235
|
}
|
|
224
236
|
```
|
|
225
237
|
|
|
238
|
+
### `PublicPostBodyDescriptor`(Phase 4)
|
|
239
|
+
|
|
240
|
+
`publicBodyForPost` は `PublicPostBodyDescriptor[]` を返します。これは `inlineScript` の制限サブセットで、`scriptType` が必須かつ `'application/ld+json'` のみ有効です:
|
|
241
|
+
|
|
242
|
+
```ts
|
|
243
|
+
// publicBodyForPost で返せる唯一の形:
|
|
244
|
+
{
|
|
245
|
+
type: 'inlineScript',
|
|
246
|
+
id: 'schema-article',
|
|
247
|
+
scriptType: 'application/ld+json', // 必須 — これ以外は drop + warn
|
|
248
|
+
body: JSON.stringify({ '@context': 'https://schema.org', '@type': 'Article', ... }),
|
|
249
|
+
}
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
`meta` / `link` を除いている理由:投稿単位のメタデータは Next.js `generateMetadata()` 経由の `metadata()` サーフェスが担い、フレームワークの deduplication・streaming と統合されている。`publicBodyForPost` は `generateMetadata` が生成できない構造化データ(`<script type="application/ld+json">`)のためだけに存在する。
|
|
253
|
+
|
|
254
|
+
### JSON-LD 自動 escape
|
|
255
|
+
|
|
256
|
+
`scriptType === 'application/ld+json'` のとき、runtime は描画前に **`body` 文字列を自動 escape** する — `<` → `<`、`>` → `>`、`&` → `&`、U+2028 → ` `、U+2029 → ` `。この処理は `inlineScript` を受け付ける 3 つのサーフェス(`publicHead` / `publicBodyEnd` / `publicBodyForPost`)すべてで行われる。プラグイン作者は生の JSON 文字列を返せばよく、自前で escape しなくてよい。
|
|
257
|
+
|
|
258
|
+
サポート外の `scriptType` を持つ descriptor は **console warning 付きで drop** される。
|
|
259
|
+
|
|
260
|
+
### サーフェス別 scriptType ルール
|
|
261
|
+
|
|
262
|
+
| サーフェス | `scriptType` |
|
|
263
|
+
|---|---|
|
|
264
|
+
| `publicHead` | `undefined`(デフォルト JS)または `'application/ld+json'` |
|
|
265
|
+
| `publicBodyEnd` | `publicHead` と同じ |
|
|
266
|
+
| `publicBodyForPost` | `'application/ld+json'` **必須**。他の値(省略含む)は drop + warn |
|
|
267
|
+
|
|
226
268
|
### Validation ルール
|
|
227
269
|
|
|
228
270
|
- **URL scheme allowlist**: `http`、`https`、または相対パス。`javascript:`、`data:`、`vbscript:`、`blob:`、`file:` は要素描画前に拒否されます
|
|
@@ -244,6 +286,39 @@ trust level がズレているプラグインは「権限不足で sliently fail
|
|
|
244
286
|
|
|
245
287
|
`cms.config.plugins` の順序は集約後も保たれます。
|
|
246
288
|
|
|
289
|
+
### `publicBodyForPost` の使用例(Phase 4)
|
|
290
|
+
|
|
291
|
+
`schema` capability を宣言してサーフェスを実装します:
|
|
292
|
+
|
|
293
|
+
```typescript
|
|
294
|
+
import { definePlugin } from 'ampless'
|
|
295
|
+
|
|
296
|
+
export default function schemaJsonldPlugin() {
|
|
297
|
+
return definePlugin({
|
|
298
|
+
name: 'schema-jsonld',
|
|
299
|
+
apiVersion: 1,
|
|
300
|
+
trust_level: 'untrusted',
|
|
301
|
+
capabilities: ['schema'],
|
|
302
|
+
publicBodyForPost(post, ctx) {
|
|
303
|
+
return [{
|
|
304
|
+
type: 'inlineScript',
|
|
305
|
+
id: 'schema-article',
|
|
306
|
+
scriptType: 'application/ld+json',
|
|
307
|
+
body: JSON.stringify({
|
|
308
|
+
'@context': 'https://schema.org',
|
|
309
|
+
'@type': 'Article',
|
|
310
|
+
headline: post.title,
|
|
311
|
+
url: `${ctx.site.url}/${post.slug}`,
|
|
312
|
+
datePublished: post.publishedAt,
|
|
313
|
+
}),
|
|
314
|
+
}]
|
|
315
|
+
},
|
|
316
|
+
})
|
|
317
|
+
}
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
テーマの `pages/post.tsx` が `ampless.publicBodyForPost(post)` を呼び、返された descriptor を描画します。runtime は自動 escape した body を持つ `<script type="application/ld+json">` 要素をページに挿入します。
|
|
321
|
+
|
|
247
322
|
---
|
|
248
323
|
|
|
249
324
|
## 7. 非同期イベントフック
|
|
@@ -493,6 +568,7 @@ it('admin が空文字保存した場合は空配列', () => {
|
|
|
493
568
|
- [`packages/plugin-seo`](https://github.com/heavymoons/ampless/tree/main/packages/plugin-seo) — `metadata()` + `siteMetadata()`
|
|
494
569
|
- [`packages/plugin-webhook`](https://github.com/heavymoons/ampless/tree/main/packages/plugin-webhook) — untrusted hook + 外向き HTTP
|
|
495
570
|
- [`packages/plugin-og-image`](https://github.com/heavymoons/ampless/tree/main/packages/plugin-og-image) — `ogImage` ルートレンダラ
|
|
571
|
+
- [`packages/plugin-schema-jsonld`](https://github.com/heavymoons/ampless/tree/main/packages/plugin-schema-jsonld) — `publicBodyForPost` + `schema` capability、投稿単位 Article JSON-LD。(Phase 4)
|
|
496
572
|
|
|
497
573
|
---
|
|
498
574
|
|
|
@@ -510,6 +586,8 @@ it('admin が空文字保存した場合は空配列', () => {
|
|
|
510
586
|
- **`inlineScript` の `id` 忘れ**。production では silent に drop、dev では warn。inline script の dedup は id 無しではできません
|
|
511
587
|
- **`publicHead` から `ReactNode` を返す**。TypeScript で弾かれます — `publicHead` の戻り値型は descriptor のみ。任意 `ReactNode` が必要なら、それは Phase 6b の `developer.headElements` capability 待ち
|
|
512
588
|
- **admin form から `manifest.default` を保存してしまう**。resolved default を「明示値」として書き戻さないでください — admin form が touched フィールドのみ書き込む設計はまさにこのため。default 値を保存すると future のパッケージ更新で default が変わってもそのフィールドだけ反映されなくなります
|
|
589
|
+
- **`publicBodyForPost` で `scriptType: 'application/ld+json'` を省略**。`publicBodyForPost` が返す descriptor で `scriptType` を省略するか他の値を指定すると、production では silent に drop、dev では warn されます。このサーフェスで有効なのは `'application/ld+json'` だけです
|
|
590
|
+
- **`schema` capability と `publicBodyForPost` の不一致**。`capabilities: ['schema']` を宣言して `publicBodyForPost` を未実装(またはその逆)にすると起動時に warning が出ます。宣言と実装は同期させてください
|
|
513
591
|
|
|
514
592
|
---
|
|
515
593
|
|
|
@@ -12,8 +12,8 @@
|
|
|
12
12
|
This guide walks through everything a plugin author needs to ship a
|
|
13
13
|
working ampless plugin, from first `definePlugin()` call to the
|
|
14
14
|
admin-editable settings panel and an npm publish. It covers the
|
|
15
|
-
Phase 1
|
|
16
|
-
injection, the async event hooks, and admin-managed
|
|
15
|
+
Phase 1–4 surfaces — descriptor-based `<head>` / `<body>` /
|
|
16
|
+
per-post body injection, the async event hooks, and admin-managed
|
|
17
17
|
`settings.public` values.
|
|
18
18
|
|
|
19
19
|
The design rationale is in [`docs/architecture/08-plugin-architecture.md`](https://github.com/heavymoons/ampless/blob/main/docs/architecture/08-plugin-architecture.md);
|
|
@@ -33,6 +33,7 @@ surfaces:
|
|
|
33
33
|
| `siteMetadata(site)` | root layout `generateMetadata()` | sync | Existing |
|
|
34
34
|
| `publicHead(ctx)` | root layout `<head>` | sync (called from async layout) | 1 |
|
|
35
35
|
| `publicBodyEnd(ctx)` | root layout end of `<body>` | sync | 1 |
|
|
36
|
+
| `publicBodyForPost(post, ctx)` | theme post page template (per-post) | sync | 4 |
|
|
36
37
|
| `ogImage` | `/og/[slug]` route | request-time, in public Lambda | Existing |
|
|
37
38
|
| `hooks` | trust_level-matched processor Lambda | async, on SQS event | Existing |
|
|
38
39
|
| `settings.public` | `/admin/plugins` form | declarative manifest | 2 |
|
|
@@ -117,6 +118,7 @@ interface AmplessPlugin {
|
|
|
117
118
|
siteMetadata?(site): PluginMetadata
|
|
118
119
|
publicHead?(ctx): readonly PublicHeadDescriptor[]
|
|
119
120
|
publicBodyEnd?(ctx): readonly PublicBodyDescriptor[]
|
|
121
|
+
publicBodyForPost?(post: Post, ctx): readonly PublicPostBodyDescriptor[]
|
|
120
122
|
ogImage?: OgImageConfig
|
|
121
123
|
settings?: { public?: readonly PluginSettingField[] }
|
|
122
124
|
}
|
|
@@ -201,6 +203,7 @@ network — and execute synchronously.
|
|
|
201
203
|
| `siteMetadata(site)` | `PluginMetadata` | Site-wide `<title>` / favicon / RSS `<link rel="alternate">` |
|
|
202
204
|
| `publicHead(ctx)` | `PublicHeadDescriptor[]` | Analytics loader, fonts, jsonld, hreflang |
|
|
203
205
|
| `publicBodyEnd(ctx)` | `PublicBodyDescriptor[]` | GTM no-script frame, chat widgets, tail snippets |
|
|
206
|
+
| `publicBodyForPost(post, ctx)` | `PublicPostBodyDescriptor[]` | Per-post body injection — JSON-LD structured data; rendered by the theme's post page template |
|
|
204
207
|
|
|
205
208
|
The `ctx` object carries:
|
|
206
209
|
|
|
@@ -246,6 +249,15 @@ untrusted code in the public render path.
|
|
|
246
249
|
strategy: 'afterInteractive',
|
|
247
250
|
}
|
|
248
251
|
|
|
252
|
+
// Inline script — JSON-LD variant (valid in publicHead, publicBodyEnd, publicBodyForPost)
|
|
253
|
+
// The runtime auto-escapes the body; return raw JSON, not pre-escaped.
|
|
254
|
+
{
|
|
255
|
+
type: 'inlineScript',
|
|
256
|
+
id: 'schema-article',
|
|
257
|
+
scriptType: 'application/ld+json',
|
|
258
|
+
body: JSON.stringify({ '@context': 'https://schema.org', '@type': 'Article', ... }),
|
|
259
|
+
}
|
|
260
|
+
|
|
249
261
|
// Meta / link / noscript
|
|
250
262
|
{ type: 'meta', name: 'theme-color', content: '#fff' }
|
|
251
263
|
{ type: 'meta', property: 'og:image', content: 'https://…' }
|
|
@@ -267,6 +279,49 @@ untrusted code in the public render path.
|
|
|
267
279
|
}
|
|
268
280
|
```
|
|
269
281
|
|
|
282
|
+
### `PublicPostBodyDescriptor` (Phase 4)
|
|
283
|
+
|
|
284
|
+
`publicBodyForPost` returns `PublicPostBodyDescriptor[]`. This is a
|
|
285
|
+
restricted subset of `inlineScript` where `scriptType` is required
|
|
286
|
+
and must be `'application/ld+json'`:
|
|
287
|
+
|
|
288
|
+
```ts
|
|
289
|
+
// The only valid form for publicBodyForPost:
|
|
290
|
+
{
|
|
291
|
+
type: 'inlineScript',
|
|
292
|
+
id: 'schema-article',
|
|
293
|
+
scriptType: 'application/ld+json', // required — only value accepted
|
|
294
|
+
body: JSON.stringify({ '@context': 'https://schema.org', '@type': 'Article', ... }),
|
|
295
|
+
}
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
Why `meta` and `link` are excluded: per-post metadata is already
|
|
299
|
+
handled by Next.js `generateMetadata()` via the `metadata()` surface,
|
|
300
|
+
which integrates with the framework's deduplication and streaming
|
|
301
|
+
behaviour. `publicBodyForPost` exists solely for the structured data
|
|
302
|
+
use-case (`<script type="application/ld+json">`) that `generateMetadata`
|
|
303
|
+
cannot produce.
|
|
304
|
+
|
|
305
|
+
### JSON-LD auto-escape
|
|
306
|
+
|
|
307
|
+
When `scriptType === 'application/ld+json'`, the runtime **automatically
|
|
308
|
+
escapes the `body` string** before rendering — `<` → `<`,
|
|
309
|
+
`>` → `>`, `&` → `&`, U+2028 → `
`, U+2029 → `
`.
|
|
310
|
+
This applies across all three surfaces that accept `inlineScript`
|
|
311
|
+
(`publicHead`, `publicBodyEnd`, `publicBodyForPost`). Return the raw
|
|
312
|
+
JSON string; do not pre-escape it.
|
|
313
|
+
|
|
314
|
+
Descriptors with an unsupported `scriptType` value are **dropped with
|
|
315
|
+
a console warning** and never rendered.
|
|
316
|
+
|
|
317
|
+
### Surface-dependent `scriptType` rules
|
|
318
|
+
|
|
319
|
+
| Surface | `scriptType` |
|
|
320
|
+
|---|---|
|
|
321
|
+
| `publicHead` | `undefined` (default JS) or `'application/ld+json'` |
|
|
322
|
+
| `publicBodyEnd` | same as `publicHead` |
|
|
323
|
+
| `publicBodyForPost` | `'application/ld+json'` **required**; any other value (including omitted) is dropped + warned |
|
|
324
|
+
|
|
270
325
|
### Validation rules
|
|
271
326
|
|
|
272
327
|
- **URL scheme allowlist**: `http`, `https`, or relative paths.
|
|
@@ -302,6 +357,42 @@ interpolates that fragment directly:
|
|
|
302
357
|
`cms.config.plugins` iteration order is preserved across the
|
|
303
358
|
collected list.
|
|
304
359
|
|
|
360
|
+
### `publicBodyForPost` example (Phase 4)
|
|
361
|
+
|
|
362
|
+
Declare the `schema` capability and implement the surface:
|
|
363
|
+
|
|
364
|
+
```typescript
|
|
365
|
+
import { definePlugin } from 'ampless'
|
|
366
|
+
|
|
367
|
+
export default function schemaJsonldPlugin() {
|
|
368
|
+
return definePlugin({
|
|
369
|
+
name: 'schema-jsonld',
|
|
370
|
+
apiVersion: 1,
|
|
371
|
+
trust_level: 'untrusted',
|
|
372
|
+
capabilities: ['schema'],
|
|
373
|
+
publicBodyForPost(post, ctx) {
|
|
374
|
+
return [{
|
|
375
|
+
type: 'inlineScript',
|
|
376
|
+
id: 'schema-article',
|
|
377
|
+
scriptType: 'application/ld+json',
|
|
378
|
+
body: JSON.stringify({
|
|
379
|
+
'@context': 'https://schema.org',
|
|
380
|
+
'@type': 'Article',
|
|
381
|
+
headline: post.title,
|
|
382
|
+
url: `${ctx.site.url}/${post.slug}`,
|
|
383
|
+
datePublished: post.publishedAt,
|
|
384
|
+
}),
|
|
385
|
+
}]
|
|
386
|
+
},
|
|
387
|
+
})
|
|
388
|
+
}
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
The theme's `pages/post.tsx` calls `ampless.publicBodyForPost(post)`
|
|
392
|
+
and renders the returned descriptors. The runtime inserts a
|
|
393
|
+
`<script type="application/ld+json">` element with the auto-escaped
|
|
394
|
+
body into the page.
|
|
395
|
+
|
|
305
396
|
---
|
|
306
397
|
|
|
307
398
|
## 7. Async event hooks
|
|
@@ -619,6 +710,7 @@ Worked examples to crib from:
|
|
|
619
710
|
- [`packages/plugin-seo`](https://github.com/heavymoons/ampless/tree/main/packages/plugin-seo) — `metadata()` + `siteMetadata()`.
|
|
620
711
|
- [`packages/plugin-webhook`](https://github.com/heavymoons/ampless/tree/main/packages/plugin-webhook) — untrusted hook with outbound HTTP.
|
|
621
712
|
- [`packages/plugin-og-image`](https://github.com/heavymoons/ampless/tree/main/packages/plugin-og-image) — `ogImage` route renderer.
|
|
713
|
+
- [`packages/plugin-schema-jsonld`](https://github.com/heavymoons/ampless/tree/main/packages/plugin-schema-jsonld) — `publicBodyForPost` + `schema` capability; per-post Article JSON-LD. (Phase 4)
|
|
622
714
|
|
|
623
715
|
---
|
|
624
716
|
|
|
@@ -654,6 +746,14 @@ Worked examples to crib from:
|
|
|
654
746
|
admin form only writes touched fields for exactly this reason.
|
|
655
747
|
Saving a default value freezes it: future package updates that
|
|
656
748
|
change the default won't take effect for that field anymore.
|
|
749
|
+
- **`publicBodyForPost` without `scriptType: 'application/ld+json'`.** Descriptors
|
|
750
|
+
returned from `publicBodyForPost` that omit `scriptType` or use any
|
|
751
|
+
other value are silently dropped in production and warned in dev.
|
|
752
|
+
Only `scriptType: 'application/ld+json'` is valid in that surface.
|
|
753
|
+
- **`schema` capability + `publicBodyForPost` mismatch.** Declaring
|
|
754
|
+
`capabilities: ['schema']` without implementing `publicBodyForPost`
|
|
755
|
+
(or the reverse) prints a startup warning. Keep the declaration and
|
|
756
|
+
implementation in sync.
|
|
657
757
|
|
|
658
758
|
---
|
|
659
759
|
|