fix(avbeat): 3 homepage bugs — invisible AI sidebar titles, dark tagline on blue header, literal <p> in excerpts

(A) src/components/AISuggestedArticles.tsx :186 + :287 — 'AI Suggested Articles' card titles
    used text-[#e0e0e0] on now-white card surfaces, making the title invisible until
    group-hover flipped it to #1D4ED8. Both occurrences changed to text-[#0F172A].
    (ArticleFeed.tsx:627 and NewsPageClient.tsx:276 were already navy from the
    prior contrast-fix deploy and cover both pagination + infinite-scroll variants.)

(B) src/components/AvBeatLogo.tsx :99 — header tagline 'Inside the Future of Pro AV'
    was rendered in --brand-text-muted (#475569 slate) on a now-blue nav (#1D4ED8),
    near-invisible. Changed to rgba(255,255,255,0.85) so it reads against the blue
    while sitting visually subordinate to the wordmark above it.

(C) src/lib/articles/excerpt.ts — cleanExcerpt now strips HTML tags (<p>, <br>, <em>, etc.)
    and decodes common entities (&nbsp;, &amp;, &lt;, &gt;, &quot;, &#039;, &apos;) at
    render time, then collapses whitespace before the existing trailing-junk/last-word
    logic runs. Display-layer only — stored excerpts are not mutated, matching the
    'fix at the DISPLAY layer' instruction.
This commit is contained in:
2026-06-03 13:20:32 +00:00
parent a1148e213b
commit 1977e077f8
3 changed files with 23 additions and 8 deletions

View File

@@ -183,7 +183,7 @@ export default function AISuggestedArticles({ variant = 'compact' }: Props) {
<p className="text-[#1D4ED8] font-body text-[9px] font-bold uppercase tracking-wider mb-0.5">
{article.category}
</p>
<p className="font-heading text-[#e0e0e0] text-xs font-semibold leading-snug line-clamp-2 group-hover:text-[#1D4ED8] transition-colors">
<p className="font-heading text-[#0F172A] text-xs font-semibold leading-snug line-clamp-2 group-hover:text-[#1D4ED8] transition-colors">
{article.title}
</p>
</div>
@@ -284,7 +284,7 @@ export default function AISuggestedArticles({ variant = 'compact' }: Props) {
{article.category}
</span>
</div>
<h3 className="font-heading text-[#e0e0e0] text-sm font-semibold leading-snug line-clamp-2 group-hover:text-[#1D4ED8] transition-colors">
<h3 className="font-heading text-[#0F172A] text-sm font-semibold leading-snug line-clamp-2 group-hover:text-[#1D4ED8] transition-colors">
{article.title}
</h3>
<p className="text-[#555] font-body text-[11px] mt-1">{article.readTime}</p>

View File

@@ -96,7 +96,7 @@ export default function AvBeatLogo({
fontWeight: 500,
fontSize: 11,
letterSpacing: "0.04em",
color: "var(--brand-text-muted, #475569)",
color: "rgba(255, 255, 255, 0.85)",
lineHeight: 1.2,
whiteSpace: "nowrap",
}}

View File

@@ -1,16 +1,31 @@
// Render-time excerpt sanitizer. Some imported / AI-generated excerpts were
// hard-truncated at a fixed byte count, leaving sentences clipped mid-word
// (e.g. "Since 1976, the co"). Until the upstream excerpt generator gets the
// same treatment, this helper makes any clipped excerpt visually clean:
// strip trailing junk, snap to the last complete word, and append an
// ellipsis only when the excerpt isn't already a complete sentence.
// (e.g. "Since 1976, the co"). Others arrive with raw HTML still in the
// string (e.g. "<p>In today's fast-paced..."), which renders as visible
// "<p>" text on the card. This helper handles both at render time without
// mutating stored data.
const SENTENCE_END = /[.!?…]['")\]]?$/;
const TRAILING_JUNK = /[\s,;:—–\-—_]+$/;
export function cleanExcerpt(raw: string | null | undefined): string {
if (!raw) return "";
let s = raw.replace(/&nbsp;/g, " ").trim();
// Strip any HTML tags first — excerpts are rendered as plain text, so
// <p>, <br>, <em>, etc. should be removed rather than escaped.
// Replace tags with a space so adjacent words don't collide when
// formerly-block-level boundaries are removed.
let s = raw.replace(/<[^>]*>/g, " ");
// Decode the common entities that show up in WP/AI-imported excerpts.
s = s
.replace(/&nbsp;/g, " ")
.replace(/&amp;/g, "&")
.replace(/&lt;/g, "<")
.replace(/&gt;/g, ">")
.replace(/&quot;/g, '"')
.replace(/&#039;/g, "'")
.replace(/&apos;/g, "'");
// Collapse multi-whitespace runs left behind by tag removal.
s = s.replace(/\s+/g, " ").trim();
if (!s) return "";
if (SENTENCE_END.test(s)) return s;
// Drop the trailing partial token.