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";
|
||||
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 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 Link from "next/link";
|
||||
|
||||
const MOBILE_AD_EVERY_N = 4;
|
||||
|
||||
|
||||
interface ArticleItem {
|
||||
title: string;
|
||||
@@ -86,6 +90,10 @@ export default function ArticleFeed() {
|
||||
const sentinelRef = useRef<HTMLDivElement>(null);
|
||||
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
|
||||
useEffect(() => {
|
||||
try {
|
||||
@@ -550,8 +558,18 @@ export default function ArticleFeed() {
|
||||
: `/articles/${article?.slug}`;
|
||||
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 (
|
||||
<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
|
||||
href={articleHref}
|
||||
{...linkProps}
|
||||
@@ -604,6 +622,18 @@ export default function ArticleFeed() {
|
||||
</div>
|
||||
</div>
|
||||
</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>
|
||||
|
||||
Reference in New Issue
Block a user