@parchar/cross-promo 0.0.0-stage → 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/dist/analytics.d.ts +47 -0
- package/dist/analytics.d.ts.map +1 -0
- package/dist/analytics.js +93 -0
- package/dist/analytics.js.map +1 -0
- package/dist/cap.d.ts +28 -0
- package/dist/cap.d.ts.map +1 -0
- package/dist/cap.js +112 -0
- package/dist/cap.js.map +1 -0
- package/dist/config/network.json +230 -0
- package/dist/config.d.ts +5 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +6 -0
- package/dist/config.js.map +1 -0
- package/dist/consent.d.ts +13 -0
- package/dist/consent.d.ts.map +1 -0
- package/dist/consent.js +21 -0
- package/dist/consent.js.map +1 -0
- package/dist/element.d.ts +26 -0
- package/dist/element.d.ts.map +1 -0
- package/dist/element.js +159 -0
- package/dist/element.js.map +1 -0
- package/dist/embed-entry.d.ts +7 -0
- package/dist/embed-entry.d.ts.map +1 -0
- package/dist/embed-entry.js +31 -0
- package/dist/embed-entry.js.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +9 -0
- package/dist/index.js.map +1 -0
- package/dist/render.d.ts +33 -0
- package/dist/render.d.ts.map +1 -0
- package/dist/render.js +112 -0
- package/dist/render.js.map +1 -0
- package/dist/select.d.ts +43 -0
- package/dist/select.d.ts.map +1 -0
- package/dist/select.js +106 -0
- package/dist/select.js.map +1 -0
- package/dist/styles.css +158 -0
- package/dist/types.d.ts +69 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/validate.d.ts +53 -0
- package/dist/validate.d.ts.map +1 -0
- package/dist/validate.js +111 -0
- package/dist/validate.js.map +1 -0
- package/embed/README.md +98 -0
- package/embed/iife.js +17 -0
- package/package.json +40 -4
- package/README.md +0 -3
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { ConsentProvider } from './consent';
|
|
2
|
+
declare global {
|
|
3
|
+
interface Window {
|
|
4
|
+
dataLayer?: unknown[];
|
|
5
|
+
gtag?: (...args: unknown[]) => void;
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
export interface PromoEventPayload {
|
|
9
|
+
promotionId: string;
|
|
10
|
+
promotionName: string;
|
|
11
|
+
creativeName: string;
|
|
12
|
+
creativeSlot: string;
|
|
13
|
+
locationId: string;
|
|
14
|
+
configVersion: number;
|
|
15
|
+
}
|
|
16
|
+
export interface CrossPromoAnalyticsOptions {
|
|
17
|
+
/** Network GA4 measurement id; null disables measurement (render still works). */
|
|
18
|
+
measurementId: string | null;
|
|
19
|
+
consent?: ConsentProvider;
|
|
20
|
+
excludedPathPrefixes?: string[];
|
|
21
|
+
/**
|
|
22
|
+
* Location source, injectable for tests and non-DOM runtimes. Defaults to
|
|
23
|
+
* window.location when present.
|
|
24
|
+
*/
|
|
25
|
+
getLocation?: () => {
|
|
26
|
+
hostname: string;
|
|
27
|
+
pathname: string;
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Ensure a gtag runtime exists for the network measurement id. Reuses the
|
|
32
|
+
* host's dataLayer/gtag when present (gtag.js routes by config id), and only
|
|
33
|
+
* injects a loader when this id has none — so a host's own analytics setup is
|
|
34
|
+
* never altered.
|
|
35
|
+
*/
|
|
36
|
+
export declare function ensureNetworkGtag(measurementId: string | null): boolean;
|
|
37
|
+
export declare function isLocalHostname(hostname: string): boolean;
|
|
38
|
+
export declare function shouldSkipPromoAnalytics(options: Pick<CrossPromoAnalyticsOptions, 'excludedPathPrefixes'>, hostname: string, pathname: string): boolean;
|
|
39
|
+
/** GA4 promotion impression (fires at most once per slot per page view). */
|
|
40
|
+
export declare function trackPromoView(options: CrossPromoAnalyticsOptions, payload: PromoEventPayload): boolean;
|
|
41
|
+
/**
|
|
42
|
+
* GA4 promotion click. Fired synchronously on click; gtag.js delivers queued
|
|
43
|
+
* events via sendBeacon-class transport that survives the outbound navigation.
|
|
44
|
+
*/
|
|
45
|
+
export declare function trackPromoSelect(options: CrossPromoAnalyticsOptions, payload: PromoEventPayload): boolean;
|
|
46
|
+
export declare function defaultAnalyticsOptions(measurementId: string | null): CrossPromoAnalyticsOptions;
|
|
47
|
+
//# sourceMappingURL=analytics.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"analytics.d.ts","sourceRoot":"","sources":["../src/analytics.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,WAAW,CAAC;AAGjD,OAAO,CAAC,MAAM,CAAC;IACb,UAAU,MAAM;QACd,SAAS,CAAC,EAAE,OAAO,EAAE,CAAC;QACtB,IAAI,CAAC,EAAE,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,IAAI,CAAC;KACrC;CACF;AAID,MAAM,WAAW,iBAAiB;IAChC,WAAW,EAAE,MAAM,CAAC;IACpB,aAAa,EAAE,MAAM,CAAC;IACtB,YAAY,EAAE,MAAM,CAAC;IACrB,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,MAAM,CAAC;IACnB,aAAa,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,0BAA0B;IACzC,kFAAkF;IAClF,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,OAAO,CAAC,EAAE,eAAe,CAAC;IAC1B,oBAAoB,CAAC,EAAE,MAAM,EAAE,CAAC;IAChC;;;OAGG;IACH,WAAW,CAAC,EAAE,MAAM;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC;CAC5D;AAiBD;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,aAAa,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO,CAuBvE;AAED,wBAAgB,eAAe,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAEzD;AAED,wBAAgB,wBAAwB,CACtC,OAAO,EAAE,IAAI,CAAC,0BAA0B,EAAE,sBAAsB,CAAC,EACjE,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM,GACf,OAAO,CAKT;AAgCD,4EAA4E;AAC5E,wBAAgB,cAAc,CAC5B,OAAO,EAAE,0BAA0B,EACnC,OAAO,EAAE,iBAAiB,GACzB,OAAO,CAET;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAC9B,OAAO,EAAE,0BAA0B,EACnC,OAAO,EAAE,iBAAiB,GACzB,OAAO,CAET;AAED,wBAAgB,uBAAuB,CACrC,aAAa,EAAE,MAAM,GAAG,IAAI,GAC3B,0BAA0B,CAE5B"}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { permissiveConsent } from './consent';
|
|
2
|
+
const NETWORK_GROUP = 'parchar-cross-promo';
|
|
3
|
+
function currentLocation(options) {
|
|
4
|
+
if (options.getLocation)
|
|
5
|
+
return options.getLocation();
|
|
6
|
+
if (typeof window !== 'undefined' && window.location) {
|
|
7
|
+
return { hostname: window.location.hostname, pathname: window.location.pathname };
|
|
8
|
+
}
|
|
9
|
+
return { hostname: '', pathname: '/' };
|
|
10
|
+
}
|
|
11
|
+
function gtagLoaderPresent(measurementId) {
|
|
12
|
+
if (typeof document === 'undefined')
|
|
13
|
+
return true;
|
|
14
|
+
return Boolean(document.querySelector(`script[src*="${measurementId}"]`));
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Ensure a gtag runtime exists for the network measurement id. Reuses the
|
|
18
|
+
* host's dataLayer/gtag when present (gtag.js routes by config id), and only
|
|
19
|
+
* injects a loader when this id has none — so a host's own analytics setup is
|
|
20
|
+
* never altered.
|
|
21
|
+
*/
|
|
22
|
+
export function ensureNetworkGtag(measurementId) {
|
|
23
|
+
if (typeof window === 'undefined')
|
|
24
|
+
return false;
|
|
25
|
+
if (!measurementId)
|
|
26
|
+
return false;
|
|
27
|
+
window.dataLayer = window.dataLayer ?? [];
|
|
28
|
+
window.gtag =
|
|
29
|
+
window.gtag ??
|
|
30
|
+
((...args) => {
|
|
31
|
+
window.dataLayer?.push(args);
|
|
32
|
+
});
|
|
33
|
+
if (!gtagLoaderPresent(measurementId)) {
|
|
34
|
+
const script = document.createElement('script');
|
|
35
|
+
script.async = true;
|
|
36
|
+
script.src = `https://www.googletagmanager.com/gtag/js?id=${measurementId}`;
|
|
37
|
+
document.head.appendChild(script);
|
|
38
|
+
}
|
|
39
|
+
window.gtag('config', measurementId, {
|
|
40
|
+
send_page_view: false,
|
|
41
|
+
groups: NETWORK_GROUP,
|
|
42
|
+
});
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
export function isLocalHostname(hostname) {
|
|
46
|
+
return hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '[::1]';
|
|
47
|
+
}
|
|
48
|
+
export function shouldSkipPromoAnalytics(options, hostname, pathname) {
|
|
49
|
+
if (isLocalHostname(hostname))
|
|
50
|
+
return true;
|
|
51
|
+
return (options.excludedPathPrefixes ?? []).some((prefix) => pathname === prefix || pathname.startsWith(`${prefix}/`));
|
|
52
|
+
}
|
|
53
|
+
function eventParams(payload) {
|
|
54
|
+
return {
|
|
55
|
+
promotion_id: payload.promotionId,
|
|
56
|
+
promotion_name: payload.promotionName,
|
|
57
|
+
creative_name: payload.creativeName,
|
|
58
|
+
creative_slot: payload.creativeSlot,
|
|
59
|
+
location_id: payload.locationId,
|
|
60
|
+
cp_config_version: payload.configVersion,
|
|
61
|
+
send_to: NETWORK_GROUP,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
function emit(options, name, payload) {
|
|
65
|
+
if (typeof window === 'undefined')
|
|
66
|
+
return false;
|
|
67
|
+
if (!options.measurementId)
|
|
68
|
+
return false;
|
|
69
|
+
if (options.consent?.() === 'denied')
|
|
70
|
+
return false;
|
|
71
|
+
const location = currentLocation(options);
|
|
72
|
+
if (shouldSkipPromoAnalytics(options, location.hostname, location.pathname)) {
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
ensureNetworkGtag(options.measurementId);
|
|
76
|
+
window.gtag?.('event', name, eventParams(payload));
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
79
|
+
/** GA4 promotion impression (fires at most once per slot per page view). */
|
|
80
|
+
export function trackPromoView(options, payload) {
|
|
81
|
+
return emit(options, 'view_promotion', payload);
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* GA4 promotion click. Fired synchronously on click; gtag.js delivers queued
|
|
85
|
+
* events via sendBeacon-class transport that survives the outbound navigation.
|
|
86
|
+
*/
|
|
87
|
+
export function trackPromoSelect(options, payload) {
|
|
88
|
+
return emit(options, 'select_promotion', payload);
|
|
89
|
+
}
|
|
90
|
+
export function defaultAnalyticsOptions(measurementId) {
|
|
91
|
+
return { measurementId, consent: permissiveConsent };
|
|
92
|
+
}
|
|
93
|
+
//# sourceMappingURL=analytics.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"analytics.js","sourceRoot":"","sources":["../src/analytics.ts"],"names":[],"mappings":"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"}
|
package/dist/cap.d.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { NetworkConfig, PromotionSelection } from './types';
|
|
2
|
+
export interface CapDayState {
|
|
3
|
+
day: string;
|
|
4
|
+
count: number;
|
|
5
|
+
lastShown: number;
|
|
6
|
+
}
|
|
7
|
+
export type CapState = Record<string, CapDayState>;
|
|
8
|
+
export declare function readCapState(): CapState;
|
|
9
|
+
export declare function writeCapState(state: CapState): void;
|
|
10
|
+
export declare function isCapped(state: CapState, siteId: string, day: string, capPerDay: number): boolean;
|
|
11
|
+
export declare function recordImpression(state: CapState, siteId: string, day: string, now?: number): CapState;
|
|
12
|
+
export interface PickWithCapInput {
|
|
13
|
+
config: NetworkConfig;
|
|
14
|
+
hostSiteId: string;
|
|
15
|
+
slotId: string;
|
|
16
|
+
timeBucket: string;
|
|
17
|
+
locale: string;
|
|
18
|
+
capState: CapState;
|
|
19
|
+
seedExtra?: string;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Client-side cap-aware selection: the deterministic server pick first; when
|
|
23
|
+
* it is capped for this visitor, pick among uncapped alternatives with the
|
|
24
|
+
* same weighted mechanism; when everything is capped, fall back to the
|
|
25
|
+
* least-recently-shown destination so the slot still renders.
|
|
26
|
+
*/
|
|
27
|
+
export declare function pickPromotionWithCap(input: PickWithCapInput): PromotionSelection | null;
|
|
28
|
+
//# sourceMappingURL=cap.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cap.d.ts","sourceRoot":"","sources":["../src/cap.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,kBAAkB,EAAE,MAAM,SAAS,CAAC;AAWjE,MAAM,WAAW,WAAW;IAC1B,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,MAAM,QAAQ,GAAG,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC;AAoBnD,wBAAgB,YAAY,IAAI,QAAQ,CAYvC;AAED,wBAAgB,aAAa,CAAC,KAAK,EAAE,QAAQ,GAAG,IAAI,CAQnD;AAED,wBAAgB,QAAQ,CACtB,KAAK,EAAE,QAAQ,EACf,MAAM,EAAE,MAAM,EACd,GAAG,EAAE,MAAM,EACX,SAAS,EAAE,MAAM,GAChB,OAAO,CAGT;AAED,wBAAgB,gBAAgB,CAC9B,KAAK,EAAE,QAAQ,EACf,MAAM,EAAE,MAAM,EACd,GAAG,EAAE,MAAM,EACX,GAAG,GAAE,MAAmB,GACvB,QAAQ,CASV;AAeD,MAAM,WAAW,gBAAgB;IAC/B,MAAM,EAAE,aAAa,CAAC;IACtB,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,QAAQ,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,gBAAgB,GAAG,kBAAkB,GAAG,IAAI,CAiDvF"}
|
package/dist/cap.js
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { hashString, mulberry32, pickCreativeForLocale, pickPromotion, promotionCandidates, } from './select';
|
|
2
|
+
const STORAGE_KEY = 'parchar-cross-promo-cap-v1';
|
|
3
|
+
function storage() {
|
|
4
|
+
try {
|
|
5
|
+
const candidate = globalThis.localStorage;
|
|
6
|
+
if (!candidate)
|
|
7
|
+
return null;
|
|
8
|
+
const probe = '__parchar_cp_probe__';
|
|
9
|
+
candidate.setItem(probe, probe);
|
|
10
|
+
candidate.removeItem(probe);
|
|
11
|
+
return candidate;
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
export function readCapState() {
|
|
18
|
+
const store = storage();
|
|
19
|
+
if (!store)
|
|
20
|
+
return {};
|
|
21
|
+
try {
|
|
22
|
+
const raw = store.getItem(STORAGE_KEY);
|
|
23
|
+
if (!raw)
|
|
24
|
+
return {};
|
|
25
|
+
const parsed = JSON.parse(raw);
|
|
26
|
+
if (typeof parsed !== 'object' || parsed === null)
|
|
27
|
+
return {};
|
|
28
|
+
return parsed;
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return {};
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
export function writeCapState(state) {
|
|
35
|
+
const store = storage();
|
|
36
|
+
if (!store)
|
|
37
|
+
return;
|
|
38
|
+
try {
|
|
39
|
+
store.setItem(STORAGE_KEY, JSON.stringify(state));
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
// Private-mode quota failures must never break rendering.
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
export function isCapped(state, siteId, day, capPerDay) {
|
|
46
|
+
const entry = state[siteId];
|
|
47
|
+
return entry?.day === day && entry.count >= capPerDay;
|
|
48
|
+
}
|
|
49
|
+
export function recordImpression(state, siteId, day, now = Date.now()) {
|
|
50
|
+
const entry = state[siteId];
|
|
51
|
+
const next = { ...state };
|
|
52
|
+
if (entry?.day === day) {
|
|
53
|
+
next[siteId] = { day, count: entry.count + 1, lastShown: now };
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
next[siteId] = { day, count: 1, lastShown: now };
|
|
57
|
+
}
|
|
58
|
+
return next;
|
|
59
|
+
}
|
|
60
|
+
function leastRecentlyShown(state, siteIds, day) {
|
|
61
|
+
const ordered = [...siteIds].sort((a, b) => {
|
|
62
|
+
const entryA = state[a]?.day === day ? state[a].lastShown : 0;
|
|
63
|
+
const entryB = state[b]?.day === day ? state[b].lastShown : 0;
|
|
64
|
+
return entryA - entryB;
|
|
65
|
+
});
|
|
66
|
+
return ordered[0] ?? siteIds[0] ?? '';
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Client-side cap-aware selection: the deterministic server pick first; when
|
|
70
|
+
* it is capped for this visitor, pick among uncapped alternatives with the
|
|
71
|
+
* same weighted mechanism; when everything is capped, fall back to the
|
|
72
|
+
* least-recently-shown destination so the slot still renders.
|
|
73
|
+
*/
|
|
74
|
+
export function pickPromotionWithCap(input) {
|
|
75
|
+
const { config, hostSiteId, slotId, timeBucket, locale, capState, seedExtra } = input;
|
|
76
|
+
const candidates = promotionCandidates(config, hostSiteId);
|
|
77
|
+
if (candidates.siteIds.length === 0)
|
|
78
|
+
return null;
|
|
79
|
+
// Primary pick mirrors the server exactly (no extra entropy) so hydration
|
|
80
|
+
// agrees with SSR HTML; seedExtra only feeds the cap-substitution path.
|
|
81
|
+
const primary = pickPromotion({
|
|
82
|
+
config,
|
|
83
|
+
hostSiteId,
|
|
84
|
+
slotId,
|
|
85
|
+
timeBucket,
|
|
86
|
+
locale,
|
|
87
|
+
});
|
|
88
|
+
if (primary && !isCapped(capState, primary.siteId, timeBucket, config.capPerDay)) {
|
|
89
|
+
return primary;
|
|
90
|
+
}
|
|
91
|
+
const uncapped = candidates.siteIds.filter((siteId) => !isCapped(capState, siteId, timeBucket, config.capPerDay));
|
|
92
|
+
if (uncapped.length === 0) {
|
|
93
|
+
const fallbackSite = leastRecentlyShown(capState, candidates.siteIds, timeBucket);
|
|
94
|
+
const creativeId = pickCreativeForLocale(candidates.creatives.get(fallbackSite) ?? [], config, locale);
|
|
95
|
+
return creativeId ? { siteId: fallbackSite, creativeId } : null;
|
|
96
|
+
}
|
|
97
|
+
const rng = mulberry32(hashString(`${config.version}|${slotId}|${timeBucket}|cap|${seedExtra ?? ''}`));
|
|
98
|
+
const weights = new Map(uncapped.map((siteId) => [siteId, candidates.weights.get(siteId) ?? 0]));
|
|
99
|
+
const total = uncapped.reduce((sum, siteId) => sum + (weights.get(siteId) ?? 0), 0);
|
|
100
|
+
let cursor = rng() * total;
|
|
101
|
+
let chosen = uncapped[uncapped.length - 1] ?? uncapped[0] ?? '';
|
|
102
|
+
for (const siteId of uncapped) {
|
|
103
|
+
cursor -= weights.get(siteId) ?? 0;
|
|
104
|
+
if (cursor < 0) {
|
|
105
|
+
chosen = siteId;
|
|
106
|
+
break;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
const creativeId = pickCreativeForLocale(candidates.creatives.get(chosen) ?? [], config, locale);
|
|
110
|
+
return creativeId ? { siteId: chosen, creativeId } : null;
|
|
111
|
+
}
|
|
112
|
+
//# sourceMappingURL=cap.js.map
|
package/dist/cap.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cap.js","sourceRoot":"","sources":["../src/cap.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 1,
|
|
3
|
+
"medium": "cross-promo",
|
|
4
|
+
"campaign": "parchar-network",
|
|
5
|
+
"defaultLocale": "es",
|
|
6
|
+
"capPerDay": 3,
|
|
7
|
+
"sites": [
|
|
8
|
+
{
|
|
9
|
+
"id": "becas-parchar-ai",
|
|
10
|
+
"name": "becas.parchar.ai",
|
|
11
|
+
"url": "https://becas.parchar.ai/",
|
|
12
|
+
"roles": ["display", "destination"],
|
|
13
|
+
"weight": 35,
|
|
14
|
+
"placements": [
|
|
15
|
+
{ "id": "home-band", "format": "band" },
|
|
16
|
+
{ "id": "explorar-in-feed", "format": "card" },
|
|
17
|
+
{ "id": "beca-detail-mid", "format": "band" },
|
|
18
|
+
{ "id": "pre-footer", "format": "strip" }
|
|
19
|
+
],
|
|
20
|
+
"creatives": ["becas-evergreen-es", "becas-evergreen-en"]
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
"id": "parchar-ai",
|
|
24
|
+
"name": "parchar.ai",
|
|
25
|
+
"url": "https://parchar.ai/",
|
|
26
|
+
"roles": ["display", "destination"],
|
|
27
|
+
"weight": 35,
|
|
28
|
+
"placements": [
|
|
29
|
+
{ "id": "home-band", "format": "band" },
|
|
30
|
+
{ "id": "explorar-in-feed", "format": "card" },
|
|
31
|
+
{ "id": "event-detail-mid", "format": "band" },
|
|
32
|
+
{ "id": "pre-footer", "format": "strip" }
|
|
33
|
+
],
|
|
34
|
+
"creatives": ["eventos-evergreen-es", "eventos-evergreen-en"]
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
"id": "medium-horacio",
|
|
38
|
+
"name": "Medium @horacioreyesrios",
|
|
39
|
+
"url": "https://medium.com/@horacioreyesrios",
|
|
40
|
+
"roles": ["destination"],
|
|
41
|
+
"weight": 10,
|
|
42
|
+
"placements": [],
|
|
43
|
+
"creatives": ["medium-evergreen-es", "medium-evergreen-en"]
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
"id": "soypolkadot-com",
|
|
47
|
+
"name": "soypolkadot.com",
|
|
48
|
+
"url": "https://soypolkadot.com/",
|
|
49
|
+
"roles": ["display", "destination"],
|
|
50
|
+
"weight": 5,
|
|
51
|
+
"placements": [
|
|
52
|
+
{ "id": "home-band", "format": "band" },
|
|
53
|
+
{ "id": "end-of-guide", "format": "card" }
|
|
54
|
+
],
|
|
55
|
+
"creatives": ["polkadot-evergreen-es", "polkadot-evergreen-en"]
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
"id": "artmax-club",
|
|
59
|
+
"name": "artmax.club",
|
|
60
|
+
"url": "https://artmax.club/",
|
|
61
|
+
"roles": ["display", "destination"],
|
|
62
|
+
"weight": 5,
|
|
63
|
+
"placements": [
|
|
64
|
+
{ "id": "home-band", "format": "band" },
|
|
65
|
+
{ "id": "end-of-page", "format": "card" }
|
|
66
|
+
],
|
|
67
|
+
"creatives": ["artmax-evergreen-es", "artmax-evergreen-en"]
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
"id": "ganima-co",
|
|
71
|
+
"name": "ganima.co",
|
|
72
|
+
"url": "https://ganima.co/",
|
|
73
|
+
"roles": ["destination"],
|
|
74
|
+
"weight": 5,
|
|
75
|
+
"placements": [],
|
|
76
|
+
"creatives": ["ganima-evergreen-es", "ganima-evergreen-en"]
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
"id": "instagram-ganima-bug",
|
|
80
|
+
"name": "Instagram @ganima_bug",
|
|
81
|
+
"url": "https://www.instagram.com/ganima_bug/",
|
|
82
|
+
"roles": ["destination"],
|
|
83
|
+
"weight": 5,
|
|
84
|
+
"placements": [],
|
|
85
|
+
"creatives": ["instagram-evergreen-es", "instagram-evergreen-en"]
|
|
86
|
+
}
|
|
87
|
+
],
|
|
88
|
+
"creatives": [
|
|
89
|
+
{
|
|
90
|
+
"id": "becas-evergreen-es",
|
|
91
|
+
"destination": "becas-parchar-ai",
|
|
92
|
+
"variant": "becas-evergreen-es",
|
|
93
|
+
"locale": "es",
|
|
94
|
+
"headline": "Becas verificadas, sin rumores",
|
|
95
|
+
"blurb": "Oportunidades con fuente oficial, fechas de cierre y evidencia en cada ficha.",
|
|
96
|
+
"cta": "Explorar becas",
|
|
97
|
+
"accent": "#e63a7a"
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
"id": "becas-evergreen-en",
|
|
101
|
+
"destination": "becas-parchar-ai",
|
|
102
|
+
"variant": "becas-evergreen-en",
|
|
103
|
+
"locale": "en",
|
|
104
|
+
"headline": "Verified scholarships, no rumors",
|
|
105
|
+
"blurb": "Opportunities with official sources, closing dates and evidence on every listing.",
|
|
106
|
+
"cta": "Browse scholarships",
|
|
107
|
+
"accent": "#e63a7a"
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
"id": "eventos-evergreen-es",
|
|
111
|
+
"destination": "parchar-ai",
|
|
112
|
+
"variant": "eventos-evergreen-es",
|
|
113
|
+
"locale": "es",
|
|
114
|
+
"headline": "¿Y el parche de este fin de semana?",
|
|
115
|
+
"blurb": "Conciertos, teatro y expos verificados en tu ciudad, agregados con IA.",
|
|
116
|
+
"cta": "Ver planes",
|
|
117
|
+
"accent": "#f6c24a"
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
"id": "eventos-evergreen-en",
|
|
121
|
+
"destination": "parchar-ai",
|
|
122
|
+
"variant": "eventos-evergreen-en",
|
|
123
|
+
"locale": "en",
|
|
124
|
+
"headline": "Weekend plans, already verified",
|
|
125
|
+
"blurb": "Concerts, theater and exhibitions in your city, aggregated with AI.",
|
|
126
|
+
"cta": "Browse events",
|
|
127
|
+
"accent": "#f6c24a"
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
"id": "medium-evergreen-es",
|
|
131
|
+
"destination": "medium-horacio",
|
|
132
|
+
"variant": "medium-evergreen-es",
|
|
133
|
+
"locale": "es",
|
|
134
|
+
"headline": "Cómo se construye Parchar",
|
|
135
|
+
"blurb": "Ensayos del fundador sobre datos abiertos, IA y descubrimiento cultural.",
|
|
136
|
+
"cta": "Leer en Medium",
|
|
137
|
+
"accent": "#10b981"
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
"id": "medium-evergreen-en",
|
|
141
|
+
"destination": "medium-horacio",
|
|
142
|
+
"variant": "medium-evergreen-en",
|
|
143
|
+
"locale": "en",
|
|
144
|
+
"headline": "How Parchar gets built",
|
|
145
|
+
"blurb": "Founder essays on open data, AI and cultural discovery.",
|
|
146
|
+
"cta": "Read on Medium",
|
|
147
|
+
"accent": "#10b981"
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
"id": "polkadot-evergreen-es",
|
|
151
|
+
"destination": "soypolkadot-com",
|
|
152
|
+
"variant": "polkadot-evergreen-es",
|
|
153
|
+
"locale": "es",
|
|
154
|
+
"headline": "Polkadot, en buen español",
|
|
155
|
+
"blurb": "Guías revisadas con criterios editoriales abiertos, sin humo.",
|
|
156
|
+
"cta": "Leer las guías",
|
|
157
|
+
"accent": "#e6007a"
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
"id": "polkadot-evergreen-en",
|
|
161
|
+
"destination": "soypolkadot-com",
|
|
162
|
+
"variant": "polkadot-evergreen-en",
|
|
163
|
+
"locale": "en",
|
|
164
|
+
"headline": "Polkadot, carefully explained",
|
|
165
|
+
"blurb": "Reviewed guides with open editorial criteria and no hype.",
|
|
166
|
+
"cta": "Read the guides",
|
|
167
|
+
"accent": "#e6007a"
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
"id": "artmax-evergreen-es",
|
|
171
|
+
"destination": "artmax-club",
|
|
172
|
+
"variant": "artmax-evergreen-es",
|
|
173
|
+
"locale": "es",
|
|
174
|
+
"headline": "Arte con mensaje",
|
|
175
|
+
"blurb": "Colecciones NFT que amplifican voces sociales del mundo real.",
|
|
176
|
+
"cta": "Ver colecciones",
|
|
177
|
+
"accent": "#8b5cf6"
|
|
178
|
+
},
|
|
179
|
+
{
|
|
180
|
+
"id": "artmax-evergreen-en",
|
|
181
|
+
"destination": "artmax-club",
|
|
182
|
+
"variant": "artmax-evergreen-en",
|
|
183
|
+
"locale": "en",
|
|
184
|
+
"headline": "Art with a message",
|
|
185
|
+
"blurb": "NFT collections that amplify real-world social voices.",
|
|
186
|
+
"cta": "View collections",
|
|
187
|
+
"accent": "#8b5cf6"
|
|
188
|
+
},
|
|
189
|
+
{
|
|
190
|
+
"id": "ganima-evergreen-es",
|
|
191
|
+
"destination": "ganima-co",
|
|
192
|
+
"variant": "ganima-evergreen-es",
|
|
193
|
+
"locale": "es",
|
|
194
|
+
"headline": "Cuadro a cuadro",
|
|
195
|
+
"blurb": "Estudio bogotano de animación 2D y stop-motion en desarrollo.",
|
|
196
|
+
"cta": "Conocer GANIMA",
|
|
197
|
+
"accent": "#f97316"
|
|
198
|
+
},
|
|
199
|
+
{
|
|
200
|
+
"id": "ganima-evergreen-en",
|
|
201
|
+
"destination": "ganima-co",
|
|
202
|
+
"variant": "ganima-evergreen-en",
|
|
203
|
+
"locale": "en",
|
|
204
|
+
"headline": "Frame by frame",
|
|
205
|
+
"blurb": "A Bogotá-based 2D and stop-motion animation studio in development.",
|
|
206
|
+
"cta": "Meet GANIMA",
|
|
207
|
+
"accent": "#f97316"
|
|
208
|
+
},
|
|
209
|
+
{
|
|
210
|
+
"id": "instagram-evergreen-es",
|
|
211
|
+
"destination": "instagram-ganima-bug",
|
|
212
|
+
"variant": "instagram-evergreen-es",
|
|
213
|
+
"locale": "es",
|
|
214
|
+
"headline": "El proceso, sin filtro",
|
|
215
|
+
"blurb": "El detrás de cámaras del estudio GANIMA, directo al feed.",
|
|
216
|
+
"cta": "Seguir a @ganima_bug",
|
|
217
|
+
"accent": "#ec4899"
|
|
218
|
+
},
|
|
219
|
+
{
|
|
220
|
+
"id": "instagram-evergreen-en",
|
|
221
|
+
"destination": "instagram-ganima-bug",
|
|
222
|
+
"variant": "instagram-evergreen-en",
|
|
223
|
+
"locale": "en",
|
|
224
|
+
"headline": "The process, unfiltered",
|
|
225
|
+
"blurb": "GANIMA studio's behind-the-scenes, straight to the feed.",
|
|
226
|
+
"cta": "Follow @ganima_bug",
|
|
227
|
+
"accent": "#ec4899"
|
|
228
|
+
}
|
|
229
|
+
]
|
|
230
|
+
}
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAK7C,eAAO,MAAM,aAAa,EAAkB,aAAa,CAAC;AAE1D,YAAY,EAAE,aAAa,EAAE,CAAC;AAC9B,mBAAmB,SAAS,CAAC"}
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import networkJson from './config/network.json';
|
|
2
|
+
// The shipped config is structurally validated by the package test suite and
|
|
3
|
+
// by consumers at build time (validateNetworkConfig). Casting here keeps the
|
|
4
|
+
// runtime — and the embed bundle — free of the schema dependency.
|
|
5
|
+
export const networkConfig = networkJson;
|
|
6
|
+
//# sourceMappingURL=config.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA,OAAO,WAAW,MAAM,uBAAuB,CAAC;AAGhD,6EAA6E;AAC7E,6EAA6E;AAC7E,kEAAkE;AAClE,MAAM,CAAC,MAAM,aAAa,GAAG,WAA4B,CAAC"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export type PromoConsent = 'granted' | 'denied';
|
|
2
|
+
export type ConsentProvider = () => PromoConsent;
|
|
3
|
+
/**
|
|
4
|
+
* First-party portals where GA already loads: promos may measure by default.
|
|
5
|
+
*/
|
|
6
|
+
export declare const permissiveConsent: ConsentProvider;
|
|
7
|
+
/**
|
|
8
|
+
* Hosts with an analytics consent gate (e.g. soypolkadot's cookie
|
|
9
|
+
* preferences). `detect` returns the host's analytics-consent state; when it
|
|
10
|
+
* cannot be read, consent is denied so no measurement request is sent.
|
|
11
|
+
*/
|
|
12
|
+
export declare function gatedConsent(detect: () => boolean | string | undefined): ConsentProvider;
|
|
13
|
+
//# sourceMappingURL=consent.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"consent.d.ts","sourceRoot":"","sources":["../src/consent.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,YAAY,GAAG,SAAS,GAAG,QAAQ,CAAC;AAEhD,MAAM,MAAM,eAAe,GAAG,MAAM,YAAY,CAAC;AAEjD;;GAEG;AACH,eAAO,MAAM,iBAAiB,EAAE,eAAiC,CAAC;AAElE;;;;GAIG;AACH,wBAAgB,YAAY,CAC1B,MAAM,EAAE,MAAM,OAAO,GAAG,MAAM,GAAG,SAAS,GACzC,eAAe,CASjB"}
|
package/dist/consent.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* First-party portals where GA already loads: promos may measure by default.
|
|
3
|
+
*/
|
|
4
|
+
export const permissiveConsent = () => 'granted';
|
|
5
|
+
/**
|
|
6
|
+
* Hosts with an analytics consent gate (e.g. soypolkadot's cookie
|
|
7
|
+
* preferences). `detect` returns the host's analytics-consent state; when it
|
|
8
|
+
* cannot be read, consent is denied so no measurement request is sent.
|
|
9
|
+
*/
|
|
10
|
+
export function gatedConsent(detect) {
|
|
11
|
+
return () => {
|
|
12
|
+
try {
|
|
13
|
+
const value = detect();
|
|
14
|
+
return value === true || value === 'granted' ? 'granted' : 'denied';
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return 'denied';
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
//# sourceMappingURL=consent.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"consent.js","sourceRoot":"","sources":["../src/consent.ts"],"names":[],"mappings":"AAIA;;GAEG;AACH,MAAM,CAAC,MAAM,iBAAiB,GAAoB,GAAG,EAAE,CAAC,SAAS,CAAC;AAElE;;;;GAIG;AACH,MAAM,UAAU,YAAY,CAC1B,MAA0C;IAE1C,OAAO,GAAG,EAAE;QACV,IAAI,CAAC;YACH,MAAM,KAAK,GAAG,MAAM,EAAE,CAAC;YACvB,OAAO,KAAK,KAAK,IAAI,IAAI,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,QAAQ,CAAC;QACtE,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,QAAQ,CAAC;QAClB,CAAC;IACH,CAAC,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { NetworkConfig } from './types';
|
|
2
|
+
import { type CrossPromoAnalyticsOptions } from './analytics';
|
|
3
|
+
import { type ConsentProvider } from './consent';
|
|
4
|
+
export interface DefineParcharPromoOptions {
|
|
5
|
+
config?: NetworkConfig;
|
|
6
|
+
/** Network GA4 measurement id; null keeps rendering but skips analytics. */
|
|
7
|
+
measurementId?: string | null;
|
|
8
|
+
consent?: ConsentProvider;
|
|
9
|
+
excludedPathPrefixes?: string[];
|
|
10
|
+
/** Extra analytics options (tests inject a location provider here). */
|
|
11
|
+
analyticsOptions?: Partial<CrossPromoAnalyticsOptions>;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Registers the `<parchar-promo>` element for embed hosts (static sites).
|
|
15
|
+
* Options also become the live configuration for in-place enhancement.
|
|
16
|
+
*/
|
|
17
|
+
export declare function defineParcharPromo(options?: DefineParcharPromoOptions): void;
|
|
18
|
+
/**
|
|
19
|
+
* In-place enhancement for server-rendered slots: the SvelteKit apps render a
|
|
20
|
+
* placeholder div with deterministic SSR markup; this applies the visitor's
|
|
21
|
+
* cap state (substituting the creative when the server pick is capped) and
|
|
22
|
+
* wires GA4 view/click events without disturbing Svelte's ownership of the
|
|
23
|
+
* surrounding tree. Idempotent; safe to call after every client navigation.
|
|
24
|
+
*/
|
|
25
|
+
export declare function enhanceCrossPromoPlaceholders(root?: ParentNode): void;
|
|
26
|
+
//# sourceMappingURL=element.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"element.d.ts","sourceRoot":"","sources":["../src/element.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAoB,aAAa,EAAE,MAAM,SAAS,CAAC;AAK/D,OAAO,EAGL,KAAK,0BAA0B,EAChC,MAAM,aAAa,CAAC;AACrB,OAAO,EAAqB,KAAK,eAAe,EAAE,MAAM,WAAW,CAAC;AAEpE,MAAM,WAAW,yBAAyB;IACxC,MAAM,CAAC,EAAE,aAAa,CAAC;IACvB,4EAA4E;IAC5E,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,OAAO,CAAC,EAAE,eAAe,CAAC;IAC1B,oBAAoB,CAAC,EAAE,MAAM,EAAE,CAAC;IAChC,uEAAuE;IACvE,gBAAgB,CAAC,EAAE,OAAO,CAAC,0BAA0B,CAAC,CAAC;CACxD;AAiHD;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,GAAE,yBAA8B,GAAG,IAAI,CAiBhF;AAID;;;;;;GAMG;AACH,wBAAgB,6BAA6B,CAAC,IAAI,GAAE,UAAqB,GAAG,IAAI,CA6B/E"}
|