diff --git a/lib/ads/house.ts b/lib/ads/house.ts new file mode 100644 index 00000000..50472bf7 --- /dev/null +++ b/lib/ads/house.ts @@ -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, "&").replace(//g, ">").replace(/"/g, """); +} + +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 = `CrawlProof Ads`; + const headline = `
${esc(HOUSE.headline)}
`; + const body = isMobile + ? "" + : `
${esc(HOUSE.body)}
`; + const cta = `${esc(HOUSE.cta)}`; + + const content = row + ? `
+
${headline}${body}
+
${cta}
+
` + : `
+
${headline}${body}
+
${cta}
+
`; + + 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 ` + +
${label}${content} +
+ `; +} + +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), + }; +} diff --git a/lib/ads/serve.ts b/lib/ads/serve.ts index 1d85a0cc..ae3fd127 100644 --- a/lib/ads/serve.ts +++ b/lib/ads/serve.ts @@ -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 @@ -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; @@ -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); diff --git a/public/ads/house/promo-rect.webp b/public/ads/house/promo-rect.webp new file mode 100644 index 00000000..b874bbf8 Binary files /dev/null and b/public/ads/house/promo-rect.webp differ diff --git a/public/ads/house/promo-wide.webp b/public/ads/house/promo-wide.webp new file mode 100644 index 00000000..4f776d5c Binary files /dev/null and b/public/ads/house/promo-wide.webp differ