@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 +2 -2
- package/package.json +1 -1
- package/src/ads/adsense/AdsenseAdBlockingRecoveryHead.astro +30 -0
- package/src/ads/adsense/AdsenseHead.astro +16 -22
- package/src/ads/adsense/AdsenseMessagingBootstrap.astro +179 -0
- package/src/ads/adsense/AdsensePublisherRevocationStyle.astro +5 -0
- package/src/ads/contracts.ts +45 -0
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,
|
|
67
|
-
|
|
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
|
@@ -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
|
|
14
|
+
const adBlockingRecoveryEnabled = Boolean(
|
|
15
|
+
config.privacyMessaging?.adBlockingRecovery,
|
|
16
|
+
);
|
|
13
17
|
---
|
|
14
18
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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>
|
package/src/ads/contracts.ts
CHANGED
|
@@ -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
|
}
|