add AdSlot component and place banner zones on article pages
Adds <AdSlot zone size /> server component that renders a labeled placeholder for now (size + zone exposed as data attrs for the future bb.ad_placements lookup). Inserted on news/[slug] article template: - 728×90 leaderboard above the <article> - 300×600 half-page at top of sidebar - 300×250 mid-rectangle in sidebar (replaces the old hand-rolled placeholder div) - 728×90 leaderboard below the </article>, before related-articles Same scheme should be applied to articles/[slug]/page.tsx in a follow-up once that template's body is finalized.
This commit is contained in:
@@ -4,6 +4,7 @@ import Link from "next/link";
|
|||||||
import AppImage from "@/components/ui/AppImage";
|
import AppImage from "@/components/ui/AppImage";
|
||||||
import Header from "@/components/Header";
|
import Header from "@/components/Header";
|
||||||
import Footer from "@/components/Footer";
|
import Footer from "@/components/Footer";
|
||||||
|
import AdSlot from "@/components/AdSlot";
|
||||||
import { createClient } from "@/lib/supabase/client";
|
import { createClient } from "@/lib/supabase/client";
|
||||||
import type { Article } from "@/lib/articles/sampleArticles";
|
import type { Article } from "@/lib/articles/sampleArticles";
|
||||||
|
|
||||||
@@ -280,6 +281,11 @@ export default function NewsArticleDetailClient({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Top leaderboard */}
|
||||||
|
<div className="max-w-container mx-auto px-4 pt-6">
|
||||||
|
<AdSlot zone="article-top" size="728x90" />
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Article */}
|
{/* Article */}
|
||||||
<article className="max-w-container mx-auto px-4 py-8 md:py-12">
|
<article className="max-w-container mx-auto px-4 py-8 md:py-12">
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-12 gap-8">
|
<div className="grid grid-cols-1 lg:grid-cols-12 gap-8">
|
||||||
@@ -444,6 +450,9 @@ export default function NewsArticleDetailClient({
|
|||||||
|
|
||||||
{/* Sidebar */}
|
{/* Sidebar */}
|
||||||
<aside className="lg:col-span-4 space-y-6">
|
<aside className="lg:col-span-4 space-y-6">
|
||||||
|
{/* Sidebar fixed half-page */}
|
||||||
|
<AdSlot zone="article-sidebar-top" size="300x600" />
|
||||||
|
|
||||||
{/* Related Articles Sidebar */}
|
{/* Related Articles Sidebar */}
|
||||||
<div className="bg-[#111] border border-[#222] p-5">
|
<div className="bg-[#111] border border-[#222] p-5">
|
||||||
<h3 className="font-body font-bold text-xs text-[#3b82f6] uppercase tracking-widest mb-4 pb-2 border-b border-[#222]">
|
<h3 className="font-body font-bold text-xs text-[#3b82f6] uppercase tracking-widest mb-4 pb-2 border-b border-[#222]">
|
||||||
@@ -491,16 +500,16 @@ export default function NewsArticleDetailClient({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Ad placeholder */}
|
{/* Ad placeholder */}
|
||||||
<div className="ad-placeholder w-full h-[250px] flex items-center justify-center">
|
<AdSlot zone="article-sidebar-mid" size="300x250" />
|
||||||
<div className="text-center">
|
|
||||||
<p className="text-[#444] text-xs font-body">Advertisement</p>
|
|
||||||
<p className="text-[#333] text-[10px] font-body mt-1">300×250</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</aside>
|
</aside>
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
|
|
||||||
|
{/* Bottom leaderboard */}
|
||||||
|
<div className="max-w-container mx-auto px-4 pb-8">
|
||||||
|
<AdSlot zone="article-bottom" size="728x90" />
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Related Articles Carousel */}
|
{/* Related Articles Carousel */}
|
||||||
{relatedArticles.length > 0 && (
|
{relatedArticles.length > 0 && (
|
||||||
<section className="border-t border-[#1a1a1a] bg-[#0d0d0d] py-10">
|
<section className="border-t border-[#1a1a1a] bg-[#0d0d0d] py-10">
|
||||||
|
|||||||
49
src/components/AdSlot.tsx
Normal file
49
src/components/AdSlot.tsx
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
import { ReactElement } from "react";
|
||||||
|
|
||||||
|
type IabSize = "728x90" | "300x250" | "300x600" | "160x600" | "970x250" | "320x50";
|
||||||
|
type Zone =
|
||||||
|
| "article-top"
|
||||||
|
| "article-bottom"
|
||||||
|
| "article-sidebar-top"
|
||||||
|
| "article-sidebar-mid"
|
||||||
|
| "article-inline"
|
||||||
|
| "homepage-top"
|
||||||
|
| "homepage-bottom";
|
||||||
|
|
||||||
|
const SIZE_MAP: Record<IabSize, { w: number; h: number }> = {
|
||||||
|
"728x90": { w: 728, h: 90 },
|
||||||
|
"300x250": { w: 300, h: 250 },
|
||||||
|
"300x600": { w: 300, h: 600 },
|
||||||
|
"160x600": { w: 160, h: 600 },
|
||||||
|
"970x250": { w: 970, h: 250 },
|
||||||
|
"320x50": { w: 320, h: 50 },
|
||||||
|
};
|
||||||
|
|
||||||
|
interface AdSlotProps {
|
||||||
|
zone: Zone;
|
||||||
|
size: IabSize;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Banner ad slot. Server component (no client JS).
|
||||||
|
*
|
||||||
|
* Renders a labeled placeholder until populated. The eventual implementation
|
||||||
|
* will query bb.ad_placements (server-side) and render an <a><img></a> for the
|
||||||
|
* winning campaign + record an impression.
|
||||||
|
*/
|
||||||
|
export default function AdSlot({ zone, size, className = "" }: AdSlotProps): ReactElement {
|
||||||
|
const { w, h } = SIZE_MAP[size];
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
role="complementary"
|
||||||
|
aria-label={`Advertisement (${size})`}
|
||||||
|
data-ad-zone={zone}
|
||||||
|
data-ad-size={size}
|
||||||
|
className={`mx-auto flex items-center justify-center bg-[#0d0d0d] border border-[#1a1a1a] text-[#444] text-[10px] font-body uppercase tracking-widest ${className}`}
|
||||||
|
style={{ width: w, height: h, maxWidth: "100%" }}
|
||||||
|
>
|
||||||
|
<span>Ad · {size}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user