|
| 1 | +import crypto from "node:crypto"; |
| 2 | +import { env } from "@/lib/env"; |
| 3 | +import { formatSpec, type AdCreative, type AdFormatId } from "./creative"; |
| 4 | +import type { Fill } from "./serve"; |
| 5 | + |
| 6 | +// Default "house" ad for the CrawlProof Ad Network. Shown when a slot has no |
| 7 | +// eligible paid campaign, so a publisher's unit is never blank. Uses default |
| 8 | +// AI-generated promo artwork (public/ads/house/*) with an overlaid pitch that |
| 9 | +// links back to CrawlProof. House ads are not metered or billed. |
| 10 | + |
| 11 | +const HOUSE = { |
| 12 | + headline: "Your ad here", |
| 13 | + body: "Crypto pay-per-click ads for indie sites — no middlemen.", |
| 14 | + cta: "Advertise →", |
| 15 | +} as const; |
| 16 | + |
| 17 | +function imageUrlFor(format: AdFormatId): string { |
| 18 | + const rect = format === "banner_300x250"; |
| 19 | + return `${env.siteUrl}/ads/house/${rect ? "promo-rect" : "promo-wide"}.webp`; |
| 20 | +} |
| 21 | + |
| 22 | +function esc(s: string): string { |
| 23 | + return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """); |
| 24 | +} |
| 25 | + |
| 26 | +export function renderHouseAdHtml(format: AdFormatId, clickUrl: string): string { |
| 27 | + const { w, h } = formatSpec(format); |
| 28 | + const img = imageUrlFor(format); |
| 29 | + const isMobile = format === "banner_320x50"; |
| 30 | + const isRect = format === "banner_300x250"; |
| 31 | + const row = !isRect; // leaderboard + mobile are horizontal |
| 32 | + |
| 33 | + 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>`; |
| 34 | + 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>`; |
| 35 | + const body = isMobile |
| 36 | + ? "" |
| 37 | + : `<div style="font-size:${isRect ? 13 : 12}px;color:#c7d2de;margin-top:4px;max-width:${isRect ? "100%" : "62%"}">${esc(HOUSE.body)}</div>`; |
| 38 | + 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>`; |
| 39 | + |
| 40 | + const content = row |
| 41 | + ? `<div style="position:relative;z-index:2;display:flex;align-items:center;gap:10px;height:100%;padding:0 12px"> |
| 42 | + <div style="min-width:0">${headline}${body}</div> |
| 43 | + <div style="margin-left:auto;flex:0 0 auto">${cta}</div> |
| 44 | + </div>` |
| 45 | + : `<div style="position:relative;z-index:2;display:flex;flex-direction:column;height:100%;padding:14px"> |
| 46 | + <div style="margin-top:auto">${headline}${body}</div> |
| 47 | + <div style="margin-top:12px">${cta}</div> |
| 48 | + </div>`; |
| 49 | + |
| 50 | + const overlay = isRect |
| 51 | + ? "linear-gradient(180deg, rgba(7,10,16,.12) 0%, rgba(7,10,16,.86) 76%)" |
| 52 | + : "linear-gradient(90deg, rgba(7,10,16,.92) 0%, rgba(7,10,16,.5) 62%, rgba(7,10,16,.2) 100%)"; |
| 53 | + |
| 54 | + return `<!doctype html><html><head><meta charset="utf-8"><style> |
| 55 | + *{box-sizing:border-box;margin:0} |
| 56 | + a{text-decoration:none;display:block} |
| 57 | + .cp-ad{position:relative;width:${w}px;height:${h}px;overflow:hidden;border-radius:8px; |
| 58 | + border:1px solid rgba(255,255,255,.08);background:#070a10; |
| 59 | + font-family:system-ui,-apple-system,Segoe UI,Roboto,sans-serif} |
| 60 | + .cp-ad .bg{position:absolute;inset:0;z-index:0;background:url("${esc(img)}") center/cover no-repeat} |
| 61 | + .cp-ad .ov{position:absolute;inset:0;z-index:1;background:${overlay}} |
| 62 | + </style></head><body> |
| 63 | + <a class="cp-ad" href="${esc(clickUrl)}" target="_blank" rel="noopener sponsored"> |
| 64 | + <div class="bg"></div><div class="ov"></div>${label}${content} |
| 65 | + </a> |
| 66 | + </body></html>`; |
| 67 | +} |
| 68 | + |
| 69 | +function houseCreative(format: AdFormatId): AdCreative { |
| 70 | + return { |
| 71 | + format, |
| 72 | + headline: HOUSE.headline, |
| 73 | + body: HOUSE.body, |
| 74 | + ctaText: HOUSE.cta, |
| 75 | + bgColor: "#070a10", |
| 76 | + fgColor: "#eef3f8", |
| 77 | + accentColor: "#6ee7b7", |
| 78 | + fontFamily: "system-ui, -apple-system, Segoe UI, Roboto, sans-serif", |
| 79 | + logoUrl: null, |
| 80 | + imageUrl: imageUrlFor(format), |
| 81 | + }; |
| 82 | +} |
| 83 | + |
| 84 | +/** A default house-ad fill promoting the CrawlProof Ad Network. Not metered. */ |
| 85 | +export function houseFill(format: AdFormatId): Fill { |
| 86 | + const clickUrl = `${env.siteUrl}/?utm_source=house-ad&utm_medium=ad&utm_campaign=crawlproof-ads`; |
| 87 | + return { |
| 88 | + impressionId: crypto.randomUUID(), |
| 89 | + campaignId: "house", |
| 90 | + creativeId: "house", |
| 91 | + refSlug: "house", |
| 92 | + creative: houseCreative(format), |
| 93 | + clickUrl, |
| 94 | + html: renderHouseAdHtml(format, clickUrl), |
| 95 | + }; |
| 96 | +} |
0 commit comments