create-ampless 1.0.0-alpha.71 → 1.0.0-alpha.75

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 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,6 +1782,16 @@ 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$/,
@@ -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
- {tags.map((tag) => (
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 + Phase 2 の機能を網羅 — descriptor ベースの `<head>` / `<body>` 注入、非同期イベントフック、admin 管理の `settings.public` 値です。
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 + Phase 2 surfaces — descriptor-based `<head>` / `<body>`
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.3",
29
- "@ampless/plugin-gtm": "^0.1.1-alpha.2",
30
- "@ampless/plugin-og-image": "^0.2.0-alpha.20",
31
- "@ampless/plugin-plausible": "^0.1.1-alpha.2",
32
- "@ampless/plugin-rss": "^0.2.0-alpha.20",
33
- "@ampless/plugin-seo": "^0.2.0-alpha.20",
34
- "@ampless/plugin-webhook": "^0.2.0-alpha.20",
35
- "@ampless/admin": "^1.0.0-alpha.46",
36
- "@ampless/backend": "^1.0.0-alpha.37",
37
- "@ampless/runtime": "^1.0.0-alpha.26",
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.20",
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",
@@ -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"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-ampless",
3
- "version": "1.0.0-alpha.71",
3
+ "version": "1.0.0-alpha.75",
4
4
  "description": "Create a new ampless project",
5
5
  "license": "MIT",
6
6
  "type": "module",