Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 96 additions & 0 deletions lib/ads/house.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import crypto from "node:crypto";
import { env } from "@/lib/env";
import { formatSpec, type AdCreative, type AdFormatId } from "./creative";
import type { Fill } from "./serve";

// Default "house" ad for the CrawlProof Ad Network. Shown when a slot has no
// eligible paid campaign, so a publisher's unit is never blank. Uses default
// AI-generated promo artwork (public/ads/house/*) with an overlaid pitch that
// links back to CrawlProof. House ads are not metered or billed.

const HOUSE = {
headline: "Your ad here",
body: "Crypto pay-per-click ads for indie sites — no middlemen.",
cta: "Advertise →",
} as const;

function imageUrlFor(format: AdFormatId): string {
const rect = format === "banner_300x250";
return `${env.siteUrl}/ads/house/${rect ? "promo-rect" : "promo-wide"}.webp`;
}

function esc(s: string): string {
return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
}

export function renderHouseAdHtml(format: AdFormatId, clickUrl: string): string {
const { w, h } = formatSpec(format);
const img = imageUrlFor(format);
const isMobile = format === "banner_320x50";
const isRect = format === "banner_300x250";
const row = !isRect; // leaderboard + mobile are horizontal

const label = `<span style="position:absolute;top:8px;left:10px;z-index:3;font-size:9px;letter-spacing:.14em;text-transform:uppercase;color:#9fb0c3">CrawlProof Ads</span>`;
const headline = `<div style="font-weight:800;font-size:${isMobile ? 13 : isRect ? 20 : 17}px;line-height:1.1;color:#eef3f8">${esc(HOUSE.headline)}</div>`;
const body = isMobile
? ""
: `<div style="font-size:${isRect ? 13 : 12}px;color:#c7d2de;margin-top:4px;max-width:${isRect ? "100%" : "62%"}">${esc(HOUSE.body)}</div>`;
const cta = `<span style="background:#6ee7b7;color:#04121a;font-weight:700;border-radius:6px;padding:${isMobile ? "4px 8px" : "7px 12px"};font-size:${isMobile ? 11 : 13}px;white-space:nowrap">${esc(HOUSE.cta)}</span>`;

const content = row
? `<div style="position:relative;z-index:2;display:flex;align-items:center;gap:10px;height:100%;padding:0 12px">
<div style="min-width:0">${headline}${body}</div>
<div style="margin-left:auto;flex:0 0 auto">${cta}</div>
</div>`
: `<div style="position:relative;z-index:2;display:flex;flex-direction:column;height:100%;padding:14px">
<div style="margin-top:auto">${headline}${body}</div>
<div style="margin-top:12px">${cta}</div>
</div>`;

const overlay = isRect
? "linear-gradient(180deg, rgba(7,10,16,.12) 0%, rgba(7,10,16,.86) 76%)"
: "linear-gradient(90deg, rgba(7,10,16,.92) 0%, rgba(7,10,16,.5) 62%, rgba(7,10,16,.2) 100%)";

return `<!doctype html><html><head><meta charset="utf-8"><style>
*{box-sizing:border-box;margin:0}
a{text-decoration:none;display:block}
.cp-ad{position:relative;width:${w}px;height:${h}px;overflow:hidden;border-radius:8px;
border:1px solid rgba(255,255,255,.08);background:#070a10;
font-family:system-ui,-apple-system,Segoe UI,Roboto,sans-serif}
.cp-ad .bg{position:absolute;inset:0;z-index:0;background:url("${esc(img)}") center/cover no-repeat}
.cp-ad .ov{position:absolute;inset:0;z-index:1;background:${overlay}}
</style></head><body>
<a class="cp-ad" href="${esc(clickUrl)}" target="_blank" rel="noopener sponsored">
<div class="bg"></div><div class="ov"></div>${label}${content}
</a>
</body></html>`;
}

function houseCreative(format: AdFormatId): AdCreative {
return {
format,
headline: HOUSE.headline,
body: HOUSE.body,
ctaText: HOUSE.cta,
bgColor: "#070a10",
fgColor: "#eef3f8",
accentColor: "#6ee7b7",
fontFamily: "system-ui, -apple-system, Segoe UI, Roboto, sans-serif",
logoUrl: null,
imageUrl: imageUrlFor(format),
};
}

/** A default house-ad fill promoting the CrawlProof Ad Network. Not metered. */
export function houseFill(format: AdFormatId): Fill {
const clickUrl = `${env.siteUrl}/?utm_source=house-ad&utm_medium=ad&utm_campaign=crawlproof-ads`;
return {
impressionId: crypto.randomUUID(),
campaignId: "house",
creativeId: "house",
refSlug: "house",
creative: houseCreative(format),
clickUrl,
html: renderHouseAdHtml(format, clickUrl),
};
}
7 changes: 5 additions & 2 deletions lib/ads/serve.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
type AdCreative,
type AdFormatId,
} from "./creative";
import { houseFill } from "./house";
import { CPC_CENTS, CPC_CREDITS, PLATFORM_RATE } from "./pricing";

// Server-side ad selection + metering. Runs under the service-role client so
Expand Down Expand Up @@ -101,7 +102,8 @@ export async function serveAd(
.eq("ad_campaigns.status", "active")
.limit(100);

if (!creatives || creatives.length === 0) return null;
// No paid creative for this format → default CrawlProof house ad.
if (!creatives || creatives.length === 0) return houseFill(format);

type CampaignJoin = {
id: string;
Expand All @@ -125,7 +127,8 @@ export async function serveAd(
return spentToday + CPC_CENTS <= c.daily_budget_cents;
},
);
if (eligible.length === 0) return null;
// Every candidate is out of budget → fall back to the house ad.
if (eligible.length === 0) return houseFill(format);

const pick = eligible[Math.floor(Math.random() * eligible.length)];
const campaign = oneCampaign(pick.ad_campaigns);
Expand Down
Binary file added public/ads/house/promo-rect.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/ads/house/promo-wide.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading