@konvert7/promoot 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/README.md ADDED
@@ -0,0 +1,78 @@
1
+ # @konvert7/promoot
2
+
3
+ Render a [Promoot](https://promootlabs.com) sponsor slot inline, as part of your own
4
+ markup, instead of in an iframe.
5
+
6
+ ```bash
7
+ bun add @konvert7/promoot
8
+ ```
9
+
10
+ ```tsx
11
+ import { PromootBlock } from "@konvert7/promoot";
12
+
13
+ <PromootBlock url="https://promootlabs.com/embed/YOUR_SLOT_ID" />;
14
+ ```
15
+
16
+ The `url` is the embed URL from your Promoot dashboard — the same string the iframe
17
+ snippet uses, so one value serves either method.
18
+
19
+ ## Why inline
20
+
21
+ An iframe fails closed. If a content filter ever blocks the frame, your sponsor's ad
22
+ disappears and they get nothing. An inline block renders inside your own document, so the
23
+ worst a filter can do is drop the creative or the view beacon: the ad is still seen.
24
+
25
+ It also participates in your layout. No fixed-height box to defend, no frame to style
26
+ around.
27
+
28
+ ## What it costs you
29
+
30
+ The creative lives in your DOM, so this is a weaker guarantee for sponsors than the
31
+ iframe: nothing stops a site owner swapping or hiding it after approval. If you want that
32
+ guarantee, keep the iframe. Both count views identically.
33
+
34
+ ## Props
35
+
36
+ | Prop | Type | Default | |
37
+ |---|---|---|---|
38
+ | `url` | `string` | — | Required. The dashboard embed URL. |
39
+ | `className` | `string` | — | Applied to the container. |
40
+ | `style` | `CSSProperties` | — | Applied to the container. |
41
+ | `fallback` | `ReactNode` | `null` | Rendered when the slot is paused, empty, or unreachable. |
42
+
43
+ ## Requirements
44
+
45
+ A React Server Components framework — Next.js App Router or equivalent. The block fetches
46
+ on the server and ships **no client JavaScript**; the view is counted by a plain `<img>`
47
+ the visitor's browser loads, which is what keeps the count a first-hand observation rather
48
+ than something your server claims.
49
+
50
+ **The page must render dynamically.** The block fetches with `cache: "no-store"`, but if
51
+ your route is statically generated the fetch runs once at build time and an expired ad
52
+ keeps rendering — the sponsor gets free time and the next buyer never appears. In Next.js:
53
+
54
+ ```ts
55
+ export const dynamic = "force-dynamic";
56
+ // or a short revalidate window
57
+ export const revalidate = 30;
58
+ ```
59
+
60
+ Counting is unaffected by caching, because the beacon is a tag in your HTML that every
61
+ visitor's browser fetches regardless of how that HTML was produced.
62
+
63
+ ## Styling
64
+
65
+ `className` and `style` land on the container, and the internal structure carries
66
+ `data-promoot-*` attributes plus `promoot-*` class names you can target.
67
+
68
+ The block **never declares a colour scheme of its own**. Its palette uses `light-dark()`,
69
+ which resolves against the scheme it inherits from your page — so a light-only site keeps
70
+ a light panel even for a visitor whose OS prefers dark, and a dark site gets a dark one.
71
+ All of its CSS is scoped to the block; nothing is defined on `:root`.
72
+
73
+ The one thing you cannot restyle is the "Sponsored" label, which renders from inline
74
+ styles. Disclosure of a paid placement is not the site owner's to remove.
75
+
76
+ ## License
77
+
78
+ MIT
@@ -0,0 +1,2 @@
1
+ export { PromootBlock, type PromootBlockProps } from "./promoot-block.js";
2
+ export type { BlockPayload, BlockRender } from "./types.js";
package/dist/index.js ADDED
@@ -0,0 +1 @@
1
+ export { PromootBlock } from "./promoot-block.js";
@@ -0,0 +1,15 @@
1
+ import type { CSSProperties, ReactNode } from "react";
2
+ import type { BlockPayload } from "./types.js";
3
+ export type PromootBlockProps = {
4
+ /** The embed URL from your Promoot dashboard, the same one the iframe uses. */
5
+ url: string;
6
+ /** Applied to the block's container. */
7
+ className?: string;
8
+ /** Applied to the block's container. */
9
+ style?: CSSProperties;
10
+ /** Rendered when the slot is paused, empty, or unreachable. */
11
+ fallback?: ReactNode;
12
+ };
13
+ export declare function blockEndpoint(embedUrl: string): string;
14
+ export declare function scopedCss(payload: BlockPayload): string;
15
+ export declare function PromootBlock({ url, className, style, fallback, }: PromootBlockProps): Promise<import("react").JSX.Element>;
@@ -0,0 +1,52 @@
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ export function blockEndpoint(embedUrl) {
3
+ return embedUrl.replace("/embed/", "/api/block/");
4
+ }
5
+ // no-store, because a cached render keeps showing a creative whose run has
6
+ // already ended: the sponsor gets free time and the next buyer never appears.
7
+ async function loadBlock(embedUrl) {
8
+ try {
9
+ const response = await fetch(blockEndpoint(embedUrl), { cache: "no-store" });
10
+ return response.ok ? (await response.json()) : null;
11
+ }
12
+ catch {
13
+ return null;
14
+ }
15
+ }
16
+ // Every rule is scoped to this one block, and it never declares a colour scheme
17
+ // of its own: the palette uses light-dark(), which resolves against the scheme
18
+ // inherited from the host page, so a light-only site keeps a light panel.
19
+ export function scopedCss(payload) {
20
+ const scope = `[data-promoot-slot="${payload.slot.id}"]`;
21
+ return `${scope} { ${payload.paletteVars}; position: relative; display: block; width: 100%; max-width: ${payload.slot.widthPx}px; height: auto; max-height: ${payload.slot.heightPx}px; aspect-ratio: ${payload.slot.widthPx} / ${payload.slot.heightPx}; }
22
+ ${scope} a { text-decoration: none; }
23
+ ${scope} .promoot-ad { display: block; position: relative; height: 100%; width: 100%; }
24
+ ${scope} .promoot-ad img { display: block; height: 100%; width: 100%; object-fit: cover; }
25
+ ${scope} .promoot-cta { display: flex; flex-direction: column; align-items: center; gap: 3px; width: 100%; height: 100%; justify-content: center; padding: 0 10px; text-align: center; border: 1px dashed var(--edge); border-radius: 10px; box-sizing: border-box; color: var(--muted); background: var(--surface); font-family: system-ui, -apple-system, sans-serif; }
26
+ ${scope} .promoot-cta:hover { border-color: var(--edge-hover); background: var(--surface-hover); }
27
+ ${scope} .promoot-cta strong { font-size: 14px; color: var(--ink); }
28
+ ${scope} .promoot-cta .promoot-pitch { font-size: 13px; color: var(--ink); }
29
+ ${scope} .promoot-cta .promoot-proof { font-size: 11px; color: var(--muted); }
30
+ ${scope} .promoot-cta .promoot-terms { font-size: 12px; }
31
+ ${scope} .promoot-beacon { position: absolute; bottom: 0; right: 0; width: 1px; height: 1px; }`;
32
+ }
33
+ // Inline, not a class: a host can restyle everything else, but the disclosure
34
+ // is not theirs to remove.
35
+ const labelStyle = {
36
+ position: "absolute",
37
+ right: "4px",
38
+ bottom: "4px",
39
+ fontSize: "10px",
40
+ lineHeight: "1.4",
41
+ color: "#ffffff",
42
+ background: "rgba(0,0,0,.55)",
43
+ borderRadius: "4px",
44
+ padding: "1px 5px",
45
+ };
46
+ export async function PromootBlock({ url, className, style, fallback = null, }) {
47
+ const block = await loadBlock(url);
48
+ if (!block || block.render === "blank") {
49
+ return _jsx(_Fragment, { children: fallback });
50
+ }
51
+ return (_jsxs("div", { "data-promoot-slot": block.slot.id, className: className, style: style, children: [_jsx("style", { dangerouslySetInnerHTML: { __html: scopedCss(block) } }), block.ad && (_jsxs("a", { className: "promoot-ad", href: block.ad.clickUrl, target: "_blank", rel: "noopener nofollow sponsored", children: [_jsx("img", { src: block.ad.imageUrl, alt: block.ad.title }), block.slot.sponsoredLabel && (_jsx("span", { style: labelStyle, children: block.slot.sponsoredLabel }))] })), block.pitch && (_jsxs("a", { className: "promoot-cta", href: block.pitch.purchaseUrl, target: "_blank", rel: "noopener", children: [_jsx("strong", { children: block.pitch.headline }), block.pitch.pitch && _jsx("span", { className: "promoot-pitch", children: block.pitch.pitch }), block.pitch.proof && _jsx("span", { className: "promoot-proof", children: block.pitch.proof }), _jsx("span", { className: "promoot-terms", children: block.pitch.terms })] })), _jsx("img", { className: "promoot-beacon", src: block.beaconUrl, width: 1, height: 1, alt: "", "aria-hidden": "true" })] }));
52
+ }
@@ -0,0 +1,26 @@
1
+ export type BlockRender = "ad" | "pitch" | "blank";
2
+ /** What GET /api/block/:slotId answers with. */
3
+ export type BlockPayload = {
4
+ slot: {
5
+ id: string;
6
+ widthPx: number;
7
+ heightPx: number;
8
+ sponsoredLabel: string | null;
9
+ };
10
+ render: BlockRender;
11
+ ad: {
12
+ title: string;
13
+ imageUrl: string;
14
+ clickUrl: string;
15
+ expiresAt: string | null;
16
+ } | null;
17
+ pitch: {
18
+ headline: string;
19
+ pitch: string | null;
20
+ proof: string | null;
21
+ terms: string;
22
+ purchaseUrl: string;
23
+ } | null;
24
+ beaconUrl: string;
25
+ paletteVars: string;
26
+ };
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "@konvert7/promoot",
3
+ "version": "0.1.0",
4
+ "description": "Render a Promoot sponsor slot inline as a React server component, with views counted by the visitor's own browser.",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "default": "./dist/index.js"
12
+ }
13
+ },
14
+ "files": ["dist", "README.md"],
15
+ "sideEffects": false,
16
+ "engines": {
17
+ "node": ">=20"
18
+ },
19
+ "peerDependencies": {
20
+ "react": ">=19"
21
+ },
22
+ "keywords": ["promoot", "sponsor", "slot", "react", "rsc", "server-component"],
23
+ "license": "MIT",
24
+ "repository": {
25
+ "type": "git",
26
+ "url": "git+https://github.com/konvert7/promoot.git"
27
+ },
28
+ "scripts": {
29
+ "build": "tsc -p tsconfig.build.json",
30
+ "type-check": "tsc --noEmit",
31
+ "test": "bun test",
32
+ "prepublishOnly": "bun run build"
33
+ },
34
+ "devDependencies": {
35
+ "@types/bun": "1.3.13",
36
+ "@types/react": "19.2.17",
37
+ "@types/react-dom": "19.2.3",
38
+ "react": "19.2.7",
39
+ "react-dom": "19.2.7",
40
+ "typescript": "6.0.3"
41
+ }
42
+ }