feed: intersperse mobile-only 300x250 ads every 4 articles
Mobile readers previously saw zero ads above the bottom of the feed
(header leaderboard is hidden md:block, sidebar stack pushes below the
feed at <lg breakpoints). This adds a 300x250 ad block after every 4th
article in <ArticleFeed>, wrapped in md:hidden so tablet/desktop are
untouched (they already get SidebarAdStack to the right of the feed).
- Ad pool: rotateAll('300x250') from src/lib/ads.ts (DB-backed, falls
back to the 7-ad hardcoded pool)
- Cycles through the pool if the feed is longer than the pool
- Skips the insert after the very last article
- Labelled "Sponsored", divide-y separator inherited from parent
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,10 +1,14 @@
|
|||||||
"use client";
|
"use client";
|
||||||
import React, { useState, useRef, useId, useEffect, useCallback } from "react";
|
import React, { Fragment, useState, useRef, useId, useEffect, useCallback, useMemo } from "react";
|
||||||
import AppImage from "@/components/ui/AppImage";
|
import AppImage from "@/components/ui/AppImage";
|
||||||
import SidebarAdStack from "@/components/SidebarAdStack";
|
import SidebarAdStack from "@/components/SidebarAdStack";
|
||||||
|
import AdImage from "@/components/AdImage";
|
||||||
|
import { rotateAll, type Ad } from "@/lib/ads";
|
||||||
import { PersonIcon, ChevronLeftIcon, ChevronRightIcon, SearchIcon, CloseIcon } from "@/components/ui/Icons";
|
import { PersonIcon, ChevronLeftIcon, ChevronRightIcon, SearchIcon, CloseIcon } from "@/components/ui/Icons";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
|
||||||
|
const MOBILE_AD_EVERY_N = 4;
|
||||||
|
|
||||||
|
|
||||||
interface ArticleItem {
|
interface ArticleItem {
|
||||||
title: string;
|
title: string;
|
||||||
@@ -86,6 +90,10 @@ export default function ArticleFeed() {
|
|||||||
const sentinelRef = useRef<HTMLDivElement>(null);
|
const sentinelRef = useRef<HTMLDivElement>(null);
|
||||||
const feedId = useId();
|
const feedId = useId();
|
||||||
|
|
||||||
|
// Shuffled 300x250 pool for mobile in-feed inserts (md:hidden).
|
||||||
|
// Tablet/desktop already get the SidebarAdStack to the right of the feed.
|
||||||
|
const mobileInfeedAds = useMemo<Ad[]>(() => rotateAll("300x250"), []);
|
||||||
|
|
||||||
// Load preference from localStorage
|
// Load preference from localStorage
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
try {
|
try {
|
||||||
@@ -550,8 +558,18 @@ export default function ArticleFeed() {
|
|||||||
: `/articles/${article?.slug}`;
|
: `/articles/${article?.slug}`;
|
||||||
const linkProps = isImported ? { target: "_blank", rel: "noopener noreferrer" } : {};
|
const linkProps = isImported ? { target: "_blank", rel: "noopener noreferrer" } : {};
|
||||||
|
|
||||||
|
// Mobile in-feed ad after every Nth article (skip if last item).
|
||||||
|
const isAdSlot =
|
||||||
|
(i + 1) % MOBILE_AD_EVERY_N === 0 &&
|
||||||
|
i + 1 < displayedArticles.length &&
|
||||||
|
mobileInfeedAds.length > 0;
|
||||||
|
const inFeedAd = isAdSlot
|
||||||
|
? mobileInfeedAds[Math.floor((i + 1) / MOBILE_AD_EVERY_N - 1) % mobileInfeedAds.length]
|
||||||
|
: null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<article key={i} className="article-card flex gap-3 md:gap-4 py-4 md:py-5 group cursor-pointer rounded-sm px-2 -mx-2">
|
<Fragment key={`feed-${i}`}>
|
||||||
|
<article className="article-card flex gap-3 md:gap-4 py-4 md:py-5 group cursor-pointer rounded-sm px-2 -mx-2">
|
||||||
<Link
|
<Link
|
||||||
href={articleHref}
|
href={articleHref}
|
||||||
{...linkProps}
|
{...linkProps}
|
||||||
@@ -604,6 +622,18 @@ export default function ArticleFeed() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
|
{inFeedAd && (
|
||||||
|
<div
|
||||||
|
className="md:hidden py-5 flex flex-col items-center bg-[#0a0a0a]"
|
||||||
|
aria-label="Advertisement"
|
||||||
|
>
|
||||||
|
<span className="text-[9px] font-mono uppercase tracking-wider text-[#555] mb-2">
|
||||||
|
Sponsored
|
||||||
|
</span>
|
||||||
|
<AdImage ad={inFeedAd} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Fragment>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user