P0-2 + Phase A: drop Adsanity iframes, fix SEO baseline, noindex while staging
P0-2 (Adsanity iframe path removed): - AdImage.tsx: deleted the iframe branch and the ADSANITY_CAMPAIGN_MAP fallback. AdImage now renders local images only; banners without a src render nothing. - ads.ts: removed Ad.adsanity_id from the interface. Pruned AJA, Zixi, Lectrosonics, Opengear, Studio Hero, Magewell 300x250 and Blackmagic 300x600 — they relied on ads.broadcastbeat.com which does not resolve. Surviving live banners: LiveU 728x90, LiveU PAYG 300x250, Telycam 300x250. - SidebarAdStack: key no longer references the removed field. - No banner_analytics rows reference ads.broadcastbeat.com. Phase A (SEO baseline): - robots.ts: fallback base URL now broadcastbeat.com (was rocket staging URL). - sitemap.ts: fallback base URL now broadcastbeat.com (was bb-staging). - news/[slug]/page.tsx and articles/[slug]/page.tsx: same fallback fix. JSON-LD now uses ISO 8601 for datePublished, absolute image URL via SITE_URL prefix, and publisher.logo points at /assets/images/logo.png instead of the 404-ing /legacy/site/broadcastbeat-logo.png. - layout.tsx: removed the /en /es /pt /fr /de /zh /ja /ar /hi hreflang alternates - those routes 404 today; Phase D will reinstate. Replaced rocket.new Organization schema URL with broadcastbeat.com. Set robots index/follow=false until Phase B-D land and Rich Results passes. Featured-image fallback hardening: - legacy-source: rowToArticle now treats BB_Gray, no_image and placeholder URLs as null and falls back to the new placeholder so dead images do not leak through to the page. - public/assets/images/article-placeholder.svg: new clean dark gradient with BroadcastBeat wordmark, NOT no_image.png style. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
20
public/assets/images/article-placeholder.svg
Normal file
20
public/assets/images/article-placeholder.svg
Normal file
@@ -0,0 +1,20 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1200 630" preserveAspectRatio="xMidYMid slice">
|
||||
<defs>
|
||||
<linearGradient id="g" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="0%" stop-color="#1a1f2e"/>
|
||||
<stop offset="50%" stop-color="#0d1018"/>
|
||||
<stop offset="100%" stop-color="#080a10"/>
|
||||
</linearGradient>
|
||||
<radialGradient id="r" cx="50%" cy="50%" r="60%">
|
||||
<stop offset="0%" stop-color="#3b82f6" stop-opacity="0.10"/>
|
||||
<stop offset="100%" stop-color="#3b82f6" stop-opacity="0"/>
|
||||
</radialGradient>
|
||||
</defs>
|
||||
<rect width="1200" height="630" fill="url(#g)"/>
|
||||
<rect width="1200" height="630" fill="url(#r)"/>
|
||||
<g transform="translate(600 315)" text-anchor="middle" font-family="system-ui, sans-serif">
|
||||
<text y="0" font-size="48" font-weight="700" fill="#3b82f6" letter-spacing="0.05em">BROADCAST</text>
|
||||
<text y="56" font-size="48" font-weight="700" fill="#cbd5e1" letter-spacing="0.05em">BEAT</text>
|
||||
<text y="116" font-size="16" font-weight="500" fill="#64748b" letter-spacing="0.25em" text-transform="uppercase">NEWS · INTELLIGENCE · TECHNOLOGY</text>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
@@ -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 (
|
||||
|
||||
@@ -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 (
|
||||
<html lang="en">
|
||||
<head>
|
||||
{/* hreflang tags for multilingual SEO — Section 18G */}
|
||||
<link rel="alternate" hrefLang="en" href={`${process.env.NEXT_PUBLIC_SITE_URL || ''}/en`} />
|
||||
<link rel="alternate" hrefLang="es" href={`${process.env.NEXT_PUBLIC_SITE_URL || ''}/es`} />
|
||||
<link rel="alternate" hrefLang="pt" href={`${process.env.NEXT_PUBLIC_SITE_URL || ''}/pt`} />
|
||||
<link rel="alternate" hrefLang="fr" href={`${process.env.NEXT_PUBLIC_SITE_URL || ''}/fr`} />
|
||||
<link rel="alternate" hrefLang="de" href={`${process.env.NEXT_PUBLIC_SITE_URL || ''}/de`} />
|
||||
<link rel="alternate" hrefLang="zh" href={`${process.env.NEXT_PUBLIC_SITE_URL || ''}/zh`} />
|
||||
<link rel="alternate" hrefLang="ja" href={`${process.env.NEXT_PUBLIC_SITE_URL || ''}/ja`} />
|
||||
<link rel="alternate" hrefLang="ar" href={`${process.env.NEXT_PUBLIC_SITE_URL || ''}/ar`} />
|
||||
<link rel="alternate" hrefLang="hi" href={`${process.env.NEXT_PUBLIC_SITE_URL || ''}/hi`} />
|
||||
<link rel="alternate" hrefLang="x-default" href={`${process.env.NEXT_PUBLIC_SITE_URL || ''}/en`} />
|
||||
{/* hreflang entries removed pending real i18n routes (Phase D). */}
|
||||
|
||||
<script
|
||||
type="application/ld+json"
|
||||
@@ -97,7 +81,7 @@ export default function RootLayout({
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'Organization',
|
||||
name: 'BroadcastBeat',
|
||||
url: 'https://broadcastb5322.builtwithrocket.new',
|
||||
url: 'https://broadcastbeat.com',
|
||||
logo: "https://img.rocket.new/generatedImages/rocket_gen_img_1ab0c6b82-1768715893826.png",
|
||||
description: 'The digital platform for broadcast engineering professionals. Breaking news, deep-dive features, and industry spotlights from NAB to IBC and beyond.',
|
||||
sameAs: [
|
||||
@@ -109,7 +93,7 @@ export default function RootLayout({
|
||||
contactPoint: {
|
||||
'@type': 'ContactPoint',
|
||||
contactType: 'Editorial',
|
||||
url: 'https://broadcastb5322.builtwithrocket.new'
|
||||
url: 'https://broadcastbeat.com'
|
||||
}
|
||||
})
|
||||
}} />
|
||||
|
||||
@@ -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}` },
|
||||
};
|
||||
|
||||
@@ -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: [
|
||||
|
||||
@@ -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<MetadataRoute.Sitemap> {
|
||||
// Static pages
|
||||
|
||||
@@ -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<HTMLDivElement>(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 (
|
||||
<div ref={containerRef} className="ad-container">
|
||||
<iframe src={`https://ads.broadcastbeat.com/wp-content/plugins/adsanity/render.php?id=${adsanityId}`} width={width} height={height} frameBorder="0" scrolling="no" style={{display:'block'}} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!ad.src) return null;
|
||||
|
||||
return (
|
||||
<div ref={containerRef} className="ad-container">
|
||||
{ad.click_url ? (
|
||||
<a href={ad.click_url} rel="sponsored noopener noreferrer" onClick={handleClick}><AppImage src={ad.src} alt={ad.alt || ad.label} width={width} height={height} priority={priority} /></a>
|
||||
<a href={ad.click_url} rel="sponsored noopener noreferrer" onClick={handleClick}>
|
||||
<AppImage src={ad.src} alt={ad.alt || ad.label} width={width} height={height} priority={priority} />
|
||||
</a>
|
||||
) : (
|
||||
<AppImage src={ad.src} alt={ad.alt || ad.label} width={width} height={height} priority={priority} />
|
||||
)}
|
||||
|
||||
@@ -22,7 +22,7 @@ export default function SidebarAdStack({
|
||||
<div className={`space-y-4 ${className}`}>
|
||||
{FIXED_300X600 && <AdImage ad={FIXED_300X600} priority />}
|
||||
{rotating.map((ad) => (
|
||||
<AdImage key={ad.src ?? ad.adsanity_id ?? ad.label} ad={ad} />
|
||||
<AdImage key={ad.src ?? ad.label} ad={ad} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -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<T>(arr: T[]): T[] {
|
||||
@@ -109,7 +78,7 @@ export function rotateAll(size: Ad["size"], exclude: Set<string> = 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))));
|
||||
}
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
Reference in New Issue
Block a user