Skip to content
Merged
Show file tree
Hide file tree
Changes from 43 commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
d39be53
Fix site metadata audit findings
lawrencecchen Jul 11, 2026
7bfa15f
Merge remote-tracking branch 'origin/main' into feat-seo-site-audit
lawrencecchen Jul 11, 2026
85b9d5a
Harden localized metadata truncation
lawrencecchen Jul 11, 2026
710120a
Keep metadata on grapheme boundaries
lawrencecchen Jul 11, 2026
63bfbe0
Preserve localized home title context
lawrencecchen Jul 11, 2026
ea18e20
Preserve metadata source language
lawrencecchen Jul 11, 2026
3d6a523
Canonicalize untranslated SEO routes
lawrencecchen Jul 11, 2026
abcbf95
Keep fallback SEO redirects loop-free
lawrencecchen Jul 11, 2026
b803844
Preserve fallback locale negotiation
lawrencecchen Jul 11, 2026
7bbfe14
Keep legal metadata marketing-free
lawrencecchen Jul 11, 2026
4217eb3
Align SEO bounds and canonical links
lawrencecchen Jul 11, 2026
3be4b0f
Emit direct fallback content links
lawrencecchen Jul 11, 2026
2666930
Emit constrained content links directly
lawrencecchen Jul 11, 2026
e0a3c07
Preserve localized metadata boundaries
lawrencecchen Jul 11, 2026
852d77d
Select authored SEO metadata candidates
lawrencecchen Jul 11, 2026
1da54cf
Make SEO width checks deterministic
lawrencecchen Jul 11, 2026
3e0a647
Preserve localized SEO route identity
lawrencecchen Jul 11, 2026
b4d6a4b
Share localized metadata sentence joining
lawrencecchen Jul 11, 2026
5d3bb81
Prefer complete route-specific SEO prose
lawrencecchen Jul 11, 2026
d51d50c
Reject metadata lead-in fragments
lawrencecchen Jul 11, 2026
52659ea
Reject unresolved metadata placeholders
lawrencecchen Jul 11, 2026
ab6ce8f
Keep fallback metadata semantically complete
lawrencecchen Jul 11, 2026
0930130
Use complete localized SEO context
lawrencecchen Jul 11, 2026
a3faf51
Use complete localized SEO context
lawrencecchen Jul 11, 2026
44ac6d5
Preserve useful localized SEO fallbacks
lawrencecchen Jul 11, 2026
300afa6
Avoid invalid localized SEO fallbacks
lawrencecchen Jul 11, 2026
83f475e
Stabilize navigation mocks in web tests
lawrencecchen Jul 11, 2026
101d1ce
Preserve locale preference for fallback content
lawrencecchen Jul 11, 2026
a261994
Normalize fallback SEO paths
lawrencecchen Jul 11, 2026
0d6a266
test: cover residual audited SEO routes
lawrencecchen Jul 11, 2026
cde5ee9
Fix residual blog metadata from audit
lawrencecchen Jul 11, 2026
622f9bc
test: reject rich metadata candidates
lawrencecchen Jul 11, 2026
6fbb218
Keep audited metadata plain and canonical
lawrencecchen Jul 11, 2026
14b594d
Align blog SEO with complete prose candidates
lawrencecchen Jul 11, 2026
be2a4a2
Reject metadata list lead-ins
lawrencecchen Jul 11, 2026
51d4195
test: cover English-only blog discovery
lawrencecchen Jul 11, 2026
0fefa9c
Remove localized discovery for English-only posts
lawrencecchen Jul 11, 2026
fbbd4b4
Complete Bosnian Show HN metadata
lawrencecchen Jul 11, 2026
3b022d3
test: cover Claude Teams locale availability
lawrencecchen Jul 11, 2026
1c671e5
Hide untranslated Claude Teams blog routes
lawrencecchen Jul 11, 2026
48210f8
test: cover SSH blog locale availability
lawrencecchen Jul 11, 2026
17ee4df
Limit SSH blog routes to authored locales
lawrencecchen Jul 11, 2026
19455ea
Merge main into feat-seo-blog-audit
lawrencecchen Jul 12, 2026
e34583a
Merge remote-tracking branch 'origin/main' into feat-seo-blog-audit
lawrencecchen Jul 12, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 13 additions & 2 deletions web/app/[locale]/(landing)/blog/blog-schema.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ import {
articleSchema,
breadcrumbList,
} from "@/app/[locale]/components/json-ld";
import {
type AuditedBlogPostKey,
blogPostSeoCopy,
} from "@/i18n/audited-seo";

/**
* Article + BreadcrumbList JSON-LD for a blog post. Defaults to the post's
Expand All @@ -16,21 +20,28 @@ export function BlogSchema({
datePublished,
headline: headlineOverride,
description: descriptionOverride,
seoKey,
}: {
postKey: string;
path: string;
datePublished: string;
headline?: string;
description?: string;
seoKey?: AuditedBlogPostKey;
}) {
const tp = useTranslations(`blog.posts.${postKey}`);
const tm = useTranslations(`blog.${postKey}`);
const tl = useTranslations("landing.links");
const tn = useTranslations("nav");
const siteMeta = useTranslations("meta");
const locale = useLocale();

const headline = headlineOverride ?? tp("title");
const description = descriptionOverride ?? tm("metaDescription");
const auditedCopy = seoKey
? blogPostSeoCopy(locale, seoKey, tm, tp, siteMeta)
: undefined;
const headline = headlineOverride ?? auditedCopy?.title ?? tp("title");
const description =
descriptionOverride ?? auditedCopy?.description ?? tm("metaDescription");

return (
<>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { useTranslations } from "next-intl";
import { getTranslations } from "next-intl/server";
import { buildAlternates, openGraphDefaults, seoDescription, twitterSummary } from "@/i18n/seo";
import { buildAlternates, openGraphDefaults, twitterSummary } from "@/i18n/seo";
import { blogPostSeoCopy } from "@/i18n/audited-seo";
import { Link } from "@/i18n/navigation";
import { BlogSchema } from "../blog-schema";

Expand All @@ -14,15 +15,25 @@ export async function generateMetadata({
locale,
namespace: "blog.claudeCodeBestWorktreeManager",
});
const post = await getTranslations({
locale,
namespace: "blog.posts.claudeCodeBestWorktreeManager",
});
const siteMeta = await getTranslations({ locale, namespace: "meta" });
const rawKeywords = t.raw("metaKeywords");
const keywords = Array.isArray(rawKeywords)
? rawKeywords.filter((keyword): keyword is string => typeof keyword === "string")
: [];
const alternates = buildAlternates(locale, "/blog/claude-code-best-worktree-manager");
const title = t("metaTitle");
const description = seoDescription(locale, t("metaDescription"));
const { title, description } = blogPostSeoCopy(
locale,
"claudeCodeBestWorktreeManager",
t,
post,
siteMeta,
);
return {
title,
title: { absolute: title },
description,
keywords,
openGraph: {
Expand All @@ -45,6 +56,7 @@ export default function ClaudeCodeBestWorktreeManagerPage() {
<>
<BlogSchema
postKey="claudeCodeBestWorktreeManager"
seoKey="claudeCodeBestWorktreeManager"
path="/blog/claude-code-best-worktree-manager"
datePublished="2026-07-03T00:00:00Z"
/>
Expand Down
12 changes: 7 additions & 5 deletions web/app/[locale]/(landing)/blog/cmd-shift-u/page.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
import { useTranslations } from "next-intl";
import { getTranslations } from "next-intl/server";
import { buildAlternates, openGraphDefaults, seoDescription, twitterSummary } from "@/i18n/seo";
import { buildAlternates, openGraphDefaults, twitterSummary } from "@/i18n/seo";
import { blogPostSeoCopy } from "@/i18n/audited-seo";
import { BlogSchema } from "../blog-schema";
import { Link } from "@/i18n/navigation";

export async function generateMetadata({ params }: { params: Promise<{ locale: string }> }) {
const { locale } = await params;
const t = await getTranslations({ locale, namespace: "blog.cmdShiftU" });
const post = await getTranslations({ locale, namespace: "blog.posts.cmdShiftU" });
const siteMeta = await getTranslations({ locale, namespace: "meta" });
const alternates = buildAlternates(locale, "/blog/cmd-shift-u");
const title = t("metaTitle");
const description = seoDescription(locale, t("metaDescription"));
const { title, description } = blogPostSeoCopy(locale, "cmdShiftU", t, post, siteMeta);
return {
title,
title: { absolute: title },
description,
openGraph: {
...openGraphDefaults(locale, "article"),
Expand All @@ -31,7 +33,7 @@ export default function CmdShiftUPage() {

return (
<>
<BlogSchema postKey="cmdShiftU" path="/blog/cmd-shift-u" datePublished="2026-03-04T00:00:00Z" />
<BlogSchema postKey="cmdShiftU" seoKey="cmdShiftU" path="/blog/cmd-shift-u" datePublished="2026-03-04T00:00:00Z" />
<div className="mb-8">
<Link
href="/blog"
Expand Down
7 changes: 6 additions & 1 deletion web/app/[locale]/(landing)/blog/cmux-claude-teams/page.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
import { useTranslations } from "next-intl";
import { getTranslations } from "next-intl/server";
import { buildAlternates, openGraphDefaults, seoDescription, twitterSummary } from "@/i18n/seo";
import { englishFallbackContentLocales } from "@/i18n/locale-availability";
import { BlogSchema } from "../blog-schema";
import { Link } from "@/i18n/navigation";

export async function generateMetadata({ params }: { params: Promise<{ locale: string }> }) {
const { locale } = await params;
const t = await getTranslations({ locale, namespace: "blog.cmuxClaudeTeams" });
const alternates = buildAlternates(locale, "/blog/cmux-claude-teams");
const alternates = buildAlternates(
locale,
"/blog/cmux-claude-teams",
englishFallbackContentLocales,
);
const title = t("metaTitle");
const description = seoDescription(locale, t("metaDescription"));
return {
Expand Down
12 changes: 8 additions & 4 deletions web/app/[locale]/(landing)/blog/cmux-home/page.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { useTranslations } from "next-intl";
import { getTranslations } from "next-intl/server";
import { buildAlternates, openGraphDefaults, seoDescription, twitterSummary } from "@/i18n/seo";
import { buildAlternates, openGraphDefaults, twitterSummary } from "@/i18n/seo";
import { blogPostSeoCopy } from "@/i18n/audited-seo";
import { BlogSchema } from "../blog-schema";
import { Link } from "@/i18n/navigation";

export async function generateMetadata({
Expand All @@ -10,11 +12,12 @@ export async function generateMetadata({
}) {
const { locale } = await params;
const t = await getTranslations({ locale, namespace: "blog.cmuxHome" });
const post = await getTranslations({ locale, namespace: "blog.posts.cmuxHome" });
const siteMeta = await getTranslations({ locale, namespace: "meta" });
const alternates = buildAlternates(locale, "/blog/cmux-home");
const title = t("metaTitle");
const description = seoDescription(locale, t("metaDescription"));
const { title, description } = blogPostSeoCopy(locale, "cmuxHome", t, post, siteMeta);
return {
title,
title: { absolute: title },
description,
openGraph: {
...openGraphDefaults(locale, "article"),
Expand All @@ -34,6 +37,7 @@ export default function CmuxHomeBlogPage() {

return (
<>
<BlogSchema postKey="cmuxHome" seoKey="cmuxHome" path="/blog/cmux-home" datePublished="2026-06-23T00:00:00Z" />
<div className="mb-8">
<Link
href="/blog"
Expand Down
19 changes: 13 additions & 6 deletions web/app/[locale]/(landing)/blog/cmux-omo/page.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,24 @@
import { useTranslations } from "next-intl";
import { getTranslations } from "next-intl/server";
import { buildAlternates, openGraphDefaults, seoDescription, twitterSummary } from "@/i18n/seo";
import { buildAlternates, openGraphDefaults, twitterSummary } from "@/i18n/seo";
import { blogPostSeoCopy } from "@/i18n/audited-seo";
import { englishFallbackContentLocales } from "@/i18n/locale-availability";
import { BlogSchema } from "../blog-schema";
import { Link } from "@/i18n/navigation";

export async function generateMetadata({ params }: { params: Promise<{ locale: string }> }) {
const { locale } = await params;
const t = await getTranslations({ locale, namespace: "blog.cmuxOmo" });
const alternates = buildAlternates(locale, "/blog/cmux-omo");
const title = t("metaTitle");
const description = seoDescription(locale, t("metaDescription"));
const post = await getTranslations({ locale, namespace: "blog.posts.cmuxOmo" });
const siteMeta = await getTranslations({ locale, namespace: "meta" });
Comment thread
cursor[bot] marked this conversation as resolved.
const alternates = buildAlternates(
locale,
"/blog/cmux-omo",
englishFallbackContentLocales,
);
const { title, description } = blogPostSeoCopy(locale, "cmuxOmo", t, post, siteMeta);
return {
title,
title: { absolute: title },
description,
openGraph: {
...openGraphDefaults(locale, "article"),
Expand All @@ -31,7 +38,7 @@ export default function CmuxOmoPage() {

return (
<>
<BlogSchema postKey="cmuxOmo" path="/blog/cmux-omo" datePublished="2026-03-30T00:00:00Z" />
<BlogSchema postKey="cmuxOmo" seoKey="cmuxOmo" path="/blog/cmux-omo" datePublished="2026-03-30T00:00:00Z" />
<div className="mb-8">
<Link
href="/blog"
Expand Down
11 changes: 9 additions & 2 deletions web/app/[locale]/(landing)/blog/cmux-ssh/page.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,20 @@
import { getTranslations } from "next-intl/server";
import { hasFeatureWorkflowContent } from "@/i18n/locale-availability";
import {
fallbackContentLocales,
hasFeatureWorkflowContent,
} from "@/i18n/locale-availability";
import { buildAlternates, openGraphDefaults, seoDescription, twitterSummary } from "@/i18n/seo";
import { BlogSchema } from "../blog-schema";
import { Link } from "@/i18n/navigation";

export async function generateMetadata({ params }: { params: Promise<{ locale: string }> }) {
const { locale } = await params;
const t = await getTranslations({ locale, namespace: "blog.cmuxSsh" });
const alternates = buildAlternates(locale, "/blog/cmux-ssh");
const alternates = buildAlternates(
locale,
"/blog/cmux-ssh",
fallbackContentLocales,
);
const title = t("metaTitle");
const description = seoDescription(locale, t("metaDescription"));
return {
Expand Down
19 changes: 13 additions & 6 deletions web/app/[locale]/(landing)/blog/gpl/page.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,24 @@
import { useTranslations } from "next-intl";
import { getTranslations } from "next-intl/server";
import { buildAlternates, openGraphDefaults, seoDescription, twitterSummary } from "@/i18n/seo";
import { buildAlternates, openGraphDefaults, twitterSummary } from "@/i18n/seo";
import { blogPostSeoCopy } from "@/i18n/audited-seo";
import { englishFallbackContentLocales } from "@/i18n/locale-availability";
import { BlogSchema } from "../blog-schema";
import { Link } from "@/i18n/navigation";

export async function generateMetadata({ params }: { params: Promise<{ locale: string }> }) {
const { locale } = await params;
const t = await getTranslations({ locale, namespace: "blog.gpl" });
const alternates = buildAlternates(locale, "/blog/gpl");
const title = t("metaTitle");
const description = seoDescription(locale, t("metaDescription"));
const post = await getTranslations({ locale, namespace: "blog.posts.gpl" });
const siteMeta = await getTranslations({ locale, namespace: "meta" });
const alternates = buildAlternates(
locale,
"/blog/gpl",
englishFallbackContentLocales,
);
const { title, description } = blogPostSeoCopy(locale, "gpl", t, post, siteMeta);
return {
title,
title: { absolute: title },
description,
openGraph: {
...openGraphDefaults(locale, "article"),
Expand All @@ -31,7 +38,7 @@ export default function GplPage() {

return (
<>
<BlogSchema postKey="gpl" path="/blog/gpl" datePublished="2026-03-30T00:00:00Z" />
<BlogSchema postKey="gpl" seoKey="gpl" path="/blog/gpl" datePublished="2026-03-30T00:00:00Z" />
<div className="mb-8">
<Link
href="/blog"
Expand Down
12 changes: 7 additions & 5 deletions web/app/[locale]/(landing)/blog/introducing-cmux/page.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
import { useTranslations } from "next-intl";
import { getTranslations } from "next-intl/server";
import { buildAlternates, openGraphDefaults, seoDescription, twitterSummary } from "@/i18n/seo";
import { buildAlternates, openGraphDefaults, twitterSummary } from "@/i18n/seo";
import { blogPostSeoCopy } from "@/i18n/audited-seo";
import { BlogSchema } from "../blog-schema";
import { Link } from "@/i18n/navigation";

export async function generateMetadata({ params }: { params: Promise<{ locale: string }> }) {
const { locale } = await params;
const t = await getTranslations({ locale, namespace: "blog.introducingCmux" });
const post = await getTranslations({ locale, namespace: "blog.posts.introducingCmux" });
const siteMeta = await getTranslations({ locale, namespace: "meta" });
const alternates = buildAlternates(locale, "/blog/introducing-cmux");
const title = t("metaTitle");
const description = seoDescription(locale, t("metaDescription"));
const { title, description } = blogPostSeoCopy(locale, "introducingCmux", t, post, siteMeta);
return {
title,
title: { absolute: title },
description,
openGraph: {
...openGraphDefaults(locale, "article"),
Expand All @@ -31,7 +33,7 @@ export default function IntroducingCmuxPage() {

return (
<>
<BlogSchema postKey="introducingCmux" path="/blog/introducing-cmux" datePublished="2026-02-12T00:00:00Z" />
<BlogSchema postKey="introducingCmux" seoKey="introducingCmux" path="/blog/introducing-cmux" datePublished="2026-02-12T00:00:00Z" />
<div className="mb-8">
<Link
href="/blog"
Expand Down
6 changes: 4 additions & 2 deletions web/app/[locale]/(landing)/blog/page.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { useTranslations } from "next-intl";
import { useLocale, useTranslations } from "next-intl";
import { getTranslations } from "next-intl/server";
import { buildAlternates, openGraphDefaults, twitterSummary } from "@/i18n/seo";
import { blogIndexSeoCopy } from "@/i18n/audited-seo";
import { Link } from "@/i18n/navigation";
import { blogPosts } from "@/app/[locale]/components/blog-posts";
import { blogPostsForLocale } from "@/app/[locale]/components/blog-posts";

export async function generateMetadata({
params,
Expand Down Expand Up @@ -31,6 +31,8 @@ export async function generateMetadata({

export default function BlogPage() {
const t = useTranslations("blog");
const locale = useLocale();
const blogPosts = blogPostsForLocale(locale);

return (
<>
Expand Down
12 changes: 7 additions & 5 deletions web/app/[locale]/(landing)/blog/passkey-auth/page.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { useTranslations } from "next-intl";
import { getTranslations } from "next-intl/server";
import { buildAlternates, openGraphDefaults, seoDescription, twitterSummary } from "@/i18n/seo";
import { buildAlternates, openGraphDefaults, twitterSummary } from "@/i18n/seo";
import { blogPostSeoCopy } from "@/i18n/audited-seo";
import { BlogSchema } from "../blog-schema";
import { Link } from "@/i18n/navigation";
import { CodeBlock } from "@/app/[locale]/components/code-block";
Expand All @@ -12,15 +13,16 @@ export async function generateMetadata({
}) {
const { locale } = await params;
const t = await getTranslations({ locale, namespace: "blog.passkeyAuth" });
const post = await getTranslations({ locale, namespace: "blog.posts.passkeyAuth" });
const siteMeta = await getTranslations({ locale, namespace: "meta" });
const rawKeywords = t.raw("metaKeywords");
const keywords = Array.isArray(rawKeywords)
? rawKeywords.filter((keyword): keyword is string => typeof keyword === "string")
: [];
const alternates = buildAlternates(locale, "/blog/passkey-auth");
const title = t("metaTitle");
const description = seoDescription(locale, t("metaDescription"));
const { title, description } = blogPostSeoCopy(locale, "passkeyAuth", t, post, siteMeta);
return {
title,
title: { absolute: title },
description,
keywords,
openGraph: {
Expand All @@ -41,7 +43,7 @@ export default function PasskeyAuthPage() {

return (
<>
<BlogSchema postKey="passkeyAuth" path="/blog/passkey-auth" datePublished="2026-05-22T00:00:00Z" />
<BlogSchema postKey="passkeyAuth" seoKey="passkeyAuth" path="/blog/passkey-auth" datePublished="2026-05-22T00:00:00Z" />
<div className="mb-8">
<Link
href="/blog"
Expand Down
Loading