fix(seo): strip HTML from meta description + OG/Twitter/Schema.org

Article excerpts sometimes contain raw <p> tags; Next.js HTML-escapes
them into the page <meta>, surfacing as literal '&lt;p&gt;' in view-source
and breaking SEO previews. cleanExcerpt() strips tags + entities for all
plain-text meta surfaces.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
ops-bot
2026-06-04 15:21:26 +00:00
parent e84f69f241
commit aa5bee95e2
2 changed files with 15 additions and 8 deletions

View File

@@ -1,4 +1,5 @@
import type { Metadata } from "next";
import { cleanExcerpt } from "@/lib/articles/excerpt";
import { redirect } from "next/navigation";
import ArticleDetailPage from "./ArticleDetailClient";
import type { Article } from "@/lib/articles/types";
@@ -53,12 +54,12 @@ export async function generateMetadata({ params }: PageProps): Promise<Metadata>
}
return {
title: article.title,
description: article.excerpt,
description: cleanExcerpt(article.excerpt),
alternates: { canonical: `/articles/${article.slug}` },
openGraph: {
type: "article",
title: article.title,
description: article.excerpt,
description: cleanExcerpt(article.excerpt),
url: `/articles/${article.slug}`,
images: [{ url: article.image, width: 1200, height: 630, alt: article.alt }],
authors: [article.author],
@@ -68,7 +69,7 @@ export async function generateMetadata({ params }: PageProps): Promise<Metadata>
twitter: {
card: "summary_large_image",
title: article.title,
description: article.excerpt,
description: cleanExcerpt(article.excerpt),
images: [article.image],
creator: "@AV Beat",
},
@@ -93,7 +94,7 @@ export default async function ArticlePage({ params }: PageProps) {
"@context": "https://schema.org",
"@type": "Article",
headline: article.title,
description: article.excerpt,
description: cleanExcerpt(article.excerpt),
image: [absoluteImage(article.image)],
author: { "@type": "Person", name: article.author },
publisher: {

View File

@@ -8,6 +8,7 @@ import {
getRelatedForumThreads, type RelatedForumThread,
} from "@/lib/articles/legacy-source";
import { linkifyAndExtractMentions } from "@/lib/company-mentions";
import { cleanExcerpt } from "@/lib/articles/excerpt";
import CompanyMentionsHover from "@/components/CompanyMentionsHover";
import CompaniesInThisStory from "@/components/CompaniesInThisStory";
import NewsArticleDetailClient from "./NewsArticleDetailClient";
@@ -58,14 +59,19 @@ export async function generateMetadata({ params }: PageProps): Promise<Metadata>
if (!article) {
return { title: "Article Not Found", description: "The article you are looking for does not exist." };
}
// Strip HTML from excerpt before stuffing into <meta description> /
// openGraph / twitter — those MUST be plain text. Raw excerpts sometimes
// contain <p>...</p>, which Next.js then HTML-escapes, surfacing as
// literal `&lt;p&gt;` in page source.
const cleanDesc = cleanExcerpt(article.excerpt);
return {
title: `${article.title} — AV Beat`,
description: article.excerpt,
description: cleanDesc,
alternates: { canonical: `/news/${article.slug}` },
openGraph: {
type: "article",
title: article.title,
description: article.excerpt,
description: cleanDesc,
url: `/news/${article.slug}`,
images: [{ url: article.image, width: 1200, height: 630, alt: article.alt }],
authors: [article.author],
@@ -75,7 +81,7 @@ export async function generateMetadata({ params }: PageProps): Promise<Metadata>
twitter: {
card: "summary_large_image",
title: article.title,
description: article.excerpt,
description: cleanDesc,
images: [article.image],
creator: "@AV Beat",
},
@@ -106,7 +112,7 @@ export default async function NewsArticlePage({ params }: PageProps) {
"@context": "https://schema.org",
"@type": "NewsArticle",
headline: article.title,
description: article.excerpt,
description: cleanExcerpt(article.excerpt),
image: [absoluteImage(article.image)],
author: { "@type": "Person", name: article.author },
publisher: {