@ampless/plugin-gtm 0.1.1-alpha.8 → 0.2.0-alpha.10

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/README.ja.md CHANGED
@@ -44,6 +44,51 @@ export default defineConfig({
44
44
  |---|---|---|
45
45
  | `containerId` | `''` | 初期 GTM コンテナ ID。例: `GTM-XXXXXXX`。マニフェストの default を seed するだけで、ライブ値はリクエスト時に `/admin/plugins` から読み出される。空文字でプラグインを無効化したまま ship できる。 |
46
46
  | `instanceId` | `'gtm'` | script / noscript 要素 id と設定保存キーの namespace。同サイトに複数の GTM コンテナを入れる場合に分ける。 |
47
+ | `consentCategory` | `''` | オプションの同意カテゴリ slug。設定すると `window.amplessConsent.has(<この値>)` が true になるまで GTM loader を発火しない。gated mode では `<noscript>` fallback は出力されない。詳細は[同意ゲーティング](#同意ゲーティング)を参照。 |
48
+
49
+ ## 同意ゲーティング
50
+
51
+ デフォルトでは GTM loader は訪問者の同意有無にかかわらずページロードごとに発火します。同意を付与するまで発火を遅延させるには、`consentCategory` に同意カテゴリ slug を設定し、同じ `cms.config.ts` に `@ampless/plugin-cookie-consent` を登録します:
52
+
53
+ ```ts
54
+ import { defineConfig } from 'ampless'
55
+ import cookieConsent from '@ampless/plugin-cookie-consent'
56
+ import gtmPlugin from '@ampless/plugin-gtm'
57
+
58
+ export default defineConfig({
59
+ plugins: [
60
+ // cookie-consent は GTM plugin より前に置く
61
+ cookieConsent({
62
+ categories: [{ id: 'analytics', label: 'アナリティクス', defaultEnabled: false }],
63
+ }),
64
+ gtmPlugin({
65
+ containerId: 'GTM-XXXXXXX',
66
+ consentCategory: 'analytics',
67
+ }),
68
+ ],
69
+ })
70
+ ```
71
+
72
+ `consentCategory` を設定すると plugin は **gated mode** に切り替わります: 標準のインライン loader script の代わりに単一のインライン script を emit し、以下の動作をします:
73
+
74
+ 1. 直ちに `window.amplessConsent.has('analytics')` を確認(`localStorage` から復元した同意に対応)。
75
+ 2. false なら `window.amplessConsent.on('analytics', ...)` で同意イベントを購読して待機。
76
+ 3. GTM plugin が cookie-consent plugin より先にロードされたケースに備え、`ampless:consent-ready` イベントも購読。
77
+
78
+ **gated mode では `<noscript>` fallback を出力しません。** `consentCategory` が設定されている場合、通常 `publicBodyEnd` から出力される GTM の fallback iframe は省略されます。理由: JavaScript を無効にした環境では同意バナーを動かせないため、ゲーティング自体が不能です。fallback を抑制することでトラッキングを出さないのが正しいトレードオフです。これは意図した仕様変更であり、必要に応じてサイトのプライバシーポリシーに記載してください。
79
+
80
+ **Fail-closed 契約:** `consentCategory` を設定したまま `@ampless/plugin-cookie-consent` を登録しなかった場合、`window.amplessConsent` は install されません。GTM は**永久に発火しない**まま 5 秒後に `console.warn` が出力されます:
81
+
82
+ ```
83
+ [ampless:gtm] consentCategory is set but window.amplessConsent never installed.
84
+ Did you forget to register @ampless/plugin-cookie-consent?
85
+ ```
86
+
87
+ この warning は本番環境でも発火します。設定ミスを早期に検出するためのものであり、抑制する仕組みはありません。
88
+
89
+ **プラグインの順序:** `plugins` 配列内で `@ampless/plugin-cookie-consent` を GTM plugin より前に置いてください。
90
+
91
+ Consent Convention および `window.amplessConsent` API の詳細は [docs/architecture/08-plugin-architecture.md](https://github.com/heavymoons/ampless/blob/main/docs/architecture/08-plugin-architecture.md) を参照してください。
47
92
 
48
93
  ## 管理画面からコンテナ ID を編集する
49
94
 
package/README.md CHANGED
@@ -44,6 +44,51 @@ export default defineConfig({
44
44
  |---|---|---|
45
45
  | `containerId` | `''` | Initial GTM container ID, e.g. `GTM-XXXXXXX`. Seeds the manifest default — the live value is read from `/admin/plugins` at request time. Set to `''` to ship the plugin disabled. |
46
46
  | `instanceId` | `'gtm'` | Namespace used for the script / noscript element ids and the settings storage key. Set distinct values when registering multiple GTM containers on the same site. |
47
+ | `consentCategory` | `''` | Optional consent category slug. When set, the GTM loader fires only after `window.amplessConsent.has(<this>)` returns true. The `<noscript>` fallback is suppressed in this mode. See [Consent gating](#consent-gating) below. |
48
+
49
+ ## Consent gating
50
+
51
+ By default the GTM loader fires on every page load regardless of visitor consent. To make it fire only after the visitor has granted consent, set `consentCategory` to a category slug and register `@ampless/plugin-cookie-consent` in the same `cms.config.ts`:
52
+
53
+ ```ts
54
+ import { defineConfig } from 'ampless'
55
+ import cookieConsent from '@ampless/plugin-cookie-consent'
56
+ import gtmPlugin from '@ampless/plugin-gtm'
57
+
58
+ export default defineConfig({
59
+ plugins: [
60
+ // cookie-consent must appear before the analytics plugin
61
+ cookieConsent({
62
+ categories: [{ id: 'analytics', label: 'Analytics', defaultEnabled: false }],
63
+ }),
64
+ gtmPlugin({
65
+ containerId: 'GTM-XXXXXXX',
66
+ consentCategory: 'analytics',
67
+ }),
68
+ ],
69
+ })
70
+ ```
71
+
72
+ When `consentCategory` is set the plugin switches to **gated mode**: the standard inline loader script is replaced by a single inline script that:
73
+
74
+ 1. Checks `window.amplessConsent.has('analytics')` immediately (covers consent restored from `localStorage`).
75
+ 2. Otherwise subscribes to the consent event via `window.amplessConsent.on('analytics', ...)` and waits.
76
+ 3. Also listens for `ampless:consent-ready` in case the GTM plugin loads before the cookie-consent plugin has installed its global API.
77
+
78
+ **`<noscript>` fallback is suppressed in gated mode.** The standard GTM fallback iframe (emitted by `publicBodyEnd`) is omitted when `consentCategory` is set. Rationale: JavaScript-less environments cannot run the consent banner UI, so there is no meaningful way to gate tracking. Suppressing the fallback is the correct trade-off — it avoids tracking visitors who have not consented. This is an intentional behavior change. Document it in your site's privacy policy if needed.
79
+
80
+ **Fail-closed contract:** if `consentCategory` is set but `@ampless/plugin-cookie-consent` is never registered, `window.amplessConsent` is never installed. GTM will **never fire**, and after 5 seconds a `console.warn` appears:
81
+
82
+ ```
83
+ [ampless:gtm] consentCategory is set but window.amplessConsent never installed.
84
+ Did you forget to register @ampless/plugin-cookie-consent?
85
+ ```
86
+
87
+ This warning fires in production too — it is intended to help operators catch misconfiguration quickly. There is no mechanism to suppress it.
88
+
89
+ **Plugin ordering:** register `@ampless/plugin-cookie-consent` before the GTM plugin in the `plugins` array.
90
+
91
+ For full details on the Consent Convention and the `window.amplessConsent` API see [docs/architecture/08-plugin-architecture.md](https://github.com/heavymoons/ampless/blob/main/docs/architecture/08-plugin-architecture.md).
47
92
 
48
93
  ## Editing the container ID from the admin UI
49
94
 
package/dist/index.d.ts CHANGED
@@ -20,6 +20,17 @@ interface GtmOptions {
20
20
  * marketing + a product container on the same site).
21
21
  */
22
22
  instanceId?: string;
23
+ /**
24
+ * Optional consent category. When set, the analytics loader does
25
+ * not run until window.amplessConsent.has(consentCategory) returns
26
+ * true. See @ampless/plugin-cookie-consent and the Consent
27
+ * Convention in docs/architecture/08-plugin-architecture.md.
28
+ *
29
+ * Note: in gated mode the `<noscript>` fallback in publicBodyEnd
30
+ * is suppressed — JavaScript-less environments cannot run the
31
+ * consent UI so there is no meaningful way to gate.
32
+ */
33
+ consentCategory?: string;
23
34
  }
24
35
  /**
25
36
  * Factory for the GTM plugin. Returns a plugin manifest that emits
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  // src/index.ts
2
2
  import { definePlugin } from "ampless";
3
3
  function gtmPlugin(options = {}) {
4
- const { containerId = "", instanceId = "gtm" } = options;
4
+ const { containerId = "", instanceId = "gtm", consentCategory = "" } = options;
5
5
  return definePlugin({
6
6
  name: "gtm",
7
7
  packageName: "@ampless/plugin-gtm",
@@ -30,12 +30,60 @@ function gtmPlugin(options = {}) {
30
30
  pattern: "^$|^GTM-[A-Z0-9]+$",
31
31
  placeholder: "GTM-XXXXXXX",
32
32
  default: containerId
33
+ },
34
+ {
35
+ type: "text",
36
+ key: "consentCategory",
37
+ label: { en: "Consent category", ja: "\u540C\u610F\u30AB\u30C6\u30B4\u30EA" },
38
+ description: {
39
+ en: "Optional. When set, the analytics loader fires only after `window.amplessConsent.has(<this>)` returns true. Requires `@ampless/plugin-cookie-consent` to also be registered \u2014 fail-closed otherwise (no tracking, console warning after 5s). Note: in gated mode the <noscript> fallback is suppressed. See the Consent Convention in the plugin author guide.",
40
+ ja: "\u30AA\u30D7\u30B7\u30E7\u30F3\u3002\u8A2D\u5B9A\u3059\u308B\u3068 `window.amplessConsent.has(<value>)` \u304C true \u306B\u306A\u308B\u307E\u3067 analytics loader \u3092\u767A\u706B\u3057\u307E\u305B\u3093\u3002`@ampless/plugin-cookie-consent` \u306E\u4F75\u7528\u304C\u5FC5\u9808 \u2014 \u672A\u5C0E\u5165\u6642\u306F\u5B8C\u5168\u306B\u767A\u706B\u305B\u305A 5 \u79D2\u5F8C\u306B console warning (fail-closed)\u3002\u6CE8\u610F: gated mode \u3067\u306F <noscript> fallback \u306F\u51FA\u529B\u3055\u308C\u307E\u305B\u3093\u3002\u8A73\u7D30\u306F plugin author guide \u306E Consent Convention \u7BC0\u3092\u53C2\u7167\u3002"
41
+ },
42
+ pattern: "^$|^[a-z][a-z0-9_-]*$",
43
+ maxLength: 32,
44
+ placeholder: "analytics",
45
+ default: consentCategory
33
46
  }
34
47
  ]
35
48
  },
36
49
  publicHead(ctx) {
37
50
  const id = (ctx.setting("containerId") ?? "").trim();
38
51
  if (!id) return [];
52
+ const category = (ctx.setting("consentCategory") ?? "").trim();
53
+ if (category) {
54
+ return [
55
+ {
56
+ type: "inlineScript",
57
+ id: `gtm-gated-${instanceId}`,
58
+ strategy: "afterInteractive",
59
+ body: [
60
+ "(function () {",
61
+ " var initialized = false",
62
+ " function init() {",
63
+ " if (initialized) return",
64
+ " initialized = true",
65
+ ` var gtmSrc = 'https://www.googletagmanager.com/gtm.js?id=' + ${JSON.stringify(id)}`,
66
+ " ;(function(w,d,s,l){w[l]=w[l]||[];w[l].push({'gtm.start':new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],j=d.createElement(s);j.async=true;j.src=gtmSrc;f.parentNode.insertBefore(j,f);})(window,document,'script','dataLayer')",
67
+ " }",
68
+ " function wait() {",
69
+ ` if (window.amplessConsent.has(${JSON.stringify(category)})) init()`,
70
+ ` else window.amplessConsent.on(${JSON.stringify(category)}, init)`,
71
+ " }",
72
+ " if (window.amplessConsent) {",
73
+ " wait()",
74
+ " } else {",
75
+ " window.addEventListener('ampless:consent-ready', wait, { once: true })",
76
+ " setTimeout(function () {",
77
+ " if (!window.amplessConsent) {",
78
+ " console.warn('[ampless:gtm] consentCategory is set but window.amplessConsent never installed. Did you forget to register @ampless/plugin-cookie-consent?')",
79
+ " }",
80
+ " }, 5000)",
81
+ " }",
82
+ "})()"
83
+ ].join("\n")
84
+ }
85
+ ];
86
+ }
39
87
  return [
40
88
  {
41
89
  type: "inlineScript",
@@ -52,6 +100,8 @@ function gtmPlugin(options = {}) {
52
100
  publicBodyEnd(ctx) {
53
101
  const id = (ctx.setting("containerId") ?? "").trim();
54
102
  if (!id) return [];
103
+ const category = (ctx.setting("consentCategory") ?? "").trim();
104
+ if (category) return [];
55
105
  return [
56
106
  {
57
107
  type: "noscript",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ampless/plugin-gtm",
3
- "version": "0.1.1-alpha.8",
3
+ "version": "0.2.0-alpha.10",
4
4
  "description": "Google Tag Manager plugin for ampless",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -26,7 +26,7 @@
26
26
  "homepage": "https://github.com/heavymoons/ampless/tree/main/packages/plugin-gtm#readme",
27
27
  "bugs": "https://github.com/heavymoons/ampless/issues",
28
28
  "dependencies": {
29
- "ampless": "1.0.0-alpha.26"
29
+ "ampless": "1.0.0-alpha.27"
30
30
  },
31
31
  "keywords": [
32
32
  "ampless",