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 '<p>' 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:
@@ -1,4 +1,5 @@
|
|||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
|
import { cleanExcerpt } from "@/lib/articles/excerpt";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import ArticleDetailPage from "./ArticleDetailClient";
|
import ArticleDetailPage from "./ArticleDetailClient";
|
||||||
import type { Article } from "@/lib/articles/types";
|
import type { Article } from "@/lib/articles/types";
|
||||||
@@ -53,12 +54,12 @@ export async function generateMetadata({ params }: PageProps): Promise<Metadata>
|
|||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
title: article.title,
|
title: article.title,
|
||||||
description: article.excerpt,
|
description: cleanExcerpt(article.excerpt),
|
||||||
alternates: { canonical: `/articles/${article.slug}` },
|
alternates: { canonical: `/articles/${article.slug}` },
|
||||||
openGraph: {
|
openGraph: {
|
||||||
type: "article",
|
type: "article",
|
||||||
title: article.title,
|
title: article.title,
|
||||||
description: article.excerpt,
|
description: cleanExcerpt(article.excerpt),
|
||||||
url: `/articles/${article.slug}`,
|
url: `/articles/${article.slug}`,
|
||||||
images: [{ url: article.image, width: 1200, height: 630, alt: article.alt }],
|
images: [{ url: article.image, width: 1200, height: 630, alt: article.alt }],
|
||||||
authors: [article.author],
|
authors: [article.author],
|
||||||
@@ -68,7 +69,7 @@ export async function generateMetadata({ params }: PageProps): Promise<Metadata>
|
|||||||
twitter: {
|
twitter: {
|
||||||
card: "summary_large_image",
|
card: "summary_large_image",
|
||||||
title: article.title,
|
title: article.title,
|
||||||
description: article.excerpt,
|
description: cleanExcerpt(article.excerpt),
|
||||||
images: [article.image],
|
images: [article.image],
|
||||||
creator: "@AV Beat",
|
creator: "@AV Beat",
|
||||||
},
|
},
|
||||||
@@ -93,7 +94,7 @@ export default async function ArticlePage({ params }: PageProps) {
|
|||||||
"@context": "https://schema.org",
|
"@context": "https://schema.org",
|
||||||
"@type": "Article",
|
"@type": "Article",
|
||||||
headline: article.title,
|
headline: article.title,
|
||||||
description: article.excerpt,
|
description: cleanExcerpt(article.excerpt),
|
||||||
image: [absoluteImage(article.image)],
|
image: [absoluteImage(article.image)],
|
||||||
author: { "@type": "Person", name: article.author },
|
author: { "@type": "Person", name: article.author },
|
||||||
publisher: {
|
publisher: {
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
getRelatedForumThreads, type RelatedForumThread,
|
getRelatedForumThreads, type RelatedForumThread,
|
||||||
} from "@/lib/articles/legacy-source";
|
} from "@/lib/articles/legacy-source";
|
||||||
import { linkifyAndExtractMentions } from "@/lib/company-mentions";
|
import { linkifyAndExtractMentions } from "@/lib/company-mentions";
|
||||||
|
import { cleanExcerpt } from "@/lib/articles/excerpt";
|
||||||
import CompanyMentionsHover from "@/components/CompanyMentionsHover";
|
import CompanyMentionsHover from "@/components/CompanyMentionsHover";
|
||||||
import CompaniesInThisStory from "@/components/CompaniesInThisStory";
|
import CompaniesInThisStory from "@/components/CompaniesInThisStory";
|
||||||
import NewsArticleDetailClient from "./NewsArticleDetailClient";
|
import NewsArticleDetailClient from "./NewsArticleDetailClient";
|
||||||
@@ -58,14 +59,19 @@ export async function generateMetadata({ params }: PageProps): Promise<Metadata>
|
|||||||
if (!article) {
|
if (!article) {
|
||||||
return { title: "Article Not Found", description: "The article you are looking for does not exist." };
|
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 `<p>` in page source.
|
||||||
|
const cleanDesc = cleanExcerpt(article.excerpt);
|
||||||
return {
|
return {
|
||||||
title: `${article.title} — AV Beat`,
|
title: `${article.title} — AV Beat`,
|
||||||
description: article.excerpt,
|
description: cleanDesc,
|
||||||
alternates: { canonical: `/news/${article.slug}` },
|
alternates: { canonical: `/news/${article.slug}` },
|
||||||
openGraph: {
|
openGraph: {
|
||||||
type: "article",
|
type: "article",
|
||||||
title: article.title,
|
title: article.title,
|
||||||
description: article.excerpt,
|
description: cleanDesc,
|
||||||
url: `/news/${article.slug}`,
|
url: `/news/${article.slug}`,
|
||||||
images: [{ url: article.image, width: 1200, height: 630, alt: article.alt }],
|
images: [{ url: article.image, width: 1200, height: 630, alt: article.alt }],
|
||||||
authors: [article.author],
|
authors: [article.author],
|
||||||
@@ -75,7 +81,7 @@ export async function generateMetadata({ params }: PageProps): Promise<Metadata>
|
|||||||
twitter: {
|
twitter: {
|
||||||
card: "summary_large_image",
|
card: "summary_large_image",
|
||||||
title: article.title,
|
title: article.title,
|
||||||
description: article.excerpt,
|
description: cleanDesc,
|
||||||
images: [article.image],
|
images: [article.image],
|
||||||
creator: "@AV Beat",
|
creator: "@AV Beat",
|
||||||
},
|
},
|
||||||
@@ -106,7 +112,7 @@ export default async function NewsArticlePage({ params }: PageProps) {
|
|||||||
"@context": "https://schema.org",
|
"@context": "https://schema.org",
|
||||||
"@type": "NewsArticle",
|
"@type": "NewsArticle",
|
||||||
headline: article.title,
|
headline: article.title,
|
||||||
description: article.excerpt,
|
description: cleanExcerpt(article.excerpt),
|
||||||
image: [absoluteImage(article.image)],
|
image: [absoluteImage(article.image)],
|
||||||
author: { "@type": "Person", name: article.author },
|
author: { "@type": "Person", name: article.author },
|
||||||
publisher: {
|
publisher: {
|
||||||
|
|||||||
Reference in New Issue
Block a user