create-ampless 1.0.0-alpha.71 → 1.0.0-alpha.76
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.js +21 -1
- package/dist/templates/_shared/cms.config.ts +12 -0
- package/dist/templates/_shared/components/tag-list.tsx +9 -2
- package/dist/templates/_shared/docs/plugin-author-guide.ja.md +79 -1
- package/dist/templates/_shared/docs/plugin-author-guide.md +102 -2
- package/dist/templates/_shared/package.json +12 -11
- package/dist/templates/_shared/plugins/README.ja.md +111 -0
- package/dist/templates/_shared/plugins/README.md +113 -0
- package/dist/templates/blog/pages/home.tsx +1 -1
- package/dist/templates/blog/pages/post.tsx +26 -0
- package/dist/templates/corporate/pages/post.tsx +4 -0
- package/dist/templates/dads/pages/post.tsx +4 -0
- package/dist/templates/docs/pages/post.tsx +4 -0
- package/dist/templates/landing/pages/post.tsx +4 -0
- package/dist/templates/minimal/pages/post.tsx +4 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1755,6 +1755,7 @@ var AMPLESS_PACKAGES = /* @__PURE__ */ new Set([
|
|
|
1755
1755
|
"@ampless/runtime",
|
|
1756
1756
|
"@ampless/plugin-seo",
|
|
1757
1757
|
"@ampless/plugin-rss",
|
|
1758
|
+
"@ampless/plugin-schema-jsonld",
|
|
1758
1759
|
"@ampless/plugin-webhook",
|
|
1759
1760
|
"@ampless/plugin-og-image",
|
|
1760
1761
|
"@ampless/plugin-analytics-ga4",
|
|
@@ -1781,13 +1782,32 @@ var PROTECTED_PATTERNS = [
|
|
|
1781
1782
|
/^\.amplify(\/|$)/,
|
|
1782
1783
|
/^amplify_outputs\.json$/,
|
|
1783
1784
|
/^next-env\.d\.ts$/,
|
|
1785
|
+
// `tsconfig.json` is auto-managed by Next.js during `next build` /
|
|
1786
|
+
// `next dev` — it rewrites `jsx: "preserve"` → `"react-jsx"` for
|
|
1787
|
+
// the React automatic runtime and appends `.next/dev/types/**/*.ts`
|
|
1788
|
+
// to `include`. Treating the file as `replace` here would overwrite
|
|
1789
|
+
// those mutations on every `update-ampless`, only for Next.js to
|
|
1790
|
+
// re-apply them on the next build — a churn cycle that produces a
|
|
1791
|
+
// dirty diff after every upgrade. Protect the file; users hand-
|
|
1792
|
+
// merge new compiler options in the rare case the template
|
|
1793
|
+
// tsconfig changes meaningfully.
|
|
1794
|
+
/^tsconfig\.json$/,
|
|
1784
1795
|
/^tsconfig\.tsbuildinfo$/,
|
|
1785
1796
|
/^pnpm-lock\.yaml$/,
|
|
1786
1797
|
/^package-lock\.json$/,
|
|
1787
1798
|
// Anything under themes/ is handled by syncThemes; the file-walk
|
|
1788
1799
|
// classifier must not double-process it.
|
|
1789
1800
|
/^themes(\/|$)/,
|
|
1790
|
-
/^themes-registry\.ts
|
|
1801
|
+
/^themes-registry\.ts$/,
|
|
1802
|
+
// `plugins/` is the site-local plugin directory (user-owned, mirrors
|
|
1803
|
+
// the `themes/my-*` convention for user-customised themes). The
|
|
1804
|
+
// initial scaffold seeds a README into this directory; everything
|
|
1805
|
+
// afterwards is the user's territory. We never overwrite or delete
|
|
1806
|
+
// files here — even the seeded README is frozen at scaffold time,
|
|
1807
|
+
// because plugin authors will sometimes edit it to document the
|
|
1808
|
+
// site's own conventions. The kept-up-to-date "how to write a
|
|
1809
|
+
// plugin" doc lives in `packages/ampless/docs/plugin-author-guide.md`.
|
|
1810
|
+
/^plugins(\/|$)/
|
|
1791
1811
|
];
|
|
1792
1812
|
var SEED_IF_MISSING_PATTERN = /\.custom\.ts$/;
|
|
1793
1813
|
var TEXT_EXTENSIONS2 = /* @__PURE__ */ new Set([
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { defineConfig } from 'ampless'
|
|
2
2
|
import seoPlugin from '@ampless/plugin-seo'
|
|
3
3
|
import rssPlugin from '@ampless/plugin-rss'
|
|
4
|
+
// import schemaJsonLdPlugin from '@ampless/plugin-schema-jsonld'
|
|
4
5
|
// import analyticsGa4Plugin from '@ampless/plugin-analytics-ga4'
|
|
5
6
|
// import gtmPlugin from '@ampless/plugin-gtm'
|
|
6
7
|
// import plausiblePlugin from '@ampless/plugin-plausible'
|
|
@@ -95,6 +96,17 @@ export default defineConfig({
|
|
|
95
96
|
// domain: '', // 'example.com' to enable
|
|
96
97
|
// }),
|
|
97
98
|
//
|
|
99
|
+
// JSON-LD structured data (Schema.org). Injects <script type="application/ld+json">
|
|
100
|
+
// into post pages for Article / BlogPosting markup. articleType, authorName and
|
|
101
|
+
// publisherName fall back to the site name when left empty.
|
|
102
|
+
//
|
|
103
|
+
// schemaJsonLdPlugin({
|
|
104
|
+
// articleType: 'Article', // 'Article' | 'BlogPosting' | 'NewsArticle'
|
|
105
|
+
// authorName: '', // empty falls back to site.name
|
|
106
|
+
// publisherName: '', // empty falls back to site.name
|
|
107
|
+
// publisherLogo: '', // optional absolute URL to publisher logo
|
|
108
|
+
// }),
|
|
109
|
+
//
|
|
98
110
|
// Per-post OG images: SNS crawlers hit `/og/<slug>` and we render
|
|
99
111
|
// a JSX card → PNG via Next.js `ImageResponse`. Requires at least one
|
|
100
112
|
// font — ship a .ttf from your CDN or `/public` directory.
|
|
@@ -3,14 +3,18 @@ import Link from 'next/link'
|
|
|
3
3
|
interface TagListProps {
|
|
4
4
|
tags?: string[] | null
|
|
5
5
|
className?: string
|
|
6
|
+
/** Cap the number of tags shown; the rest collapse into a "+N" indicator. */
|
|
7
|
+
max?: number
|
|
6
8
|
}
|
|
7
9
|
|
|
8
10
|
// Renders post tags as chip-style links to /tag/[tag]. Pure-server, no JS.
|
|
9
|
-
export function TagList({ tags, className }: TagListProps) {
|
|
11
|
+
export function TagList({ tags, className, max }: TagListProps) {
|
|
10
12
|
if (!tags?.length) return null
|
|
13
|
+
const shown = max != null ? tags.slice(0, max) : tags
|
|
14
|
+
const overflow = tags.length - shown.length
|
|
11
15
|
return (
|
|
12
16
|
<ul className={`flex flex-wrap gap-2 ${className ?? ''}`}>
|
|
13
|
-
{
|
|
17
|
+
{shown.map((tag) => (
|
|
14
18
|
<li key={tag}>
|
|
15
19
|
<Link
|
|
16
20
|
href={`/tag/${encodeURIComponent(tag)}`}
|
|
@@ -20,6 +24,9 @@ export function TagList({ tags, className }: TagListProps) {
|
|
|
20
24
|
</Link>
|
|
21
25
|
</li>
|
|
22
26
|
))}
|
|
27
|
+
{overflow > 0 && (
|
|
28
|
+
<li className="inline-block px-2 py-0.5 text-xs text-muted-foreground">+{overflow}</li>
|
|
29
|
+
)}
|
|
23
30
|
</ul>
|
|
24
31
|
)
|
|
25
32
|
}
|
|
@@ -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
|
|
|
@@ -25,18 +25,19 @@
|
|
|
25
25
|
"@tiptap/pm": "^3.23.6",
|
|
26
26
|
"@tiptap/react": "^3.23.6",
|
|
27
27
|
"@tiptap/starter-kit": "^3.23.6",
|
|
28
|
-
"@ampless/plugin-analytics-ga4": "^0.2.0-alpha.
|
|
29
|
-
"@ampless/plugin-gtm": "^0.1.1-alpha.
|
|
30
|
-
"@ampless/plugin-og-image": "^0.2.0-alpha.
|
|
31
|
-
"@ampless/plugin-plausible": "^0.1.1-alpha.
|
|
32
|
-
"@ampless/plugin-rss": "^0.2.0-alpha.
|
|
33
|
-
"@ampless/plugin-
|
|
34
|
-
"@ampless/plugin-
|
|
35
|
-
"@ampless/
|
|
36
|
-
"@ampless/
|
|
37
|
-
"@ampless/
|
|
28
|
+
"@ampless/plugin-analytics-ga4": "^0.2.0-alpha.4",
|
|
29
|
+
"@ampless/plugin-gtm": "^0.1.1-alpha.3",
|
|
30
|
+
"@ampless/plugin-og-image": "^0.2.0-alpha.21",
|
|
31
|
+
"@ampless/plugin-plausible": "^0.1.1-alpha.3",
|
|
32
|
+
"@ampless/plugin-rss": "^0.2.0-alpha.21",
|
|
33
|
+
"@ampless/plugin-schema-jsonld": "^0.1.1-alpha.0",
|
|
34
|
+
"@ampless/plugin-seo": "^0.2.0-alpha.21",
|
|
35
|
+
"@ampless/plugin-webhook": "^0.2.0-alpha.21",
|
|
36
|
+
"@ampless/admin": "^1.0.0-alpha.48",
|
|
37
|
+
"@ampless/backend": "^1.0.0-alpha.38",
|
|
38
|
+
"@ampless/runtime": "^1.0.0-alpha.27",
|
|
38
39
|
"@digital-go-jp/tailwind-theme-plugin": "^0.3.4",
|
|
39
|
-
"ampless": "^1.0.0-alpha.
|
|
40
|
+
"ampless": "^1.0.0-alpha.21",
|
|
40
41
|
"aws-amplify": "^6.17.0",
|
|
41
42
|
"class-variance-authority": "^0.7.1",
|
|
42
43
|
"clsx": "^2.1.1",
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
> English: [README.md](./README.md)
|
|
2
|
+
|
|
3
|
+
# サイトローカルプラグイン
|
|
4
|
+
|
|
5
|
+
このディレクトリは **このサイト専用のプラグイン置き場**。別 npm
|
|
6
|
+
パッケージとして publish したくない小さなカスタマイズを書く場所。
|
|
7
|
+
|
|
8
|
+
`update-ampless` はこのディレクトリのファイルに一切触らない。ここに置いた
|
|
9
|
+
ものは消えない、上書きされない。
|
|
10
|
+
|
|
11
|
+
## いつ使うか
|
|
12
|
+
|
|
13
|
+
サイト固有の機能で、以下に当てはまるときに使う:
|
|
14
|
+
|
|
15
|
+
- プラグイン surface (`publicHead` / `publicBodyEnd` / `metadata` / `eventHooks` 等) が必要
|
|
16
|
+
- このサイト限定の用途 (一度きりのフッタークレジット、サイト特有の JSON-LD 拡張、まだ汎用化したくない analytics スニペット等)
|
|
17
|
+
- 別パッケージとして version 上げて publish するのは大袈裟
|
|
18
|
+
|
|
19
|
+
複数サイトで使いたくなったら、独立 npm パッケージに切り出す (Phase 5 で
|
|
20
|
+
`npx create-ampless plugin --standalone` が来る予定。今は手でコピーで OK)。
|
|
21
|
+
|
|
22
|
+
## 最小例
|
|
23
|
+
|
|
24
|
+
`plugins/footer-credit/index.ts`:
|
|
25
|
+
|
|
26
|
+
```typescript
|
|
27
|
+
import { definePlugin, type AmplessPlugin } from 'ampless'
|
|
28
|
+
|
|
29
|
+
export interface FooterCreditOptions {
|
|
30
|
+
instanceId?: string
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export default function footerCreditPlugin(
|
|
34
|
+
options: FooterCreditOptions = {}
|
|
35
|
+
): AmplessPlugin {
|
|
36
|
+
const instanceId = options.instanceId ?? 'footer-credit'
|
|
37
|
+
return definePlugin({
|
|
38
|
+
name: 'footer-credit',
|
|
39
|
+
instanceId,
|
|
40
|
+
apiVersion: 1,
|
|
41
|
+
trust_level: 'untrusted',
|
|
42
|
+
displayName: { en: 'Footer credit', ja: 'フッタークレジット' },
|
|
43
|
+
capabilities: ['publicBody', 'adminSettings'],
|
|
44
|
+
settings: {
|
|
45
|
+
public: [
|
|
46
|
+
{
|
|
47
|
+
type: 'text',
|
|
48
|
+
key: 'html',
|
|
49
|
+
label: { en: 'Snippet', ja: 'スニペット' },
|
|
50
|
+
default: '',
|
|
51
|
+
},
|
|
52
|
+
],
|
|
53
|
+
},
|
|
54
|
+
publicBodyEnd(ctx) {
|
|
55
|
+
const html = (ctx.setting<string>('html') ?? '').trim()
|
|
56
|
+
if (!html) return []
|
|
57
|
+
return [{ type: 'noscript', id: `footer-credit-${instanceId}`, html }]
|
|
58
|
+
},
|
|
59
|
+
})
|
|
60
|
+
}
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
`cms.config.ts` で register:
|
|
64
|
+
|
|
65
|
+
```typescript
|
|
66
|
+
import footerCreditPlugin from './plugins/footer-credit'
|
|
67
|
+
|
|
68
|
+
export default defineConfig({
|
|
69
|
+
// ...
|
|
70
|
+
plugins: [
|
|
71
|
+
// ...既存 plugin...
|
|
72
|
+
footerCreditPlugin(),
|
|
73
|
+
],
|
|
74
|
+
})
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
これだけ。`next dev` 再起動 → `/admin/plugins` でスニペットを設定すれば、
|
|
78
|
+
すべての公開ページの `</body>` 直前にレンダされる。
|
|
79
|
+
|
|
80
|
+
## 宣言できる surface
|
|
81
|
+
|
|
82
|
+
プラグインは `definePlugin({...})` で組み立てるただのオブジェクト。型は
|
|
83
|
+
`node_modules/ampless/dist/plugin.d.ts` を見るか、[プラグイン作者ガイド][guide]
|
|
84
|
+
の散文で。
|
|
85
|
+
|
|
86
|
+
現状 active な capability:
|
|
87
|
+
|
|
88
|
+
| capability | 用途 |
|
|
89
|
+
|---|---|
|
|
90
|
+
| `publicHead` | `<head>` に site-wide で挿入される descriptor |
|
|
91
|
+
| `publicBody` | `</body>` 直前に site-wide で挿入される descriptor |
|
|
92
|
+
| `metadata` | post ごとの Next.js Metadata 拡張 |
|
|
93
|
+
| `eventHooks` | trusted/untrusted の背景ハンドラ (content lifecycle / media event 等) |
|
|
94
|
+
| `adminSettings` | `settings.public[]` で宣言する admin 編集可能な設定 |
|
|
95
|
+
| `writePublicAsset` | trusted plugin が `public/plugins/<instanceId>/...` に書き込み |
|
|
96
|
+
| `schema` | post ごとの JSON-LD を `publicBodyForPost` 経由で (テーマ側で `ampless.publicBodyForPost(post)` を呼ぶ前提) |
|
|
97
|
+
|
|
98
|
+
[guide]: https://github.com/heavymoons/ampless/blob/main/packages/ampless/docs/plugin-author-guide.md
|
|
99
|
+
|
|
100
|
+
## TypeScript 設定
|
|
101
|
+
|
|
102
|
+
`tsconfig.json` の `**/*.ts` include glob でこのディレクトリも自動的に
|
|
103
|
+
カバーされている。`import x from './plugins/foo'` も
|
|
104
|
+
`import x from '@/plugins/foo'` も追加設定なしで動く。
|
|
105
|
+
|
|
106
|
+
## `update-ampless` はどうなるか
|
|
107
|
+
|
|
108
|
+
何もしない。`plugins/` は upgrade ツールの protected list に入っているので、
|
|
109
|
+
ここに置いたファイルは上書きも削除もされない。代償として **この README
|
|
110
|
+
も更新されない** — プラグイン API に大きな変更があるときは、本家リポジトリの
|
|
111
|
+
[プラグイン作者ガイド][guide] で最新版を確認すること。
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
> 日本語版: [README.ja.md](./README.ja.md)
|
|
2
|
+
|
|
3
|
+
# Site-local plugins
|
|
4
|
+
|
|
5
|
+
This directory is for **plugins that belong only to this site** — small
|
|
6
|
+
customizations you don't want to publish as a separate npm package.
|
|
7
|
+
|
|
8
|
+
`update-ampless` never touches files in this directory, so anything you
|
|
9
|
+
add here is yours to keep.
|
|
10
|
+
|
|
11
|
+
## When to use a local plugin
|
|
12
|
+
|
|
13
|
+
Reach for a local plugin when you want a site-specific feature that:
|
|
14
|
+
|
|
15
|
+
- needs the plugin surface (`publicHead` / `publicBodyEnd` / `metadata` / `eventHooks` / etc.)
|
|
16
|
+
- is specific to this site (a one-off footer credit, a custom JSON-LD enrichment, an analytics snippet you're not yet ready to ship as a reusable package)
|
|
17
|
+
- you'd rather not version-bump or republish to iterate on
|
|
18
|
+
|
|
19
|
+
When the plugin grows useful for more than one site, lift it into its
|
|
20
|
+
own npm package (see `npx create-ampless plugin --standalone` once it
|
|
21
|
+
ships in Phase 5; for now you can copy it out by hand).
|
|
22
|
+
|
|
23
|
+
## Minimal example
|
|
24
|
+
|
|
25
|
+
`plugins/footer-credit/index.ts`:
|
|
26
|
+
|
|
27
|
+
```typescript
|
|
28
|
+
import { definePlugin, type AmplessPlugin } from 'ampless'
|
|
29
|
+
|
|
30
|
+
export interface FooterCreditOptions {
|
|
31
|
+
instanceId?: string
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export default function footerCreditPlugin(
|
|
35
|
+
options: FooterCreditOptions = {}
|
|
36
|
+
): AmplessPlugin {
|
|
37
|
+
const instanceId = options.instanceId ?? 'footer-credit'
|
|
38
|
+
return definePlugin({
|
|
39
|
+
name: 'footer-credit',
|
|
40
|
+
instanceId,
|
|
41
|
+
apiVersion: 1,
|
|
42
|
+
trust_level: 'untrusted',
|
|
43
|
+
displayName: { en: 'Footer credit', ja: 'フッタークレジット' },
|
|
44
|
+
capabilities: ['publicBody', 'adminSettings'],
|
|
45
|
+
settings: {
|
|
46
|
+
public: [
|
|
47
|
+
{
|
|
48
|
+
type: 'text',
|
|
49
|
+
key: 'html',
|
|
50
|
+
label: { en: 'Snippet', ja: 'スニペット' },
|
|
51
|
+
default: '',
|
|
52
|
+
},
|
|
53
|
+
],
|
|
54
|
+
},
|
|
55
|
+
publicBodyEnd(ctx) {
|
|
56
|
+
const html = (ctx.setting<string>('html') ?? '').trim()
|
|
57
|
+
if (!html) return []
|
|
58
|
+
return [{ type: 'noscript', id: `footer-credit-${instanceId}`, html }]
|
|
59
|
+
},
|
|
60
|
+
})
|
|
61
|
+
}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Register it in `cms.config.ts`:
|
|
65
|
+
|
|
66
|
+
```typescript
|
|
67
|
+
import footerCreditPlugin from './plugins/footer-credit'
|
|
68
|
+
|
|
69
|
+
export default defineConfig({
|
|
70
|
+
// ...
|
|
71
|
+
plugins: [
|
|
72
|
+
// ...existing plugins...
|
|
73
|
+
footerCreditPlugin(),
|
|
74
|
+
],
|
|
75
|
+
})
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
That's it. Restart `next dev` and visit `/admin/plugins` to configure
|
|
79
|
+
the snippet, then any public page renders it before `</body>`.
|
|
80
|
+
|
|
81
|
+
## What you can declare
|
|
82
|
+
|
|
83
|
+
A plugin is just an object built with `definePlugin({...})`. The full
|
|
84
|
+
shape lives in `node_modules/ampless/dist/plugin.d.ts` (or [the plugin
|
|
85
|
+
author guide][guide] if you prefer prose).
|
|
86
|
+
|
|
87
|
+
Capabilities currently active:
|
|
88
|
+
|
|
89
|
+
| capability | purpose |
|
|
90
|
+
|---|---|
|
|
91
|
+
| `publicHead` | descriptors rendered in `<head>` site-wide |
|
|
92
|
+
| `publicBody` | descriptors rendered before `</body>` site-wide |
|
|
93
|
+
| `metadata` | per-post Next.js Metadata contributions |
|
|
94
|
+
| `eventHooks` | trusted/untrusted background handlers (content lifecycle, media events, ...) |
|
|
95
|
+
| `adminSettings` | admin-editable settings declared via `settings.public[]` |
|
|
96
|
+
| `writePublicAsset` | trusted plugins can write namespaced files under `public/plugins/<instanceId>/...` |
|
|
97
|
+
| `schema` | per-post JSON-LD via `publicBodyForPost` (theme template must call `ampless.publicBodyForPost(post)`) |
|
|
98
|
+
|
|
99
|
+
[guide]: https://github.com/heavymoons/ampless/blob/main/packages/ampless/docs/plugin-author-guide.md
|
|
100
|
+
|
|
101
|
+
## What about TypeScript?
|
|
102
|
+
|
|
103
|
+
`tsconfig.json` already covers this directory through its `**/*.ts`
|
|
104
|
+
include glob. Both `import x from './plugins/foo'` and
|
|
105
|
+
`import x from '@/plugins/foo'` work; no extra setup needed.
|
|
106
|
+
|
|
107
|
+
## What `update-ampless` does
|
|
108
|
+
|
|
109
|
+
Nothing. `plugins/` is in the upgrade tool's protected list, so files
|
|
110
|
+
here are never overwritten or deleted on upgrade. The flip side is that
|
|
111
|
+
this README does not get refreshed either — when ampless ships
|
|
112
|
+
significant changes to the plugin API, check the [plugin author guide][guide]
|
|
113
|
+
in the canonical repo for the up-to-date version.
|
|
@@ -81,7 +81,7 @@ export default async function BlogHome(_: ThemeRouteContext) {
|
|
|
81
81
|
)}
|
|
82
82
|
{post.excerpt && <p className="mt-2 text-gray-700">{post.excerpt}</p>}
|
|
83
83
|
</Link>
|
|
84
|
-
<TagList tags={post.tags} className="mt-3" />
|
|
84
|
+
<TagList tags={post.tags} className="mt-3" max={3} />
|
|
85
85
|
</li>
|
|
86
86
|
))}
|
|
87
87
|
</ul>
|
|
@@ -12,6 +12,16 @@ import { SiteFooter } from '@/components/site-chrome/site-footer'
|
|
|
12
12
|
|
|
13
13
|
type PostCtx = ThemeRouteContext<{ slug: string }>
|
|
14
14
|
|
|
15
|
+
function buildPostUrl(siteUrl: string, slug: string) {
|
|
16
|
+
return new URL(`/${slug}`, siteUrl).toString()
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function buildXShareUrl(articleUrl: string, title: string, excerpt?: string | null) {
|
|
20
|
+
const text = [`"${title}"`, excerpt].filter(Boolean).join('\n\n')
|
|
21
|
+
const params = new URLSearchParams({ url: articleUrl, text })
|
|
22
|
+
return `https://x.com/intent/tweet?${params.toString()}`
|
|
23
|
+
}
|
|
24
|
+
|
|
15
25
|
export async function generatePostMetadata({ params }: PostCtx): Promise<Metadata> {
|
|
16
26
|
const { slug } = await params
|
|
17
27
|
const post = await ampless.getPublishedPost(slug)
|
|
@@ -28,6 +38,8 @@ export default async function BlogPost({ params }: PostCtx) {
|
|
|
28
38
|
])
|
|
29
39
|
if (!post) notFound()
|
|
30
40
|
|
|
41
|
+
const postBody = await ampless.publicBodyForPost(post)
|
|
42
|
+
|
|
31
43
|
const defaultLightbox = settings.media.imageDisplay === 'lightbox'
|
|
32
44
|
const maxWidth = settings.media.imageMaxWidth ?? '100%'
|
|
33
45
|
const proseStyle: React.CSSProperties = {
|
|
@@ -36,6 +48,8 @@ export default async function BlogPost({ params }: PostCtx) {
|
|
|
36
48
|
const showHeader =
|
|
37
49
|
parseLinkList(theme.values.headerNav).length > 0 || !!theme.values.logoUrl?.trim()
|
|
38
50
|
const showFooter = parseLinkList(theme.values.footerLinks).length > 0
|
|
51
|
+
const articleUrl = buildPostUrl(settings.site.url, post.slug)
|
|
52
|
+
const xShareUrl = buildXShareUrl(articleUrl, post.title, post.excerpt)
|
|
39
53
|
|
|
40
54
|
return (
|
|
41
55
|
<>
|
|
@@ -63,6 +77,8 @@ export default async function BlogPost({ params }: PostCtx) {
|
|
|
63
77
|
)}
|
|
64
78
|
</header>
|
|
65
79
|
|
|
80
|
+
{postBody}
|
|
81
|
+
|
|
66
82
|
<div
|
|
67
83
|
id="post-body"
|
|
68
84
|
className="prose prose-neutral dark:prose-invert max-w-none [&_img]:max-w-[var(--ampless-img-max-width)] [&_img]:mx-auto"
|
|
@@ -71,6 +87,16 @@ export default async function BlogPost({ params }: PostCtx) {
|
|
|
71
87
|
/>
|
|
72
88
|
|
|
73
89
|
<TagList tags={post.tags} className="mt-8 border-t pt-6" />
|
|
90
|
+
|
|
91
|
+
<footer className="mt-6 flex flex-wrap items-center gap-x-6 gap-y-2 text-sm text-gray-500">
|
|
92
|
+
<a href={articleUrl} className="hover:text-foreground hover:underline">{admin.t('public.permalink')}</a>
|
|
93
|
+
<a href={xShareUrl} target="_blank" rel="noopener noreferrer" className="inline-flex items-center gap-1.5 hover:text-foreground hover:underline">
|
|
94
|
+
<svg aria-hidden="true" viewBox="0 0 24 24" className="h-3.5 w-3.5" fill="currentColor">
|
|
95
|
+
<path d="M13.8 10.4 21 2h-1.7l-6.2 7.3L8.1 2H2.3l7.6 11.1L2.3 22h1.7l6.7-7.8 5.4 7.8h5.8l-8.1-11.6Zm-2.4 2.8-.8-1.1L4.5 3.3h2.8l4.9 7.1.8 1.1 6.4 9.2h-2.8l-5.2-7.5Z" />
|
|
96
|
+
</svg>
|
|
97
|
+
{admin.t('public.shareOnX')}
|
|
98
|
+
</a>
|
|
99
|
+
</footer>
|
|
74
100
|
</article>
|
|
75
101
|
|
|
76
102
|
<LightboxBinder scopeSelector="#post-body" defaultLightbox={defaultLightbox} />
|
|
@@ -28,6 +28,8 @@ export default async function CorporatePost({ params }: PostCtx) {
|
|
|
28
28
|
])
|
|
29
29
|
if (!post) notFound()
|
|
30
30
|
|
|
31
|
+
const postBody = await ampless.publicBodyForPost(post)
|
|
32
|
+
|
|
31
33
|
const defaultLightbox = settings.media.imageDisplay === 'lightbox'
|
|
32
34
|
const maxWidth = settings.media.imageMaxWidth ?? '100%'
|
|
33
35
|
const proseStyle: React.CSSProperties = {
|
|
@@ -64,6 +66,8 @@ export default async function CorporatePost({ params }: PostCtx) {
|
|
|
64
66
|
)}
|
|
65
67
|
</header>
|
|
66
68
|
|
|
69
|
+
{postBody}
|
|
70
|
+
|
|
67
71
|
<div
|
|
68
72
|
id="post-body"
|
|
69
73
|
className="prose prose-neutral dark:prose-invert max-w-none [&_img]:max-w-[var(--ampless-img-max-width)] [&_img]:mx-auto"
|
|
@@ -28,6 +28,8 @@ export default async function DadsPost({ params }: PostCtx) {
|
|
|
28
28
|
])
|
|
29
29
|
if (!post) notFound()
|
|
30
30
|
|
|
31
|
+
const postBody = await ampless.publicBodyForPost(post)
|
|
32
|
+
|
|
31
33
|
const defaultLightbox = settings.media.imageDisplay === 'lightbox'
|
|
32
34
|
const maxWidth = settings.media.imageMaxWidth ?? '100%'
|
|
33
35
|
const proseStyle: React.CSSProperties = {
|
|
@@ -69,6 +71,8 @@ export default async function DadsPost({ params }: PostCtx) {
|
|
|
69
71
|
)}
|
|
70
72
|
</header>
|
|
71
73
|
|
|
74
|
+
{postBody}
|
|
75
|
+
|
|
72
76
|
<div
|
|
73
77
|
id="post-body"
|
|
74
78
|
className="prose prose-neutral dark:prose-invert max-w-none [&_a]:text-[var(--primary)] [&_a]:underline-offset-4 [&_img]:max-w-[var(--ampless-img-max-width)] [&_img]:mx-auto"
|
|
@@ -32,6 +32,8 @@ export default async function DocsPost({ params }: PostCtx) {
|
|
|
32
32
|
])
|
|
33
33
|
if (!post) notFound()
|
|
34
34
|
|
|
35
|
+
const postBody = await ampless.publicBodyForPost(post)
|
|
36
|
+
|
|
35
37
|
const defaultLightbox = settings.media.imageDisplay === 'lightbox'
|
|
36
38
|
const maxWidth = settings.media.imageMaxWidth ?? '100%'
|
|
37
39
|
const proseStyle: React.CSSProperties = {
|
|
@@ -66,6 +68,8 @@ export default async function DocsPost({ params }: PostCtx) {
|
|
|
66
68
|
)}
|
|
67
69
|
</header>
|
|
68
70
|
|
|
71
|
+
{postBody}
|
|
72
|
+
|
|
69
73
|
<div
|
|
70
74
|
id="post-body"
|
|
71
75
|
className="prose prose-neutral dark:prose-invert max-w-none [&_img]:max-w-[var(--ampless-img-max-width)] [&_img]:mx-auto"
|
|
@@ -28,6 +28,8 @@ export default async function LandingPost({ params }: PostCtx) {
|
|
|
28
28
|
])
|
|
29
29
|
if (!post) notFound()
|
|
30
30
|
|
|
31
|
+
const postBody = await ampless.publicBodyForPost(post)
|
|
32
|
+
|
|
31
33
|
const defaultLightbox = settings.media.imageDisplay === 'lightbox'
|
|
32
34
|
const maxWidth = settings.media.imageMaxWidth ?? '100%'
|
|
33
35
|
const proseStyle: React.CSSProperties = {
|
|
@@ -66,6 +68,8 @@ export default async function LandingPost({ params }: PostCtx) {
|
|
|
66
68
|
)}
|
|
67
69
|
</header>
|
|
68
70
|
|
|
71
|
+
{postBody}
|
|
72
|
+
|
|
69
73
|
<div
|
|
70
74
|
id="post-body"
|
|
71
75
|
className="prose prose-neutral dark:prose-invert max-w-none [&_img]:max-w-[var(--ampless-img-max-width)] [&_img]:mx-auto"
|
|
@@ -25,6 +25,8 @@ export default async function MinimalPost({ params }: PostCtx) {
|
|
|
25
25
|
])
|
|
26
26
|
if (!post) notFound()
|
|
27
27
|
|
|
28
|
+
const postBody = await ampless.publicBodyForPost(post)
|
|
29
|
+
|
|
28
30
|
const defaultLightbox = settings.media.imageDisplay === 'lightbox'
|
|
29
31
|
const maxWidth = settings.media.imageMaxWidth ?? '100%'
|
|
30
32
|
const proseStyle: React.CSSProperties = {
|
|
@@ -47,6 +49,8 @@ export default async function MinimalPost({ params }: PostCtx) {
|
|
|
47
49
|
)}
|
|
48
50
|
</header>
|
|
49
51
|
|
|
52
|
+
{postBody}
|
|
53
|
+
|
|
50
54
|
<div
|
|
51
55
|
id="post-body"
|
|
52
56
|
className="prose prose-neutral dark:prose-invert max-w-none [&_img]:max-w-[var(--ampless-img-max-width)] [&_img]:mx-auto"
|