Skip to content

Commit 50e1e7b

Browse files
Fix residual blog metadata from Ahrefs audit (#7894)
* Fix site metadata audit findings * Harden localized metadata truncation * Keep metadata on grapheme boundaries * Preserve localized home title context * Preserve metadata source language * Canonicalize untranslated SEO routes * Keep fallback SEO redirects loop-free * Preserve fallback locale negotiation * Keep legal metadata marketing-free * Align SEO bounds and canonical links * Emit direct fallback content links * Emit constrained content links directly * Preserve localized metadata boundaries * Select authored SEO metadata candidates * Make SEO width checks deterministic * Preserve localized SEO route identity * Share localized metadata sentence joining * Prefer complete route-specific SEO prose * Reject metadata lead-in fragments * Reject unresolved metadata placeholders * Keep fallback metadata semantically complete * Use complete localized SEO context * Use complete localized SEO context * Preserve useful localized SEO fallbacks * Avoid invalid localized SEO fallbacks * Stabilize navigation mocks in web tests * Preserve locale preference for fallback content * Normalize fallback SEO paths * test: cover residual audited SEO routes * Fix residual blog metadata from audit * test: reject rich metadata candidates * Keep audited metadata plain and canonical * Align blog SEO with complete prose candidates * Reject metadata list lead-ins * test: cover English-only blog discovery * Remove localized discovery for English-only posts * Complete Bosnian Show HN metadata * test: cover Claude Teams locale availability * Hide untranslated Claude Teams blog routes * test: cover SSH blog locale availability * Limit SSH blog routes to authored locales
1 parent 690ca58 commit 50e1e7b

30 files changed

Lines changed: 609 additions & 215 deletions

File tree

web/app/[locale]/(landing)/blog/blog-schema.tsx

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@ import {
44
articleSchema,
55
breadcrumbList,
66
} from "@/app/[locale]/components/json-ld";
7+
import {
8+
type AuditedBlogPostKey,
9+
blogPostSeoCopy,
10+
} from "@/i18n/audited-seo";
711

812
/**
913
* Article + BreadcrumbList JSON-LD for a blog post. Defaults to the post's
@@ -16,21 +20,28 @@ export function BlogSchema({
1620
datePublished,
1721
headline: headlineOverride,
1822
description: descriptionOverride,
23+
seoKey,
1924
}: {
2025
postKey: string;
2126
path: string;
2227
datePublished: string;
2328
headline?: string;
2429
description?: string;
30+
seoKey?: AuditedBlogPostKey;
2531
}) {
2632
const tp = useTranslations(`blog.posts.${postKey}`);
2733
const tm = useTranslations(`blog.${postKey}`);
2834
const tl = useTranslations("landing.links");
2935
const tn = useTranslations("nav");
36+
const siteMeta = useTranslations("meta");
3037
const locale = useLocale();
3138

32-
const headline = headlineOverride ?? tp("title");
33-
const description = descriptionOverride ?? tm("metaDescription");
39+
const auditedCopy = seoKey
40+
? blogPostSeoCopy(locale, seoKey, tm, tp, siteMeta)
41+
: undefined;
42+
const headline = headlineOverride ?? auditedCopy?.title ?? tp("title");
43+
const description =
44+
descriptionOverride ?? auditedCopy?.description ?? tm("metaDescription");
3445

3546
return (
3647
<>

web/app/[locale]/(landing)/blog/claude-code-best-worktree-manager/page.tsx

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { useTranslations } from "next-intl";
22
import { getTranslations } from "next-intl/server";
3-
import { buildAlternates, openGraphDefaults, seoDescription, twitterSummary } from "@/i18n/seo";
3+
import { buildAlternates, openGraphDefaults, twitterSummary } from "@/i18n/seo";
4+
import { blogPostSeoCopy } from "@/i18n/audited-seo";
45
import { Link } from "@/i18n/navigation";
56
import { BlogSchema } from "../blog-schema";
67

@@ -14,15 +15,25 @@ export async function generateMetadata({
1415
locale,
1516
namespace: "blog.claudeCodeBestWorktreeManager",
1617
});
18+
const post = await getTranslations({
19+
locale,
20+
namespace: "blog.posts.claudeCodeBestWorktreeManager",
21+
});
22+
const siteMeta = await getTranslations({ locale, namespace: "meta" });
1723
const rawKeywords = t.raw("metaKeywords");
1824
const keywords = Array.isArray(rawKeywords)
1925
? rawKeywords.filter((keyword): keyword is string => typeof keyword === "string")
2026
: [];
2127
const alternates = buildAlternates(locale, "/blog/claude-code-best-worktree-manager");
22-
const title = t("metaTitle");
23-
const description = seoDescription(locale, t("metaDescription"));
28+
const { title, description } = blogPostSeoCopy(
29+
locale,
30+
"claudeCodeBestWorktreeManager",
31+
t,
32+
post,
33+
siteMeta,
34+
);
2435
return {
25-
title,
36+
title: { absolute: title },
2637
description,
2738
keywords,
2839
openGraph: {
@@ -45,6 +56,7 @@ export default function ClaudeCodeBestWorktreeManagerPage() {
4556
<>
4657
<BlogSchema
4758
postKey="claudeCodeBestWorktreeManager"
59+
seoKey="claudeCodeBestWorktreeManager"
4860
path="/blog/claude-code-best-worktree-manager"
4961
datePublished="2026-07-03T00:00:00Z"
5062
/>

web/app/[locale]/(landing)/blog/cmd-shift-u/page.tsx

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,19 @@
11
import { useTranslations } from "next-intl";
22
import { getTranslations } from "next-intl/server";
3-
import { buildAlternates, openGraphDefaults, seoDescription, twitterSummary } from "@/i18n/seo";
3+
import { buildAlternates, openGraphDefaults, twitterSummary } from "@/i18n/seo";
4+
import { blogPostSeoCopy } from "@/i18n/audited-seo";
45
import { BlogSchema } from "../blog-schema";
56
import { Link } from "@/i18n/navigation";
67

78
export async function generateMetadata({ params }: { params: Promise<{ locale: string }> }) {
89
const { locale } = await params;
910
const t = await getTranslations({ locale, namespace: "blog.cmdShiftU" });
11+
const post = await getTranslations({ locale, namespace: "blog.posts.cmdShiftU" });
12+
const siteMeta = await getTranslations({ locale, namespace: "meta" });
1013
const alternates = buildAlternates(locale, "/blog/cmd-shift-u");
11-
const title = t("metaTitle");
12-
const description = seoDescription(locale, t("metaDescription"));
14+
const { title, description } = blogPostSeoCopy(locale, "cmdShiftU", t, post, siteMeta);
1315
return {
14-
title,
16+
title: { absolute: title },
1517
description,
1618
openGraph: {
1719
...openGraphDefaults(locale, "article"),
@@ -31,7 +33,7 @@ export default function CmdShiftUPage() {
3133

3234
return (
3335
<>
34-
<BlogSchema postKey="cmdShiftU" path="/blog/cmd-shift-u" datePublished="2026-03-04T00:00:00Z" />
36+
<BlogSchema postKey="cmdShiftU" seoKey="cmdShiftU" path="/blog/cmd-shift-u" datePublished="2026-03-04T00:00:00Z" />
3537
<div className="mb-8">
3638
<Link
3739
href="/blog"

web/app/[locale]/(landing)/blog/cmux-claude-teams/page.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,18 @@
11
import { useTranslations } from "next-intl";
22
import { getTranslations } from "next-intl/server";
33
import { buildAlternates, openGraphDefaults, seoDescription, twitterSummary } from "@/i18n/seo";
4+
import { englishFallbackContentLocales } from "@/i18n/locale-availability";
45
import { BlogSchema } from "../blog-schema";
56
import { Link } from "@/i18n/navigation";
67

78
export async function generateMetadata({ params }: { params: Promise<{ locale: string }> }) {
89
const { locale } = await params;
910
const t = await getTranslations({ locale, namespace: "blog.cmuxClaudeTeams" });
10-
const alternates = buildAlternates(locale, "/blog/cmux-claude-teams");
11+
const alternates = buildAlternates(
12+
locale,
13+
"/blog/cmux-claude-teams",
14+
englishFallbackContentLocales,
15+
);
1116
const title = t("metaTitle");
1217
const description = seoDescription(locale, t("metaDescription"));
1318
return {

web/app/[locale]/(landing)/blog/cmux-home/page.tsx

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import { useTranslations } from "next-intl";
22
import { getTranslations } from "next-intl/server";
3-
import { buildAlternates, openGraphDefaults, seoDescription, twitterSummary } from "@/i18n/seo";
3+
import { buildAlternates, openGraphDefaults, twitterSummary } from "@/i18n/seo";
4+
import { blogPostSeoCopy } from "@/i18n/audited-seo";
5+
import { BlogSchema } from "../blog-schema";
46
import { Link } from "@/i18n/navigation";
57

68
export async function generateMetadata({
@@ -10,11 +12,12 @@ export async function generateMetadata({
1012
}) {
1113
const { locale } = await params;
1214
const t = await getTranslations({ locale, namespace: "blog.cmuxHome" });
15+
const post = await getTranslations({ locale, namespace: "blog.posts.cmuxHome" });
16+
const siteMeta = await getTranslations({ locale, namespace: "meta" });
1317
const alternates = buildAlternates(locale, "/blog/cmux-home");
14-
const title = t("metaTitle");
15-
const description = seoDescription(locale, t("metaDescription"));
18+
const { title, description } = blogPostSeoCopy(locale, "cmuxHome", t, post, siteMeta);
1619
return {
17-
title,
20+
title: { absolute: title },
1821
description,
1922
openGraph: {
2023
...openGraphDefaults(locale, "article"),
@@ -34,6 +37,7 @@ export default function CmuxHomeBlogPage() {
3437

3538
return (
3639
<>
40+
<BlogSchema postKey="cmuxHome" seoKey="cmuxHome" path="/blog/cmux-home" datePublished="2026-06-23T00:00:00Z" />
3741
<div className="mb-8">
3842
<Link
3943
href="/blog"

web/app/[locale]/(landing)/blog/cmux-omo/page.tsx

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,24 @@
11
import { useTranslations } from "next-intl";
22
import { getTranslations } from "next-intl/server";
3-
import { buildAlternates, openGraphDefaults, seoDescription, twitterSummary } from "@/i18n/seo";
3+
import { buildAlternates, openGraphDefaults, twitterSummary } from "@/i18n/seo";
4+
import { blogPostSeoCopy } from "@/i18n/audited-seo";
5+
import { englishFallbackContentLocales } from "@/i18n/locale-availability";
46
import { BlogSchema } from "../blog-schema";
57
import { Link } from "@/i18n/navigation";
68

79
export async function generateMetadata({ params }: { params: Promise<{ locale: string }> }) {
810
const { locale } = await params;
911
const t = await getTranslations({ locale, namespace: "blog.cmuxOmo" });
10-
const alternates = buildAlternates(locale, "/blog/cmux-omo");
11-
const title = t("metaTitle");
12-
const description = seoDescription(locale, t("metaDescription"));
12+
const post = await getTranslations({ locale, namespace: "blog.posts.cmuxOmo" });
13+
const siteMeta = await getTranslations({ locale, namespace: "meta" });
14+
const alternates = buildAlternates(
15+
locale,
16+
"/blog/cmux-omo",
17+
englishFallbackContentLocales,
18+
);
19+
const { title, description } = blogPostSeoCopy(locale, "cmuxOmo", t, post, siteMeta);
1320
return {
14-
title,
21+
title: { absolute: title },
1522
description,
1623
openGraph: {
1724
...openGraphDefaults(locale, "article"),
@@ -31,7 +38,7 @@ export default function CmuxOmoPage() {
3138

3239
return (
3340
<>
34-
<BlogSchema postKey="cmuxOmo" path="/blog/cmux-omo" datePublished="2026-03-30T00:00:00Z" />
41+
<BlogSchema postKey="cmuxOmo" seoKey="cmuxOmo" path="/blog/cmux-omo" datePublished="2026-03-30T00:00:00Z" />
3542
<div className="mb-8">
3643
<Link
3744
href="/blog"

web/app/[locale]/(landing)/blog/cmux-ssh/page.tsx

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,20 @@
11
import { getTranslations } from "next-intl/server";
2-
import { hasFeatureWorkflowContent } from "@/i18n/locale-availability";
2+
import {
3+
fallbackContentLocales,
4+
hasFeatureWorkflowContent,
5+
} from "@/i18n/locale-availability";
36
import { buildAlternates, openGraphDefaults, seoDescription, twitterSummary } from "@/i18n/seo";
47
import { BlogSchema } from "../blog-schema";
58
import { Link } from "@/i18n/navigation";
69

710
export async function generateMetadata({ params }: { params: Promise<{ locale: string }> }) {
811
const { locale } = await params;
912
const t = await getTranslations({ locale, namespace: "blog.cmuxSsh" });
10-
const alternates = buildAlternates(locale, "/blog/cmux-ssh");
13+
const alternates = buildAlternates(
14+
locale,
15+
"/blog/cmux-ssh",
16+
fallbackContentLocales,
17+
);
1118
const title = t("metaTitle");
1219
const description = seoDescription(locale, t("metaDescription"));
1320
return {

web/app/[locale]/(landing)/blog/gpl/page.tsx

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,24 @@
11
import { useTranslations } from "next-intl";
22
import { getTranslations } from "next-intl/server";
3-
import { buildAlternates, openGraphDefaults, seoDescription, twitterSummary } from "@/i18n/seo";
3+
import { buildAlternates, openGraphDefaults, twitterSummary } from "@/i18n/seo";
4+
import { blogPostSeoCopy } from "@/i18n/audited-seo";
5+
import { englishFallbackContentLocales } from "@/i18n/locale-availability";
46
import { BlogSchema } from "../blog-schema";
57
import { Link } from "@/i18n/navigation";
68

79
export async function generateMetadata({ params }: { params: Promise<{ locale: string }> }) {
810
const { locale } = await params;
911
const t = await getTranslations({ locale, namespace: "blog.gpl" });
10-
const alternates = buildAlternates(locale, "/blog/gpl");
11-
const title = t("metaTitle");
12-
const description = seoDescription(locale, t("metaDescription"));
12+
const post = await getTranslations({ locale, namespace: "blog.posts.gpl" });
13+
const siteMeta = await getTranslations({ locale, namespace: "meta" });
14+
const alternates = buildAlternates(
15+
locale,
16+
"/blog/gpl",
17+
englishFallbackContentLocales,
18+
);
19+
const { title, description } = blogPostSeoCopy(locale, "gpl", t, post, siteMeta);
1320
return {
14-
title,
21+
title: { absolute: title },
1522
description,
1623
openGraph: {
1724
...openGraphDefaults(locale, "article"),
@@ -31,7 +38,7 @@ export default function GplPage() {
3138

3239
return (
3340
<>
34-
<BlogSchema postKey="gpl" path="/blog/gpl" datePublished="2026-03-30T00:00:00Z" />
41+
<BlogSchema postKey="gpl" seoKey="gpl" path="/blog/gpl" datePublished="2026-03-30T00:00:00Z" />
3542
<div className="mb-8">
3643
<Link
3744
href="/blog"

web/app/[locale]/(landing)/blog/introducing-cmux/page.tsx

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,19 @@
11
import { useTranslations } from "next-intl";
22
import { getTranslations } from "next-intl/server";
3-
import { buildAlternates, openGraphDefaults, seoDescription, twitterSummary } from "@/i18n/seo";
3+
import { buildAlternates, openGraphDefaults, twitterSummary } from "@/i18n/seo";
4+
import { blogPostSeoCopy } from "@/i18n/audited-seo";
45
import { BlogSchema } from "../blog-schema";
56
import { Link } from "@/i18n/navigation";
67

78
export async function generateMetadata({ params }: { params: Promise<{ locale: string }> }) {
89
const { locale } = await params;
910
const t = await getTranslations({ locale, namespace: "blog.introducingCmux" });
11+
const post = await getTranslations({ locale, namespace: "blog.posts.introducingCmux" });
12+
const siteMeta = await getTranslations({ locale, namespace: "meta" });
1013
const alternates = buildAlternates(locale, "/blog/introducing-cmux");
11-
const title = t("metaTitle");
12-
const description = seoDescription(locale, t("metaDescription"));
14+
const { title, description } = blogPostSeoCopy(locale, "introducingCmux", t, post, siteMeta);
1315
return {
14-
title,
16+
title: { absolute: title },
1517
description,
1618
openGraph: {
1719
...openGraphDefaults(locale, "article"),
@@ -31,7 +33,7 @@ export default function IntroducingCmuxPage() {
3133

3234
return (
3335
<>
34-
<BlogSchema postKey="introducingCmux" path="/blog/introducing-cmux" datePublished="2026-02-12T00:00:00Z" />
36+
<BlogSchema postKey="introducingCmux" seoKey="introducingCmux" path="/blog/introducing-cmux" datePublished="2026-02-12T00:00:00Z" />
3537
<div className="mb-8">
3638
<Link
3739
href="/blog"

web/app/[locale]/(landing)/blog/page.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
import { useTranslations } from "next-intl";
1+
import { useLocale, useTranslations } from "next-intl";
22
import { getTranslations } from "next-intl/server";
33
import { buildAlternates, openGraphDefaults, twitterSummary } from "@/i18n/seo";
44
import { blogIndexSeoCopy } from "@/i18n/audited-seo";
55
import { Link } from "@/i18n/navigation";
6-
import { blogPosts } from "@/app/[locale]/components/blog-posts";
6+
import { blogPostsForLocale } from "@/app/[locale]/components/blog-posts";
77

88
export async function generateMetadata({
99
params,
@@ -31,6 +31,8 @@ export async function generateMetadata({
3131

3232
export default function BlogPage() {
3333
const t = useTranslations("blog");
34+
const locale = useLocale();
35+
const blogPosts = blogPostsForLocale(locale);
3436

3537
return (
3638
<>

0 commit comments

Comments
 (0)