@astronox/core 0.5.0 → 0.5.2

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.md CHANGED
@@ -63,7 +63,7 @@ component labels can extend `defaultCoreMessages` through
63
63
  claims, Firestore, and app-owned mappings.
64
64
  - [`R2 publication`](docs/integrations/r2-publication.md): full provider
65
65
  snapshots and build-time public projection.
66
- - [`AdSense`](docs/integrations/adsense.md): typed configuration, responsive
67
- units, host policy, Privacy & Messaging, and deployment.
66
+ - [`AdSense`](docs/integrations/adsense.md): typed configuration, Privacy &
67
+ Messaging policy, annotation exclusions, and deployment.
68
68
  - [`release workflow`](docs/release-workflow.md): local links, verification, and
69
69
  consumer pinning order.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@astronox/core",
3
- "version": "0.5.0",
3
+ "version": "0.5.2",
4
4
  "description": "Reusable Astro components, patterns, styles, and infrastructure for AstroNox sites.",
5
5
  "license": "MIT",
6
6
  "keywords": [
@@ -0,0 +1,30 @@
1
+ ---
2
+ type Props = {
3
+ publisherId: string;
4
+ };
5
+
6
+ const { publisherId }: Props = Astro.props;
7
+ const scriptSource = `https://fundingchoicesmessages.google.com/i/${encodeURIComponent(publisherId)}?ers=1`;
8
+ ---
9
+
10
+ <script is:inline async src={scriptSource}></script>
11
+ <script is:inline>
12
+ (() => {
13
+ const signalGooglefcPresent = () => {
14
+ if (window.frames['googlefcPresent']) return;
15
+
16
+ if (!document.body) {
17
+ setTimeout(signalGooglefcPresent, 0);
18
+ return;
19
+ }
20
+
21
+ const frame = document.createElement('iframe');
22
+ frame.name = 'googlefcPresent';
23
+ frame.style.cssText =
24
+ 'display: none; width: 0; height: 0; border: none; z-index: -1000; left: -1000px; top: -1000px;';
25
+ document.body.appendChild(frame);
26
+ };
27
+
28
+ signalGooglefcPresent();
29
+ })();
30
+ </script>
@@ -1,5 +1,7 @@
1
1
  ---
2
2
  import type { AdsenseAdsConfig } from '../contracts';
3
+ import AdsenseAdBlockingRecoveryHead from './AdsenseAdBlockingRecoveryHead.astro';
4
+ import AdsenseMessagingBootstrap from './AdsenseMessagingBootstrap.astro';
3
5
  import { toAdsenseClientId } from './provider';
4
6
 
5
7
  type Props = {
@@ -9,30 +11,22 @@ type Props = {
9
11
  const { config }: Props = Astro.props;
10
12
  const clientId = toAdsenseClientId(config.publisherId);
11
13
  const scriptSource = `https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=${encodeURIComponent(clientId)}`;
12
- const adBlockingRecoverySource = `https://fundingchoicesmessages.google.com/i/${encodeURIComponent(config.publisherId)}?ers=1`;
14
+ const adBlockingRecoveryEnabled = Boolean(
15
+ config.privacyMessaging?.adBlockingRecovery,
16
+ );
13
17
  ---
14
18
 
15
- <script is:inline async src={adBlockingRecoverySource}></script>
16
- <script is:inline>
17
- (() => {
18
- const signalGooglefcPresent = () => {
19
- if (window.frames['googlefcPresent']) return;
20
-
21
- if (!document.body) {
22
- setTimeout(signalGooglefcPresent, 0);
23
- return;
24
- }
25
-
26
- const frame = document.createElement('iframe');
27
- frame.name = 'googlefcPresent';
28
- frame.style.cssText =
29
- 'display: none; width: 0; height: 0; border: none; z-index: -1000; left: -1000px; top: -1000px;';
30
- document.body.appendChild(frame);
31
- };
32
-
33
- signalGooglefcPresent();
34
- })();
35
- </script>
19
+ {
20
+ config.privacyMessaging ? (
21
+ <AdsenseMessagingBootstrap config={config.privacyMessaging} />
22
+ ) : null
23
+ }
24
+
25
+ {
26
+ adBlockingRecoveryEnabled ? (
27
+ <AdsenseAdBlockingRecoveryHead publisherId={config.publisherId} />
28
+ ) : null
29
+ }
36
30
 
37
31
  <meta name="google-adsense-account" content={clientId} />
38
32
  <script is:inline async crossorigin="anonymous" src={scriptSource}></script>
@@ -0,0 +1,179 @@
1
+ ---
2
+ import type { AdsensePrivacyMessagingConfig } from '../contracts';
3
+ import AdsensePublisherRevocationStyle from './AdsensePublisherRevocationStyle.astro';
4
+
5
+ type Props = {
6
+ config: AdsensePrivacyMessagingConfig;
7
+ };
8
+
9
+ const { config }: Props = Astro.props;
10
+ const adBlockingRecovery = config.adBlockingRecovery;
11
+ const publisherLauncher = config.consentRevocation?.launcher === 'publisher';
12
+ const adBlockingEnabled = Boolean(adBlockingRecovery);
13
+ const adBlockingRedisplayIntervalMs =
14
+ adBlockingRecovery?.redisplayIntervalMs ?? 0;
15
+ const adBlockingStorageKey = adBlockingRecovery?.storageKey ?? '';
16
+ ---
17
+
18
+ {publisherLauncher ? <AdsensePublisherRevocationStyle /> : null}
19
+
20
+ <script
21
+ is:inline
22
+ define:vars={{
23
+ adBlockingEnabled,
24
+ adBlockingRedisplayIntervalMs,
25
+ adBlockingStorageKey,
26
+ publisherLauncher,
27
+ }}
28
+ >
29
+ (() => {
30
+ const bootstrapKey = Symbol.for('astronox.adsense.messaging');
31
+ if (window[bootstrapKey]) return;
32
+ window[bootstrapKey] = true;
33
+
34
+ const googlefc = (window.googlefc ??= {});
35
+ googlefc.callbackQueue ??= [];
36
+
37
+ if (adBlockingEnabled) {
38
+ const now = Date.now();
39
+ let storageAvailable = true;
40
+ let storedTimestamp;
41
+
42
+ try {
43
+ const storedValue = window.localStorage.getItem(adBlockingStorageKey);
44
+ const parsedTimestamp = Number(storedValue);
45
+ if (
46
+ storedValue !== null &&
47
+ Number.isFinite(parsedTimestamp) &&
48
+ parsedTimestamp > 0 &&
49
+ parsedTimestamp <= now
50
+ ) {
51
+ storedTimestamp = parsedTimestamp;
52
+ }
53
+ } catch {
54
+ storageAvailable = false;
55
+ }
56
+
57
+ let cooldownActive =
58
+ storedTimestamp !== undefined &&
59
+ now - storedTimestamp < adBlockingRedisplayIntervalMs;
60
+
61
+ const startCooldown = () => {
62
+ if (cooldownActive) return;
63
+
64
+ cooldownActive = true;
65
+ if (!storageAvailable) return;
66
+
67
+ try {
68
+ window.localStorage.setItem(adBlockingStorageKey, String(Date.now()));
69
+ } catch {
70
+ storageAvailable = false;
71
+ }
72
+ };
73
+
74
+ googlefc.controlledMessagingFunction = (message) => {
75
+ if (!message || typeof message.proceed !== 'function') return;
76
+
77
+ if (cooldownActive) {
78
+ const adBlockingType = googlefc.MessageTypeEnum?.AD_BLOCKING;
79
+ if (typeof adBlockingType === 'number') {
80
+ message.proceed(false, [adBlockingType]);
81
+ return;
82
+ }
83
+
84
+ message.proceed(true);
85
+ return;
86
+ }
87
+
88
+ startCooldown();
89
+ message.proceed(true);
90
+ };
91
+ }
92
+
93
+ if (!publisherLauncher) return;
94
+
95
+ const legacyLauncherSelector = '.google-revocation-link-placeholder';
96
+ const toolbarSelector = '#ft-floating-toolbar';
97
+ const hostObservers = new WeakMap();
98
+
99
+ const disconnectHostObserver = (host) => {
100
+ hostObservers.get(host)?.disconnect();
101
+ hostObservers.delete(host);
102
+ };
103
+
104
+ const suppressToolbarLauncher = (host, shadowRoot) => {
105
+ const toolbar = shadowRoot.querySelector(toolbarSelector);
106
+ if (!toolbar) return;
107
+
108
+ toolbar
109
+ .querySelectorAll('.ft-reg-button, #ft-reg-bubble')
110
+ .forEach((element) => element.remove());
111
+
112
+ const remainingToolbarAction = toolbar.querySelector(
113
+ '.ft-button-holder button, .ft-bottom-button-holder button',
114
+ );
115
+ if (remainingToolbarAction) return;
116
+
117
+ disconnectHostObserver(host);
118
+ host.remove();
119
+ };
120
+
121
+ const observeShadowRoot = (host) => {
122
+ const shadowRoot = host.shadowRoot;
123
+ if (!shadowRoot || hostObservers.has(host)) return;
124
+
125
+ suppressToolbarLauncher(host, shadowRoot);
126
+ if (!host.isConnected) return;
127
+
128
+ const observer = new MutationObserver(() => {
129
+ suppressToolbarLauncher(host, shadowRoot);
130
+ });
131
+ observer.observe(shadowRoot, { childList: true, subtree: true });
132
+ hostObservers.set(host, observer);
133
+ };
134
+
135
+ const inspectBodyChild = (element) => {
136
+ if (element.matches(legacyLauncherSelector)) {
137
+ element.remove();
138
+ return;
139
+ }
140
+
141
+ element
142
+ .querySelectorAll(legacyLauncherSelector)
143
+ .forEach((launcher) => launcher.remove());
144
+ observeShadowRoot(element);
145
+ };
146
+
147
+ const startLauncherSuppression = () => {
148
+ if (!document.body) return;
149
+
150
+ for (const child of [...document.body.children]) {
151
+ inspectBodyChild(child);
152
+ }
153
+
154
+ new MutationObserver((records) => {
155
+ for (const record of records) {
156
+ for (const node of record.removedNodes) {
157
+ if (node instanceof Element) disconnectHostObserver(node);
158
+ }
159
+ for (const node of record.addedNodes) {
160
+ if (node instanceof Element) inspectBodyChild(node);
161
+ }
162
+ }
163
+ }).observe(document.body, { childList: true });
164
+ };
165
+
166
+ if (document.body) {
167
+ startLauncherSuppression();
168
+ return;
169
+ }
170
+
171
+ const documentObserver = new MutationObserver(() => {
172
+ if (!document.body) return;
173
+
174
+ documentObserver.disconnect();
175
+ startLauncherSuppression();
176
+ });
177
+ documentObserver.observe(document.documentElement, { childList: true });
178
+ })();
179
+ </script>
@@ -0,0 +1,5 @@
1
+ <style is:global>
2
+ .google-revocation-link-placeholder {
3
+ display: none !important;
4
+ }
5
+ </style>
@@ -2,10 +2,25 @@ export type DisabledAdsConfig = {
2
2
  enabled: false;
3
3
  };
4
4
 
5
+ export type AdsenseAdBlockingRecoveryConfig = {
6
+ redisplayIntervalMs: number;
7
+ storageKey: string;
8
+ };
9
+
10
+ export type AdsenseConsentRevocationConfig = {
11
+ launcher: 'google' | 'publisher';
12
+ };
13
+
14
+ export type AdsensePrivacyMessagingConfig = {
15
+ adBlockingRecovery?: AdsenseAdBlockingRecoveryConfig;
16
+ consentRevocation?: AdsenseConsentRevocationConfig;
17
+ };
18
+
5
19
  export type AdsenseAdsConfig = {
6
20
  enabled: true;
7
21
  provider: 'adsense';
8
22
  publisherId: string;
23
+ privacyMessaging?: AdsensePrivacyMessagingConfig;
9
24
  };
10
25
 
11
26
  export type AdsConfig = DisabledAdsConfig | AdsenseAdsConfig;
@@ -22,5 +37,35 @@ export function defineAdsConfig<const TConfig extends AdsConfig>(
22
37
  if (!/^pub-\d+$/.test(config.publisherId)) {
23
38
  throw new Error('Invalid AdSense publisher id');
24
39
  }
40
+
41
+ const adBlockingRecovery = config.privacyMessaging?.adBlockingRecovery;
42
+ if (adBlockingRecovery) {
43
+ if (
44
+ !Number.isFinite(adBlockingRecovery.redisplayIntervalMs) ||
45
+ adBlockingRecovery.redisplayIntervalMs <= 0
46
+ ) {
47
+ throw new Error(
48
+ 'AdSense ad blocking recovery redisplay interval must be positive',
49
+ );
50
+ }
51
+ if (
52
+ typeof adBlockingRecovery.storageKey !== 'string' ||
53
+ adBlockingRecovery.storageKey.trim().length === 0
54
+ ) {
55
+ throw new Error(
56
+ 'AdSense ad blocking recovery storage key must not be empty',
57
+ );
58
+ }
59
+ }
60
+
61
+ const consentLauncher = config.privacyMessaging?.consentRevocation?.launcher;
62
+ if (
63
+ consentLauncher !== undefined &&
64
+ consentLauncher !== 'google' &&
65
+ consentLauncher !== 'publisher'
66
+ ) {
67
+ throw new Error('Invalid AdSense consent revocation launcher');
68
+ }
69
+
25
70
  return config;
26
71
  }