diff --git a/public/assets/images/article-placeholder.svg b/public/assets/images/article-placeholder.svg
new file mode 100644
index 0000000..b08f131
--- /dev/null
+++ b/public/assets/images/article-placeholder.svg
@@ -0,0 +1,20 @@
+
diff --git a/src/app/articles/[slug]/page.tsx b/src/app/articles/[slug]/page.tsx
index c3a5460..8c3c589 100644
--- a/src/app/articles/[slug]/page.tsx
+++ b/src/app/articles/[slug]/page.tsx
@@ -16,7 +16,19 @@ interface PageProps {
}
const SITE_URL =
- process.env.NEXT_PUBLIC_SITE_URL || "https://bb-staging.onsethost.com";
+ process.env.NEXT_PUBLIC_SITE_URL || "https://broadcastbeat.com";
+
+function toIso(d: string | null | undefined): string {
+ if (!d) return new Date().toISOString();
+ const parsed = new Date(d);
+ return isNaN(parsed.getTime()) ? new Date().toISOString() : parsed.toISOString();
+}
+
+function absoluteImage(src: string | undefined | null): string {
+ if (!src) return `${SITE_URL}/assets/images/article-placeholder.jpg`;
+ if (/^https?:\/\//i.test(src)) return src;
+ return `${SITE_URL}${src.startsWith("/") ? "" : "/"}${src}`;
+}
async function loadArticle(slug: string): Promise<{
article: Article | null;
@@ -77,15 +89,16 @@ export default async function ArticlePage({ params }: PageProps) {
"@type": "Article",
headline: article.title,
description: article.excerpt,
- image: article.image,
+ image: [absoluteImage(article.image)],
author: { "@type": "Person", name: article.author },
publisher: {
"@type": "Organization",
name: "BroadcastBeat",
- logo: { "@type": "ImageObject", url: `${SITE_URL}/legacy/site/broadcastbeat-logo.png` },
+ logo: { "@type": "ImageObject", url: `${SITE_URL}/assets/images/logo.png` },
},
- datePublished: article.date,
+ datePublished: toIso(article.date),
dateModified: new Date().toISOString(),
+ mainEntityOfPage: { "@type": "WebPage", "@id": `${SITE_URL}/articles/${article.slug}` },
};
return (
diff --git a/src/app/layout.tsx b/src/app/layout.tsx
index 6c73d74..f6abace 100644
--- a/src/app/layout.tsx
+++ b/src/app/layout.tsx
@@ -20,18 +20,9 @@ export const metadata: Metadata = {
},
alternates: {
canonical: '/',
- languages: {
- 'en': '/en',
- 'es': '/es',
- 'pt': '/pt',
- 'fr': '/fr',
- 'de': '/de',
- 'zh': '/zh',
- 'ja': '/ja',
- 'ar': '/ar',
- 'hi': '/hi',
- 'x-default': '/en',
- },
+ // hreflang entries removed — we don't currently serve translated content,
+ // and pointing Google at /en, /es, /pt, etc. (which 404) was poisoning
+ // the SEO signal. Re-add when the i18n routes actually exist.
},
openGraph: {
type: 'website',
@@ -59,11 +50,14 @@ export const metadata: Metadata = {
images: ['/assets/images/og-image.png']
},
robots: {
- index: true,
- follow: true,
+ // noindex while Phase B (PR rewrite) and Phase D (i18n routes) land.
+ // Flip both to true once Google Rich Results Test passes on a sample
+ // NewsArticle and the news sitemap is wired up (Phase C).
+ index: false,
+ follow: false,
googleBot: {
- index: true,
- follow: true,
+ index: false,
+ follow: false,
'max-video-preview': -1,
'max-image-preview': 'large',
'max-snippet': -1
@@ -78,17 +72,7 @@ export default function RootLayout({
return (
- {/* hreflang tags for multilingual SEO — Section 18G */}
-
-
-
-
-
-
-
-
-
-
+ {/* hreflang entries removed pending real i18n routes (Phase D). */}
diff --git a/src/app/news/[slug]/page.tsx b/src/app/news/[slug]/page.tsx
index 506bc71..544bb90 100644
--- a/src/app/news/[slug]/page.tsx
+++ b/src/app/news/[slug]/page.tsx
@@ -18,7 +18,21 @@ interface PageProps {
}
const SITE_URL =
- process.env.NEXT_PUBLIC_SITE_URL || "https://bb-staging.onsethost.com";
+ process.env.NEXT_PUBLIC_SITE_URL || "https://broadcastbeat.com";
+
+/** Convert a free-form date (e.g. "April 9, 2026") to ISO 8601 for JSON-LD. */
+function toIso(d: string | null | undefined): string {
+ if (!d) return new Date().toISOString();
+ const parsed = new Date(d);
+ return isNaN(parsed.getTime()) ? new Date().toISOString() : parsed.toISOString();
+}
+
+/** Make an image path absolute against SITE_URL for Google News compliance. */
+function absoluteImage(src: string | undefined | null): string {
+ if (!src) return `${SITE_URL}/assets/images/article-placeholder.jpg`;
+ if (/^https?:\/\//i.test(src)) return src;
+ return `${SITE_URL}${src.startsWith("/") ? "" : "/"}${src}`;
+}
async function loadArticle(slug: string): Promise<{
article: Article | null;
@@ -80,14 +94,14 @@ export default async function NewsArticlePage({ params }: PageProps) {
"@type": "NewsArticle",
headline: article.title,
description: article.excerpt,
- image: article.image,
+ image: [absoluteImage(article.image)],
author: { "@type": "Person", name: article.author },
publisher: {
"@type": "Organization",
name: "BroadcastBeat",
- logo: { "@type": "ImageObject", url: `${SITE_URL}/legacy/site/broadcastbeat-logo.png` },
+ logo: { "@type": "ImageObject", url: `${SITE_URL}/assets/images/logo.png` },
},
- datePublished: article.date,
+ datePublished: toIso(article.date),
dateModified: new Date().toISOString(),
mainEntityOfPage: { "@type": "WebPage", "@id": `${SITE_URL}/news/${article.slug}` },
};
diff --git a/src/app/robots.ts b/src/app/robots.ts
index c7b4eab..16c7a01 100644
--- a/src/app/robots.ts
+++ b/src/app/robots.ts
@@ -1,7 +1,7 @@
import { MetadataRoute } from 'next';
export default function robots(): MetadataRoute.Robots {
- const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || 'https://broadcastb5322.builtwithrocket.new';
+ const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || 'https://broadcastbeat.com';
return {
rules: [
diff --git a/src/app/sitemap.ts b/src/app/sitemap.ts
index d1ff14c..c450b53 100644
--- a/src/app/sitemap.ts
+++ b/src/app/sitemap.ts
@@ -1,7 +1,7 @@
import { MetadataRoute } from 'next';
import { getLegacyRecentSitemapEntries } from '@/lib/articles/legacy-source';
-const BASE_URL = process.env.NEXT_PUBLIC_SITE_URL || 'https://bb-staging.onsethost.com';
+const BASE_URL = process.env.NEXT_PUBLIC_SITE_URL || 'https://broadcastbeat.com';
export default async function sitemap(): Promise {
// Static pages
diff --git a/src/components/AdImage.tsx b/src/components/AdImage.tsx
index f4c7328..213de3c 100644
--- a/src/components/AdImage.tsx
+++ b/src/components/AdImage.tsx
@@ -9,16 +9,21 @@ interface Ad {
src?: string;
alt?: string;
click_url?: string;
- adsanity_id?: string;
}
+// Renders a local creative from /public/legacy/ads/... and tracks impressions
+// + clicks to bb.banner_analytics. The previous Adsanity iframe path
+// (ads.broadcastbeat.com) was removed — that subdomain doesn't resolve and
+// caused 30s timeouts. To add a new banner, drop the file in
+// public/legacy/ads/ and add an entry to ADS_300X250 / ADS_728X90 in
+// src/lib/ads.ts with src + alt + click_url.
export default function AdImage({ ad, page, priority }: { ad: Ad; page?: string; priority?: boolean }) {
const containerRef = useRef(null);
const trackedRef = useRef(false);
const supabase = createClient();
- const adsanityId = ad.adsanity_id || '';
useEffect(() => {
+ if (!ad.src) return;
const observer = new IntersectionObserver(([entry]) => {
if (entry.isIntersecting && !trackedRef.current) {
trackedRef.current = true;
@@ -27,10 +32,10 @@ export default function AdImage({ ad, page, priority }: { ad: Ad; page?: string;
ad_label: ad.label,
ad_size: ad.size,
event_type: 'impression',
- page: page || typeof window !== 'undefined' ? window.location.pathname : null,
- session_id: typeof window !== 'undefined' ? (window as any).__session_id : null,
- referrer: typeof document !== 'undefined' ? document.referrer : null
- }).catch(err => console.error('Impression:', err));
+ page: page || (typeof window !== 'undefined' ? window.location.pathname : null),
+ session_id: typeof window !== 'undefined' ? (window as { __session_id?: string }).__session_id : null,
+ referrer: typeof document !== 'undefined' ? document.referrer : null,
+ }).then(() => undefined, (err: unknown) => console.error('Impression:', err));
}
}, { threshold: 0.5 });
if (containerRef.current) observer.observe(containerRef.current);
@@ -43,29 +48,23 @@ export default function AdImage({ ad, page, priority }: { ad: Ad; page?: string;
ad_label: ad.label,
ad_size: ad.size,
event_type: 'click',
- page: page || typeof window !== 'undefined' ? window.location.pathname : null,
- session_id: typeof window !== 'undefined' ? (window as any).__session_id : null,
- referrer: typeof document !== 'undefined' ? document.referrer : null
- }).catch(err => console.error('Click:', err));
+ page: page || (typeof window !== 'undefined' ? window.location.pathname : null),
+ session_id: typeof window !== 'undefined' ? (window as { __session_id?: string }).__session_id : null,
+ referrer: typeof document !== 'undefined' ? document.referrer : null,
+ }).then(() => undefined, (err: unknown) => console.error('Click:', err));
};
+ if (!ad.src) return null;
+
const width = parseInt(ad.size.split('x')[0]);
const height = parseInt(ad.size.split('x')[1]);
- if (adsanityId) {
- return (
-
-
-
- );
- }
-
- if (!ad.src) return null;
-
return (
{ad.click_url ? (
-
+
+
+
) : (
)}
diff --git a/src/components/SidebarAdStack.tsx b/src/components/SidebarAdStack.tsx
index c75cdb2..8fe021f 100644
--- a/src/components/SidebarAdStack.tsx
+++ b/src/components/SidebarAdStack.tsx
@@ -22,7 +22,7 @@ export default function SidebarAdStack({
{FIXED_300X600 &&
}
{rotating.map((ad) => (
-
+
))}
);
diff --git a/src/lib/ads.ts b/src/lib/ads.ts
index d993910..66c2138 100644
--- a/src/lib/ads.ts
+++ b/src/lib/ads.ts
@@ -2,20 +2,19 @@
// render on broadcastbeat.com. Adding a new entry here automatically
// rotates it into the appropriate zone — no other code changes needed.
//
-// Each Ad must provide EITHER:
-// - src + click_url + alt → local image, click goes to click_url
-// - adsanity_id → Adsanity iframe (Adsanity tracks the click)
+// Each Ad must provide a local image (src + click_url + alt). The legacy
+// Adsanity iframe path was removed because ads.broadcastbeat.com no longer
+// resolves; banners awaiting creatives are tracked in PENDING_ads.md.
export interface Ad {
label: string;
size: "300x250" | "300x600" | "728x90";
- src?: string;
- alt?: string;
+ src: string;
+ alt: string;
click_url?: string;
- adsanity_id?: string;
}
-/** 728x90 leaderboard pool — header (top of every page) and footer. */
+/** 728x90 leaderboard pool — header (between nav and ticker) and footer. */
export const ADS_728X90: Ad[] = [
{
label: "LiveU",
@@ -30,18 +29,12 @@ export const ADS_728X90: Ad[] = [
* 300x250 sidebar pool. ALL entries render on every page — SidebarAdStack
* stacks them vertically and shuffles the order per page-view so each banner
* cycles through positions. Add new entries here to grow the stack.
+ *
+ * AJA, Zixi, Lectrosonics, Opengear, Studio Hero, Magewell were removed
+ * pending real creatives (their Adsanity campaigns relied on
+ * ads.broadcastbeat.com, which is down). See PENDING_ads.md.
*/
export const ADS_300X250: Ad[] = [
- {
- label: "AJA Video",
- size: "300x250",
- adsanity_id: "ad-164285",
- },
- {
- label: "Zixi",
- size: "300x250",
- adsanity_id: "ad-156737",
- },
{
label: "Telycam",
size: "300x250",
@@ -56,37 +49,13 @@ export const ADS_300X250: Ad[] = [
alt: "LiveU pay-as-you-go",
click_url: "https://bit.ly/4ghXVeq",
},
- {
- label: "Lectrosonics",
- size: "300x250",
- adsanity_id: "ad-156727",
- },
- {
- label: "Opengear",
- size: "300x250",
- adsanity_id: "ad-200174",
- },
- {
- label: "Studio Hero",
- size: "300x250",
- adsanity_id: "ad-162714",
- },
- {
- label: "Magewell",
- size: "300x250",
- adsanity_id: "ad-154296",
- },
];
/**
* 300x600 fixed sidebar slot. Rendered at the TOP of SidebarAdStack and is
- * NEVER rotated. Set to null to omit. To replace, swap this single entry.
+ * NEVER rotated. Set to null to omit. Blackmagic ATEM creative pending.
*/
-export const FIXED_300X600: Ad | null = {
- label: "Blackmagic",
- size: "300x600",
- adsanity_id: "ad-210571",
-};
+export const FIXED_300X600: Ad | null = null;
/** Fisher-Yates shuffle, returns a new array. */
export function shuffle
(arr: T[]): T[] {
@@ -109,7 +78,7 @@ export function rotateAll(size: Ad["size"], exclude: Set = new Set()): A
: size === "728x90" ? ADS_728X90
: size === "300x600" ? (FIXED_300X600 ? [FIXED_300X600] : [])
: [];
- const key = (a: Ad) => a.src ?? a.adsanity_id ?? a.label;
+ const key = (a: Ad) => a.src ?? a.label;
return shuffle(pool.filter((a) => !exclude.has(key(a))));
}
diff --git a/src/lib/articles/legacy-source.ts b/src/lib/articles/legacy-source.ts
index d606339..1fe5268 100644
--- a/src/lib/articles/legacy-source.ts
+++ b/src/lib/articles/legacy-source.ts
@@ -28,7 +28,7 @@ interface ImportedPostRow {
wp_published_at: string | null;
}
-const FALLBACK_IMAGE = "/legacy/site/no_image.png";
+const FALLBACK_IMAGE = "/assets/images/article-placeholder.svg";
// Tag → section mapping. Source: tag distribution observed in
// bb.wp_imported_posts. Matches against any overlap (lowercased).
@@ -80,9 +80,16 @@ function readTime(content: string | null): string {
return `${minutes} min read`;
}
+// Featured-image URLs that we treat as "no image" — historical placeholders
+// that should resolve to the clean dark placeholder instead of the grey box.
+const BROKEN_IMAGE_PATTERNS = /no_image|BB_Gray|placeholder/i;
+
function rowToArticle(row: ImportedPostRow, section: Section = "news"): Article {
- const image = row.featured_image
- ? rewriteLegacyImageUrl(row.featured_image)
+ const rawImage = row.featured_image && !BROKEN_IMAGE_PATTERNS.test(row.featured_image)
+ ? row.featured_image
+ : null;
+ const image = rawImage
+ ? rewriteLegacyImageUrl(rawImage)
: FALLBACK_IMAGE;
const content = rewriteLegacyImageUrlsInHtml(row.content || "");
const author = row.author_name || "Staff Reporter";