@ampless/plugin-schema-jsonld 0.1.1-alpha.0

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 ampless contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.ja.md ADDED
@@ -0,0 +1,90 @@
1
+ > English: [README.md](./README.md)
2
+ >
3
+
4
+ # @ampless/plugin-schema-jsonld
5
+
6
+ [ampless](https://github.com/heavymoons/ampless) 向け JSON-LD 構造化データ (Article スキーマ) プラグイン。
7
+
8
+ > **プレリリース / アルファ版。** v1.0 まではマイナーバージョンでも破壊的変更が入る可能性があります。
9
+
10
+ `publicBodyForPost` フック (Phase 4) を使って、投稿ページの body 内に `<script type="application/ld+json">` 要素を出力します。スクリプトには、投稿フィールドと 4 つの管理設定から組み立てた [schema.org](https://schema.org/) Article 系オブジェクトが格納されます。
11
+
12
+ AWS のデータ権限は不要です。プラグインは公開 Next.js プロセス内でリクエスト時に動くだけの純粋関数です。`trust_level` は `untrusted`。
13
+
14
+ ## インストール
15
+
16
+ ```bash
17
+ npm install @ampless/plugin-schema-jsonld@alpha
18
+ ```
19
+
20
+ ## 設定
21
+
22
+ `cms.config.ts` に記述します:
23
+
24
+ ```ts
25
+ import { defineConfig } from 'ampless'
26
+ import schemaJsonLdPlugin from '@ampless/plugin-schema-jsonld'
27
+
28
+ export default defineConfig({
29
+ // ...
30
+ plugins: [
31
+ schemaJsonLdPlugin(),
32
+ ],
33
+ })
34
+ ```
35
+
36
+ オプションはすべて省略可能です。著者名・発行者名は空のとき `site.name` にフォールバックします:
37
+
38
+ ```ts
39
+ schemaJsonLdPlugin({
40
+ articleType: 'BlogPosting', // デフォルト: 'Article'
41
+ authorName: 'Jane Smith', // デフォルト: site.name
42
+ publisherName: 'Acme Blog', // デフォルト: site.name
43
+ publisherLogo: 'https://example.com/logo.png', // デフォルト: 省略
44
+ })
45
+ ```
46
+
47
+ | オプション | デフォルト | 備考 |
48
+ |---|---|---|
49
+ | `articleType` | `'Article'` | schema.org の `@type`。`Article`、`NewsArticle`、`BlogPosting`、`TechArticle` から選択。`/admin/plugins` から上書き可能。 |
50
+ | `authorName` | `site.name` | スキーマ内の著者 `Person` の名前。空文字または未指定 → `site.name`。 |
51
+ | `publisherName` | `site.name` | 発行者 `Organization` の名前。空文字または未指定 → `site.name`。 |
52
+ | `publisherLogo` | _(省略)_ | 発行者ロゴ画像の絶対 URL。空文字または未指定 → スキーマからロゴを省略。 |
53
+ | `instanceId` | `'schema-jsonld'` | script 要素 id の namespace。2 つのインスタンスを登録する場合にのみ必要。 |
54
+
55
+ ## 管理画面からの設定
56
+
57
+ 上記 4 つのオプションは `/admin/plugins` から編集できる管理設定としても公開されています。管理画面で保存した値はコンストラクタオプションより優先されるため、サイトを再デプロイせずに変更できます。
58
+
59
+ ## アーティクル種別の選び方
60
+
61
+ | `@type` | 適した用途 |
62
+ |---|---|
63
+ | `Article` | 汎用のデフォルト。一般的なブログや企業ニュースに最適。 |
64
+ | `BlogPosting` | 個人ブログや非公式な記事。Google はリッチリザルトの扱いを `Article` と同等にしています。 |
65
+ | `NewsArticle` | ニュース・ジャーナリズムサイト。Google ニュースのリッチリザルト対象。 |
66
+ | `TechArticle` | 技術ドキュメントやハウツーガイド。 |
67
+
68
+ 適格性ガイドラインは [Google の構造化データドキュメント](https://developers.google.com/search/docs/appearance/structured-data/article) を参照してください。
69
+
70
+ ## Google リッチリザルトテストで確認
71
+
72
+ デプロイ後、以下で出力を確認できます:
73
+
74
+ **<https://search.google.com/test/rich-results>**
75
+
76
+ 1. サイト上の任意の投稿 URL を入力します。
77
+ 2. ツールが `<script type="application/ld+json">` ブロックを解析し、検出したエンティティ種別を表示します。
78
+ 3. 「有効なアイテムが検出されました」と表示されれば、スキーマが正しく認識されています。
79
+
80
+ ## トラストレベル
81
+
82
+ `untrusted`。プラグインは `@ampless/runtime` が検証・描画する body descriptor を返すだけです。DynamoDB、S3、Lambda プロセッサーには一切触れません。
83
+
84
+ ## 既知の制約
85
+
86
+ - **著者・発行者が `site.name` にフォールバック** — プラグインは投稿単位の著者レコードにアクセスしません。`authorName` と `publisherName` が空(デフォルト)のとき、どちらも `site.name` を使います。管理設定またはコンストラクタオプションで実名を設定してください。
87
+ - **tags → `keywords` へのマッピング** — `post.tags`(存在する場合)は `, ` で結合されて `keywords` フィールドに書き込まれます。`ItemList` ではなくカンマ区切り文字列です。
88
+ - **ホームページ・一覧ページでは出力されない** — `publicBodyForPost` は投稿個別ページにのみ呼ばれます。インデックスページやタグ一覧などの非投稿ページにはディスクリプタが出力されません。
89
+ - **`BlogPosting` vs `Article`(Google の扱い)** — Google はリッチリザルトにおいてどちらも同等に扱います。非公式な記事には `BlogPosting`、編集コンテンツには `Article`、ニュース媒体を運営している場合のみ `NewsArticle` を選んでください。
90
+ - **image フィールド** — 投稿にカバー画像 URL がある場合でも、現時点では `image` フィールドをスキーマに含めません。将来のリリースで対応予定です。
package/README.md ADDED
@@ -0,0 +1,90 @@
1
+ > 日本語版: [README.ja.md](./README.ja.md)
2
+ >
3
+
4
+ # @ampless/plugin-schema-jsonld
5
+
6
+ JSON-LD structured data (Article schema) plugin for [ampless](https://github.com/heavymoons/ampless).
7
+
8
+ > **Pre-release / alpha.** Breaking changes possible in any minor version until v1.0.
9
+
10
+ Emits a `<script type="application/ld+json">` element inside the post body via the `publicBodyForPost` hook (Phase 4). The script carries an Article-family [schema.org](https://schema.org/) object built from the post's fields and four admin-managed settings.
11
+
12
+ No AWS data permissions are required — the plugin is a pure function that runs at request time inside the public Next.js process. The plugin's `trust_level` is `untrusted`.
13
+
14
+ ## Install
15
+
16
+ ```bash
17
+ npm install @ampless/plugin-schema-jsonld@alpha
18
+ ```
19
+
20
+ ## Configure
21
+
22
+ In `cms.config.ts`:
23
+
24
+ ```ts
25
+ import { defineConfig } from 'ampless'
26
+ import schemaJsonLdPlugin from '@ampless/plugin-schema-jsonld'
27
+
28
+ export default defineConfig({
29
+ // ...
30
+ plugins: [
31
+ schemaJsonLdPlugin(),
32
+ ],
33
+ })
34
+ ```
35
+
36
+ All options are optional. The plugin works out of the box and falls back to `site.name` for author / publisher:
37
+
38
+ ```ts
39
+ schemaJsonLdPlugin({
40
+ articleType: 'BlogPosting', // default: 'Article'
41
+ authorName: 'Jane Smith', // default: site.name
42
+ publisherName: 'Acme Blog', // default: site.name
43
+ publisherLogo: 'https://example.com/logo.png', // default: omitted
44
+ })
45
+ ```
46
+
47
+ | Option | Default | Notes |
48
+ |---|---|---|
49
+ | `articleType` | `'Article'` | schema.org `@type`. One of `Article`, `NewsArticle`, `BlogPosting`, `TechArticle`. Can be overridden per-site from `/admin/plugins`. |
50
+ | `authorName` | `site.name` | Author `Person` name in the schema. Empty string or absent → `site.name`. |
51
+ | `publisherName` | `site.name` | Publisher `Organization` name. Empty string or absent → `site.name`. |
52
+ | `publisherLogo` | _(omitted)_ | Absolute URL of the publisher logo image. Empty string or absent → logo omitted from the schema. |
53
+ | `instanceId` | `'schema-jsonld'` | Namespace for the script element id. Needed only when registering two instances. |
54
+
55
+ ## Admin-managed settings
56
+
57
+ The four options above are also exposed as admin-editable fields at `/admin/plugins`. Values stored there take precedence over the constructor options, so teams can update them without redeploying the site.
58
+
59
+ ## Choosing an Article type
60
+
61
+ | `@type` | Use when |
62
+ |---|---|
63
+ | `Article` | General-purpose default. Works for most blogs and company news. |
64
+ | `BlogPosting` | Personal blog or informal writing. Google treats it like `Article` for rich results. |
65
+ | `NewsArticle` | News and journalism sites. Eligible for Google News rich results. |
66
+ | `TechArticle` | Technical documentation or how-to guides. |
67
+
68
+ See [Google's structured data documentation](https://developers.google.com/search/docs/appearance/structured-data/article) for eligibility guidelines.
69
+
70
+ ## Verify with Google Rich Results Test
71
+
72
+ After deploying, verify the output at:
73
+
74
+ **<https://search.google.com/test/rich-results>**
75
+
76
+ 1. Enter the URL of any post on your site.
77
+ 2. The tool parses the `<script type="application/ld+json">` block and shows detected entity types.
78
+ 3. A green "Valid items detected" result means the schema is well-formed.
79
+
80
+ ## Trust level
81
+
82
+ `untrusted`. The plugin only contributes body descriptors that are validated and rendered by `@ampless/runtime`. It does not touch DynamoDB, S3, or any Lambda processor.
83
+
84
+ ## Known limitations
85
+
86
+ - **author/publisher fallback to `site.name`** — the plugin does not have access to per-post author records. When `authorName` and `publisherName` are empty (default), both use `site.name`. Set a real author name via the admin settings or the constructor option.
87
+ - **tags → `keywords`** — `post.tags` (when present) are joined with `, ` and written to the `keywords` field. This is a flat comma-separated string, which matches common structured-data consumers; it is not an `ItemList`.
88
+ - **home page / listing routes** — `publicBodyForPost` is only called for individual post pages. Index, tag-listing, and other non-post routes do not receive the descriptor.
89
+ - **`BlogPosting` vs `Article` for Google** — Google treats both types identically for Article rich results. Prefer `BlogPosting` for informal writing, `Article` for editorial content, and `NewsArticle` only if you operate a news publication.
90
+ - **image field** — the schema does not include an `image` field. If your posts have a cover image URL, consider extending the plugin in a future release.
@@ -0,0 +1,47 @@
1
+ import { AmplessPlugin } from 'ampless';
2
+
3
+ interface SchemaJsonLdOptions {
4
+ /**
5
+ * Default schema.org `@type` for this plugin instance — used as the
6
+ * manifest default for the `articleType` admin field. The admin
7
+ * value always wins at runtime; this option only seeds the initial
8
+ * default and acts as a fallback when an admin save somehow ends up
9
+ * with a value outside `ARTICLE_TYPES` (defence against a future
10
+ * manifest change).
11
+ */
12
+ articleType?: 'Article' | 'NewsArticle' | 'BlogPosting' | 'TechArticle';
13
+ /**
14
+ * Default author name — used as the manifest default for the
15
+ * `authorName` admin field. The admin value (including an explicit
16
+ * empty string, which means "fall back to site.name") always wins
17
+ * at runtime; this option only seeds the initial default.
18
+ */
19
+ authorName?: string;
20
+ /**
21
+ * Default publisher name — used as the manifest default for the
22
+ * `publisherName` admin field. Same precedence as `authorName`.
23
+ */
24
+ publisherName?: string;
25
+ /**
26
+ * Default publisher logo URL — used as the manifest default for the
27
+ * `publisherLogo` admin field. Same precedence as `authorName`. An
28
+ * empty stored value (admin save or default) omits the logo from
29
+ * the schema.
30
+ */
31
+ publisherLogo?: string;
32
+ /**
33
+ * Optional namespace when registering multiple instances (rare for
34
+ * schema). Defaults to `'schema-jsonld'`.
35
+ */
36
+ instanceId?: string;
37
+ }
38
+ /**
39
+ * Factory for the JSON-LD schema plugin. Returns a plugin manifest that
40
+ * emits a single `<script type="application/ld+json">` per post via
41
+ * `publicBodyForPost`. The plugin is `untrusted` — it only builds a pure
42
+ * in-memory object from post fields and site config; it does not touch
43
+ * DynamoDB, S3, or any Lambda processor.
44
+ */
45
+ declare function schemaJsonLdPlugin(options?: SchemaJsonLdOptions): AmplessPlugin;
46
+
47
+ export { type SchemaJsonLdOptions, schemaJsonLdPlugin as default };
package/dist/index.js ADDED
@@ -0,0 +1,126 @@
1
+ // src/index.ts
2
+ import {
3
+ definePlugin
4
+ } from "ampless";
5
+ var ARTICLE_TYPES = [
6
+ "Article",
7
+ "NewsArticle",
8
+ "BlogPosting",
9
+ "TechArticle"
10
+ ];
11
+ function normalizeBaseUrl(u) {
12
+ return u.endsWith("/") ? u : u + "/";
13
+ }
14
+ function buildSchema(post, site, opts) {
15
+ const url = new URL(
16
+ encodeURIComponent(post.slug),
17
+ normalizeBaseUrl(site.url)
18
+ ).toString();
19
+ return {
20
+ "@context": "https://schema.org",
21
+ "@type": opts.articleType,
22
+ headline: post.title,
23
+ ...post.excerpt ? { description: post.excerpt } : {},
24
+ ...post.publishedAt ? { datePublished: post.publishedAt } : {},
25
+ ...post.updatedAt ? { dateModified: post.updatedAt } : {},
26
+ mainEntityOfPage: { "@type": "WebPage", "@id": url },
27
+ url,
28
+ author: { "@type": "Person", name: opts.authorName },
29
+ publisher: {
30
+ "@type": "Organization",
31
+ name: opts.publisherName,
32
+ ...opts.publisherLogo ? { logo: { "@type": "ImageObject", url: opts.publisherLogo } } : {}
33
+ },
34
+ ...post.tags?.length ? { keywords: post.tags.join(", ") } : {}
35
+ };
36
+ }
37
+ function schemaJsonLdPlugin(options = {}) {
38
+ const instanceId = options.instanceId ?? "schema-jsonld";
39
+ return definePlugin({
40
+ name: "schema-jsonld",
41
+ instanceId,
42
+ apiVersion: 1,
43
+ trust_level: "untrusted",
44
+ displayName: { en: "JSON-LD Schema", ja: "JSON-LD \u30B9\u30AD\u30FC\u30DE" },
45
+ capabilities: ["schema", "adminSettings"],
46
+ settings: {
47
+ public: [
48
+ {
49
+ type: "select",
50
+ key: "articleType",
51
+ label: { en: "Article type", ja: "\u30A2\u30FC\u30C6\u30A3\u30AF\u30EB\u7A2E\u5225" },
52
+ description: {
53
+ en: "schema.org @type used for posts.",
54
+ ja: "\u6295\u7A3F\u306B\u4F7F\u3046 schema.org \u306E @type\u3002"
55
+ },
56
+ default: options.articleType ?? "Article",
57
+ options: ARTICLE_TYPES.map((v) => ({
58
+ value: v,
59
+ label: { en: v, ja: v }
60
+ }))
61
+ },
62
+ {
63
+ type: "text",
64
+ key: "authorName",
65
+ label: { en: "Author name", ja: "\u8457\u8005\u540D" },
66
+ description: {
67
+ en: "Displayed as the article author. Leave empty to use site.name.",
68
+ ja: "\u8A18\u4E8B\u306E\u8457\u8005\u3068\u3057\u3066\u8868\u793A\u3055\u308C\u307E\u3059\u3002\u7A7A\u6B04\u3067 site.name \u3092\u4F7F\u7528\u3002"
69
+ },
70
+ default: options.authorName ?? ""
71
+ },
72
+ {
73
+ type: "text",
74
+ key: "publisherName",
75
+ label: { en: "Publisher name", ja: "\u767A\u884C\u8005\u540D" },
76
+ description: {
77
+ en: "Displayed as the publisher organization. Leave empty to use site.name.",
78
+ ja: "\u767A\u884C\u7D44\u7E54\u3068\u3057\u3066\u8868\u793A\u3055\u308C\u307E\u3059\u3002\u7A7A\u6B04\u3067 site.name \u3092\u4F7F\u7528\u3002"
79
+ },
80
+ default: options.publisherName ?? ""
81
+ },
82
+ {
83
+ type: "url",
84
+ key: "publisherLogo",
85
+ label: { en: "Publisher logo URL", ja: "\u767A\u884C\u8005\u30ED\u30B4 URL" },
86
+ description: {
87
+ en: "Absolute URL of the publisher logo image. Leave empty to omit the logo from the schema.",
88
+ ja: "\u767A\u884C\u8005\u30ED\u30B4\u753B\u50CF\u306E\u7D76\u5BFE URL\u3002\u7A7A\u6B04\u3067\u30B9\u30AD\u30FC\u30DE\u304B\u3089\u30ED\u30B4\u3092\u7701\u7565\u3057\u307E\u3059\u3002"
89
+ },
90
+ default: options.publisherLogo ?? "",
91
+ // Google Rich Results expects an absolute URL for
92
+ // publisher.logo (ImageObject.url); a relative path would
93
+ // be interpreted against whatever page the crawler is on.
94
+ allowRelative: false
95
+ }
96
+ ]
97
+ },
98
+ publicBodyForPost(post, ctx) {
99
+ const site = ctx.site;
100
+ const rawType = (ctx.setting("articleType") ?? "").trim();
101
+ const articleType = ARTICLE_TYPES.includes(rawType) ? rawType : "Article";
102
+ const authorName = (ctx.setting("authorName") ?? "").trim() || site.name;
103
+ const publisherName = (ctx.setting("publisherName") ?? "").trim() || site.name;
104
+ const publisherLogo = (ctx.setting("publisherLogo") ?? "").trim();
105
+ const schema = buildSchema(post, site, {
106
+ articleType,
107
+ authorName,
108
+ publisherName,
109
+ publisherLogo
110
+ });
111
+ return [
112
+ {
113
+ type: "inlineScript",
114
+ id: `schema-jsonld-${instanceId}`,
115
+ scriptType: "application/ld+json",
116
+ // JSON.stringify produces valid JSON; the runtime auto-escapes
117
+ // </script>-breaking chars so we must NOT escape them here.
118
+ body: JSON.stringify(schema)
119
+ }
120
+ ];
121
+ }
122
+ });
123
+ }
124
+ export {
125
+ schemaJsonLdPlugin as default
126
+ };
package/package.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "@ampless/plugin-schema-jsonld",
3
+ "version": "0.1.1-alpha.0",
4
+ "description": "JSON-LD structured data (Article schema) for ampless posts.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "exports": {
8
+ ".": {
9
+ "import": "./dist/index.js",
10
+ "types": "./dist/index.d.ts"
11
+ }
12
+ },
13
+ "files": [
14
+ "dist",
15
+ "README.md"
16
+ ],
17
+ "publishConfig": {
18
+ "access": "public"
19
+ },
20
+ "repository": {
21
+ "type": "git",
22
+ "url": "https://github.com/heavymoons/ampless.git",
23
+ "directory": "packages/plugin-schema-jsonld"
24
+ },
25
+ "homepage": "https://github.com/heavymoons/ampless/tree/main/packages/plugin-schema-jsonld#readme",
26
+ "bugs": "https://github.com/heavymoons/ampless/issues",
27
+ "dependencies": {
28
+ "ampless": "1.0.0-alpha.21"
29
+ },
30
+ "keywords": [
31
+ "ampless",
32
+ "plugin",
33
+ "schema",
34
+ "json-ld",
35
+ "structured-data",
36
+ "seo",
37
+ "article"
38
+ ],
39
+ "scripts": {
40
+ "build": "tsup",
41
+ "dev": "tsup --watch",
42
+ "lint": "tsc --noEmit",
43
+ "test": "vitest run --passWithNoTests"
44
+ }
45
+ }