@cunpingtai/webad-react-ad-scripts 0.1.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 +22 -0
- package/README.md +261 -0
- package/dist/index.d.ts +75 -0
- package/dist/index.js +15288 -0
- package/dist/index.js.map +1 -0
- package/package.json +43 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 webad 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.
|
|
22
|
+
|
package/README.md
ADDED
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
# @cunpingtai/webad-react-ad-scripts
|
|
2
|
+
|
|
3
|
+
面向 Next.js 和 TanStack React 项目的广告组件包。`AdsProvider` 从 WebAd Cloudflare Worker 读取当前站点策略;各广告组件根据策略决定是否渲染,并对脚本 URL 和参数做失败关闭校验。
|
|
4
|
+
|
|
5
|
+
## 安装
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pnpm add @cunpingtai/webad-react-ad-scripts
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
需要 React 18.3 或 React 19。
|
|
12
|
+
|
|
13
|
+
## Next.js App Router
|
|
14
|
+
|
|
15
|
+
新建一个客户端 Provider。`pageKey` 在每次真实路由切换时变化,因此同一广告位可以在新页面重新初始化;普通 state 更新不会重新加载广告。
|
|
16
|
+
|
|
17
|
+
```tsx
|
|
18
|
+
// app/advertising-provider.tsx
|
|
19
|
+
"use client";
|
|
20
|
+
|
|
21
|
+
import { AdsProvider } from "@cunpingtai/webad-react-ad-scripts";
|
|
22
|
+
import { usePathname } from "next/navigation";
|
|
23
|
+
import type { ReactNode } from "react";
|
|
24
|
+
|
|
25
|
+
export function AdvertisingProvider({ children }: { children: ReactNode }) {
|
|
26
|
+
const pathname = usePathname();
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<AdsProvider
|
|
30
|
+
endpoint="https://webad-control-plane.example.workers.dev"
|
|
31
|
+
pageKey={pathname}
|
|
32
|
+
>
|
|
33
|
+
{children}
|
|
34
|
+
</AdsProvider>
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
在根布局中挂载一次:
|
|
40
|
+
|
|
41
|
+
```tsx
|
|
42
|
+
// app/layout.tsx
|
|
43
|
+
import { AdvertisingProvider } from "./advertising-provider";
|
|
44
|
+
|
|
45
|
+
export default function RootLayout({
|
|
46
|
+
children,
|
|
47
|
+
}: {
|
|
48
|
+
children: React.ReactNode;
|
|
49
|
+
}) {
|
|
50
|
+
return (
|
|
51
|
+
<html lang="zh-CN">
|
|
52
|
+
<body>
|
|
53
|
+
<AdvertisingProvider>{children}</AdvertisingProvider>
|
|
54
|
+
</body>
|
|
55
|
+
</html>
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## TanStack Router / Start
|
|
61
|
+
|
|
62
|
+
在根路由组件中读取当前 pathname:
|
|
63
|
+
|
|
64
|
+
```tsx
|
|
65
|
+
import {
|
|
66
|
+
HeadContent,
|
|
67
|
+
Outlet,
|
|
68
|
+
Scripts,
|
|
69
|
+
createRootRoute,
|
|
70
|
+
useRouterState,
|
|
71
|
+
} from "@tanstack/react-router";
|
|
72
|
+
import { AdsProvider } from "@cunpingtai/webad-react-ad-scripts";
|
|
73
|
+
|
|
74
|
+
export const Route = createRootRoute({ component: RootComponent });
|
|
75
|
+
|
|
76
|
+
function RootComponent() {
|
|
77
|
+
const pathname = useRouterState({
|
|
78
|
+
select: (state) => state.location.pathname,
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
return (
|
|
82
|
+
<html lang="zh-CN">
|
|
83
|
+
<head>
|
|
84
|
+
<HeadContent />
|
|
85
|
+
</head>
|
|
86
|
+
<body>
|
|
87
|
+
<AdsProvider
|
|
88
|
+
endpoint="https://webad-control-plane.example.workers.dev"
|
|
89
|
+
pageKey={pathname}
|
|
90
|
+
>
|
|
91
|
+
<Outlet />
|
|
92
|
+
</AdsProvider>
|
|
93
|
+
<Scripts />
|
|
94
|
+
</body>
|
|
95
|
+
</html>
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
不要给 `pageKey` 传 `Date.now()` 或随机数。它应当只在真实 SPA 路由切换时变化。
|
|
101
|
+
|
|
102
|
+
## 广告组件
|
|
103
|
+
|
|
104
|
+
Adsterra 提供的 zone key、脚本 URL 和容器 ID 由使用方传入。不要传整段 HTML。
|
|
105
|
+
|
|
106
|
+
### Social Bar
|
|
107
|
+
|
|
108
|
+
```tsx
|
|
109
|
+
import { AdsterraSocialBar } from "@cunpingtai/webad-react-ad-scripts";
|
|
110
|
+
|
|
111
|
+
<AdsterraSocialBar
|
|
112
|
+
slotId="global-social-bar"
|
|
113
|
+
scriptSrc="https://social.ads-cdn.example/social.js"
|
|
114
|
+
/>;
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Social Bar 在一次完整页面加载中最多初始化一次,仅在 `adsterra-full` 模式显示。
|
|
118
|
+
|
|
119
|
+
### Native Banner
|
|
120
|
+
|
|
121
|
+
```tsx
|
|
122
|
+
import { AdsterraNativeBanner } from "@cunpingtai/webad-react-ad-scripts";
|
|
123
|
+
|
|
124
|
+
<AdsterraNativeBanner
|
|
125
|
+
slotId="article-native-1"
|
|
126
|
+
scriptSrc="https://native.ads-cdn.example/widget/invoke.js"
|
|
127
|
+
containerId="container-native-example"
|
|
128
|
+
className="my-native-ad"
|
|
129
|
+
/>;
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
`containerId` 必须与该 Native Banner 代码对应,并且在当前文档中唯一。
|
|
133
|
+
|
|
134
|
+
### Banner 320×50、300×250、728×90
|
|
135
|
+
|
|
136
|
+
同一个组件支持不同固定尺寸:
|
|
137
|
+
|
|
138
|
+
```tsx
|
|
139
|
+
import { AdsterraBanner } from "@cunpingtai/webad-react-ad-scripts";
|
|
140
|
+
|
|
141
|
+
<AdsterraBanner
|
|
142
|
+
slotId="mobile-top"
|
|
143
|
+
scriptSrc="https://banner.ads-cdn.example/mobile/invoke.js"
|
|
144
|
+
zoneKey="exampleMobileZone"
|
|
145
|
+
width={320}
|
|
146
|
+
height={50}
|
|
147
|
+
/>
|
|
148
|
+
|
|
149
|
+
<AdsterraBanner
|
|
150
|
+
slotId="content-box"
|
|
151
|
+
scriptSrc="https://banner.ads-cdn.example/box/invoke.js"
|
|
152
|
+
zoneKey="exampleBoxZone"
|
|
153
|
+
width={300}
|
|
154
|
+
height={250}
|
|
155
|
+
params={{ placement: "article" }}
|
|
156
|
+
/>
|
|
157
|
+
|
|
158
|
+
<AdsterraBanner
|
|
159
|
+
slotId="desktop-top"
|
|
160
|
+
scriptSrc="https://banner.ads-cdn.example/wide/invoke.js"
|
|
161
|
+
zoneKey="exampleWideZone"
|
|
162
|
+
width={728}
|
|
163
|
+
height={90}
|
|
164
|
+
/>;
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
组件按 Adsterra 原始代码的方式设置 `window.atOptions` 并加载 `invoke.js`,不会自行增加外层 iframe。多个 Banner 会串行初始化,避免互相覆盖 `atOptions`。
|
|
168
|
+
|
|
169
|
+
### Smartlink
|
|
170
|
+
|
|
171
|
+
```tsx
|
|
172
|
+
import { AdsterraSmartlink } from "@cunpingtai/webad-react-ad-scripts";
|
|
173
|
+
|
|
174
|
+
<AdsterraSmartlink
|
|
175
|
+
href="https://links.example/go?key=example"
|
|
176
|
+
className="download-button"
|
|
177
|
+
>
|
|
178
|
+
Continue
|
|
179
|
+
</AdsterraSmartlink>;
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
Smartlink 是普通 `<a>`,默认新窗口打开,并固定带有 `noopener noreferrer sponsored`。
|
|
183
|
+
|
|
184
|
+
### Popunder
|
|
185
|
+
|
|
186
|
+
```tsx
|
|
187
|
+
import { AdsterraPopunder } from "@cunpingtai/webad-react-ad-scripts";
|
|
188
|
+
|
|
189
|
+
<AdsterraPopunder
|
|
190
|
+
slotId="global-popunder"
|
|
191
|
+
scriptSrc="https://pop.ads-cdn.example/pop.js"
|
|
192
|
+
/>;
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
Popunder 在一次完整页面加载中最多初始化一次,仅在 `adsterra-full` 模式显示。
|
|
196
|
+
|
|
197
|
+
## 策略与显示规则
|
|
198
|
+
|
|
199
|
+
| Worker 返回模式 | Native Banner | Banner | Smartlink | Social Bar | Popunder |
|
|
200
|
+
| ----------------- | ------------- | ------ | --------- | ---------- | -------- |
|
|
201
|
+
| `none` | 否 | 否 | 否 | 否 | 否 |
|
|
202
|
+
| `adsense` | 否 | 否 | 否 | 否 | 否 |
|
|
203
|
+
| `adsense-limited` | 是 | 是 | 否 | 否 | 否 |
|
|
204
|
+
| `adsterra-safe` | 是 | 是 | 是 | 否 | 否 |
|
|
205
|
+
| `adsterra-full` | 是 | 是 | 是 | 是 | 是 |
|
|
206
|
+
|
|
207
|
+
AdSense Auto Ads 等全局脚本仍由 Worker 配置中的 `providers.*.scripts` 加载。Adsterra 广告位脚本由上述组件加载,因此使用组件方案时通常把 Worker 的 `adsterra.safeScripts` 和 `adsterra.fullScripts` 留空,避免重复加载。
|
|
208
|
+
|
|
209
|
+
## 生命周期与去重
|
|
210
|
+
|
|
211
|
+
- 同一个 `pageKey` 中,相同 `slotId` 只初始化一次。
|
|
212
|
+
- React 普通 re-render、Strict Mode effect 重放和同页重新挂载不会产生第二次广告请求。
|
|
213
|
+
- `pageKey` 改变后,Native Banner 和 Banner 可以按新的 page view 重新初始化。
|
|
214
|
+
- Social Bar 和 Popunder 无法可靠卸载,因此一次完整文档生命周期只初始化一次;SPA 路由切换不会重复加载。
|
|
215
|
+
- 不要使用随机 React `key`,也不要因为 resize、Tab、FAQ 展开或游戏状态变化而改变 `pageKey`。
|
|
216
|
+
|
|
217
|
+
## AdsProvider 参数与状态
|
|
218
|
+
|
|
219
|
+
```ts
|
|
220
|
+
type AdsProviderProps = {
|
|
221
|
+
endpoint: string;
|
|
222
|
+
pageKey: string;
|
|
223
|
+
timeoutMs?: number; // 默认 3000
|
|
224
|
+
nonce?: string;
|
|
225
|
+
onStatusChange?: (status: AdsProviderStatus) => void;
|
|
226
|
+
children: React.ReactNode;
|
|
227
|
+
};
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
状态包括 `loading-config`、`disabled`、`loading-scripts`、`ready` 和 `error`。例如:
|
|
231
|
+
|
|
232
|
+
```tsx
|
|
233
|
+
<AdsProvider
|
|
234
|
+
endpoint="https://webad-control-plane.example.workers.dev"
|
|
235
|
+
pageKey={pathname}
|
|
236
|
+
nonce={cspNonce}
|
|
237
|
+
onStatusChange={(status) => {
|
|
238
|
+
if (status.state === "error") {
|
|
239
|
+
console.error("Advertising unavailable", status.reason);
|
|
240
|
+
}
|
|
241
|
+
}}
|
|
242
|
+
>
|
|
243
|
+
{children}
|
|
244
|
+
</AdsProvider>
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
配置请求、响应格式或组件参数无效时,组件失败关闭,不插入对应广告脚本。
|
|
248
|
+
|
|
249
|
+
## 脚本域名与 CSP
|
|
250
|
+
|
|
251
|
+
每个组件的 `scriptSrc` 必须:
|
|
252
|
+
|
|
253
|
+
- 使用 HTTPS;
|
|
254
|
+
- hostname 精确存在于 Worker 的 `ALLOWED_SCRIPT_HOSTS_JSON`;
|
|
255
|
+
- 不包含账号密码、显式端口或 URL fragment。
|
|
256
|
+
|
|
257
|
+
站点 CSP 至少需要在 `connect-src` 允许 Worker 地址,在 `script-src` 允许实际广告脚本 origin。使用 nonce 时,把同一个 nonce 传给 `AdsProvider`。Smartlink 是链接而不是脚本,它的 hostname 不需要放入脚本 allowlist,除非同一 hostname 也用于加载脚本。
|
|
258
|
+
|
|
259
|
+
## 发布前真实页面验证
|
|
260
|
+
|
|
261
|
+
第三方广告脚本可能依赖它被插入页面时的执行环境。首次发布前,请在 Adsterra 已批准的测试域名上分别验证五种组件,尤其确认 Banner 的 `invoke.js` 能正常生成广告且没有 `document.write` 错误。未完成这一步前不要发布生产版本。
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
import { ReactNode, CSSProperties, HTMLAttributeAnchorTarget } from 'react';
|
|
3
|
+
|
|
4
|
+
type PublicAdMode = "none" | "adsense" | "adsense-limited" | "adsterra-safe" | "adsterra-full";
|
|
5
|
+
type AdsProviderStatus = {
|
|
6
|
+
state: "idle";
|
|
7
|
+
} | {
|
|
8
|
+
state: "loading-config";
|
|
9
|
+
} | {
|
|
10
|
+
state: "disabled";
|
|
11
|
+
mode: PublicAdMode;
|
|
12
|
+
} | {
|
|
13
|
+
state: "loading-scripts";
|
|
14
|
+
mode: PublicAdMode;
|
|
15
|
+
} | {
|
|
16
|
+
state: "ready";
|
|
17
|
+
mode: PublicAdMode;
|
|
18
|
+
} | {
|
|
19
|
+
state: "error";
|
|
20
|
+
reason: string;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
type AdsProviderProps = {
|
|
24
|
+
endpoint: string;
|
|
25
|
+
pageKey: string;
|
|
26
|
+
timeoutMs?: number;
|
|
27
|
+
nonce?: string;
|
|
28
|
+
onStatusChange?: (status: AdsProviderStatus) => void;
|
|
29
|
+
children: ReactNode;
|
|
30
|
+
};
|
|
31
|
+
declare function AdsProvider({ endpoint, pageKey, timeoutMs, nonce, onStatusChange, children, }: AdsProviderProps): react.JSX.Element;
|
|
32
|
+
|
|
33
|
+
type AdsterraParams = Record<string, string | number | boolean>;
|
|
34
|
+
|
|
35
|
+
type AdsterraBannerProps = {
|
|
36
|
+
slotId: string;
|
|
37
|
+
scriptSrc: string;
|
|
38
|
+
zoneKey: string;
|
|
39
|
+
width: number;
|
|
40
|
+
height: number;
|
|
41
|
+
params?: AdsterraParams;
|
|
42
|
+
className?: string;
|
|
43
|
+
style?: CSSProperties;
|
|
44
|
+
};
|
|
45
|
+
declare function AdsterraBanner({ slotId, scriptSrc, zoneKey, width, height, params, className, style, }: AdsterraBannerProps): react.JSX.Element | null;
|
|
46
|
+
|
|
47
|
+
type AdsterraNativeBannerProps = {
|
|
48
|
+
slotId: string;
|
|
49
|
+
scriptSrc: string;
|
|
50
|
+
containerId: string;
|
|
51
|
+
className?: string;
|
|
52
|
+
style?: CSSProperties;
|
|
53
|
+
};
|
|
54
|
+
declare function AdsterraNativeBanner({ slotId, scriptSrc, containerId, className, style, }: AdsterraNativeBannerProps): react.JSX.Element | null;
|
|
55
|
+
|
|
56
|
+
type AdsterraSmartlinkProps = {
|
|
57
|
+
href: string;
|
|
58
|
+
children: ReactNode;
|
|
59
|
+
target?: HTMLAttributeAnchorTarget;
|
|
60
|
+
className?: string;
|
|
61
|
+
};
|
|
62
|
+
declare function AdsterraSmartlink({ href, children, target, className, }: AdsterraSmartlinkProps): react.JSX.Element | null;
|
|
63
|
+
|
|
64
|
+
type AdsterraSocialBarProps = {
|
|
65
|
+
slotId: string;
|
|
66
|
+
scriptSrc: string;
|
|
67
|
+
};
|
|
68
|
+
type AdsterraPopunderProps = {
|
|
69
|
+
slotId: string;
|
|
70
|
+
scriptSrc: string;
|
|
71
|
+
};
|
|
72
|
+
declare function AdsterraSocialBar(props: AdsterraSocialBarProps): react.JSX.Element;
|
|
73
|
+
declare function AdsterraPopunder(props: AdsterraPopunderProps): react.JSX.Element;
|
|
74
|
+
|
|
75
|
+
export { AdsProvider, type AdsProviderProps, type AdsProviderStatus, AdsterraBanner, type AdsterraBannerProps, AdsterraNativeBanner, type AdsterraNativeBannerProps, AdsterraPopunder, type AdsterraPopunderProps, AdsterraSmartlink, type AdsterraSmartlinkProps, AdsterraSocialBar, type AdsterraSocialBarProps };
|