Skip to content
2 changes: 1 addition & 1 deletion web/app/[locale]/(legal)/eula/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import type { Metadata } from "next";
export const metadata: Metadata = {
title: "EULA — cmux",
description: "End-User License Agreement for cmux",
alternates: { canonical: "./" },
alternates: { canonical: "https://cmux.com/eula" },

@cubic-dev-ai cubic-dev-ai Bot Mar 26, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: This metadata hardcodes an English canonical and omits hreflang alternates on a locale route, which can cause non-English EULA URLs to canonicalize incorrectly and lose locale signals.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At web/app/[locale]/(legal)/eula/page.tsx, line 6:

<comment>This metadata hardcodes an English canonical and omits hreflang alternates on a locale route, which can cause non-English EULA URLs to canonicalize incorrectly and lose locale signals.</comment>

<file context>
@@ -1,13 +1,10 @@
+export const metadata: Metadata = {
+  title: "EULA — cmux",
+  description: "End-User License Agreement for cmux",
+  alternates: { canonical: "https://cmux.com/eula" },
+};
 
</file context>
Fix with Cubic

};

export default function EulaPage() {
Expand Down
2 changes: 1 addition & 1 deletion web/app/[locale]/(legal)/privacy-policy/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { Link } from "../../../../i18n/navigation";
export const metadata: Metadata = {
title: "Privacy Policy — cmux",
description: "Privacy policy for cmux",
alternates: { canonical: "./" },
alternates: { canonical: "https://cmux.com/privacy-policy" },

@cubic-dev-ai cubic-dev-ai Bot Mar 26, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: The new static alternates value removes locale-aware canonical/hreflang metadata for this [locale] route. This can cause locale pages to share one canonical and lose hreflang signals.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At web/app/[locale]/(legal)/privacy-policy/page.tsx, line 7:

<comment>The new static `alternates` value removes locale-aware canonical/hreflang metadata for this `[locale]` route. This can cause locale pages to share one canonical and lose hreflang signals.</comment>

<file context>
@@ -1,14 +1,11 @@
+export const metadata: Metadata = {
+  title: "Privacy Policy — cmux",
+  description: "Privacy policy for cmux",
+  alternates: { canonical: "https://cmux.com/privacy-policy" },
+};
 
</file context>
Fix with Cubic

};

export default function PrivacyPolicyPage() {
Expand Down
2 changes: 1 addition & 1 deletion web/app/[locale]/(legal)/terms-of-service/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import type { Metadata } from "next";
export const metadata: Metadata = {
title: "Terms of Service — cmux",
description: "Terms of service for cmux",
alternates: { canonical: "./" },
alternates: { canonical: "https://cmux.com/terms-of-service" },

@cubic-dev-ai cubic-dev-ai Bot Mar 26, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: This locale route now uses a single hardcoded canonical URL, which regresses locale-aware SEO signals. Generate metadata from locale and use buildAlternates so each locale has the correct canonical/hreflang set.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At web/app/[locale]/(legal)/terms-of-service/page.tsx, line 6:

<comment>This locale route now uses a single hardcoded canonical URL, which regresses locale-aware SEO signals. Generate metadata from `locale` and use `buildAlternates` so each locale has the correct canonical/hreflang set.</comment>

<file context>
@@ -1,13 +1,10 @@
+export const metadata: Metadata = {
+  title: "Terms of Service — cmux",
+  description: "Terms of service for cmux",
+  alternates: { canonical: "https://cmux.com/terms-of-service" },
+};
 
</file context>
Fix with Cubic

};

export default function TermsOfServicePage() {
Expand Down
5 changes: 2 additions & 3 deletions web/app/[locale]/blog/cmd-shift-u/page.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { useTranslations } from "next-intl";
import { getTranslations } from "next-intl/server";
import { buildAlternates } from "../../../../i18n/seo";
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 url = locale === "en" ? "/blog/cmd-shift-u" : `/${locale}/blog/cmd-shift-u`;
return {
title: t("metaTitle"),
description: t("metaDescription"),
Expand All @@ -18,14 +18,13 @@ export async function generateMetadata({ params }: { params: Promise<{ locale: s
description: t("metaDescription"),
type: "article",
publishedTime: "2026-03-04T00:00:00Z",
url,
},
twitter: {
card: "summary_large_image",
title: t("metaTitle"),
description: t("metaDescription"),
},
alternates: { canonical: url },
alternates: buildAlternates(locale, "/blog/cmd-shift-u"),
};
}

Expand Down
5 changes: 2 additions & 3 deletions web/app/[locale]/blog/introducing-cmux/page.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { useTranslations } from "next-intl";
import { getTranslations } from "next-intl/server";
import { buildAlternates } from "../../../../i18n/seo";
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 url = locale === "en" ? "/blog/introducing-cmux" : `/${locale}/blog/introducing-cmux`;
return {
title: t("metaTitle"),
description: t("metaDescription"),
Expand All @@ -18,14 +18,13 @@ export async function generateMetadata({ params }: { params: Promise<{ locale: s
description: t("metaDescription"),
type: "article",
publishedTime: "2026-02-12T00:00:00Z",
url,
},
twitter: {
card: "summary_large_image",
title: t("metaTitle"),
description: t("metaDescription"),
},
alternates: { canonical: url },
alternates: buildAlternates(locale, "/blog/introducing-cmux"),
};
}

Expand Down
5 changes: 2 additions & 3 deletions web/app/[locale]/blog/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { getTranslations } from "next-intl/server";
import { buildAlternates } from "../../../i18n/seo";
import { SiteHeader } from "../components/site-header";
import { BlogPager } from "../components/blog-pager";
import { BlogCTA } from "../components/blog-cta";
Expand All @@ -19,9 +20,7 @@ export async function generateMetadata({
siteName: "cmux",
type: "article" as const,
},
alternates: {
canonical: "./",
},
alternates: buildAlternates(locale, "/blog"),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Blog layout canonical collides with individual post metadata

The blog layout emits alternates: buildAlternates(locale, "/blog"). Because individual blog post pages do define their own alternates they correctly override this. The /blog list page will also be correct since it has no page-level generateMetadata. This is fine — just noting it here for contrast with the docs situation above, where sub-pages do not override the layout canonical.

};
}

Expand Down
2 changes: 2 additions & 0 deletions web/app/[locale]/blog/page.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { useTranslations } from "next-intl";
import { getTranslations } from "next-intl/server";
import { buildAlternates } from "../../../i18n/seo";
import { Link } from "../../../i18n/navigation";

export async function generateMetadata({
Expand All @@ -12,6 +13,7 @@ export async function generateMetadata({
return {
title: t("metaTitle"),
description: t("metaDescription"),
alternates: buildAlternates(locale, "/blog"),
};
}

Expand Down
5 changes: 2 additions & 3 deletions web/app/[locale]/blog/show-hn-launch/page.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import Image from "next/image";
import { useTranslations } from "next-intl";
import { getTranslations } from "next-intl/server";
import { buildAlternates } from "../../../../i18n/seo";
import { Link } from "../../../../i18n/navigation";
import { Tweet } from "react-tweet";
import starHistory from "./star-history.png";

export async function generateMetadata({ params }: { params: Promise<{ locale: string }> }) {
const { locale } = await params;
const t = await getTranslations({ locale, namespace: "blog.showHnLaunch" });
const url = locale === "en" ? "/blog/show-hn-launch" : `/${locale}/blog/show-hn-launch`;
return {
title: t("metaTitle"),
description: t("metaDescription"),
Expand All @@ -22,14 +22,13 @@ export async function generateMetadata({ params }: { params: Promise<{ locale: s
description: t("metaDescription"),
type: "article",
publishedTime: "2026-02-21T00:00:00Z",
url,
},
twitter: {
card: "summary_large_image",
title: t("metaTitle"),
description: t("metaDescription"),
},
alternates: { canonical: url },
alternates: buildAlternates(locale, "/blog/show-hn-launch"),
};
}

Expand Down
5 changes: 2 additions & 3 deletions web/app/[locale]/blog/zen-of-cmux/page.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { useTranslations } from "next-intl";
import { getTranslations } from "next-intl/server";
import { buildAlternates } from "../../../../i18n/seo";
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.zenOfCmux" });
const url = locale === "en" ? "/blog/zen-of-cmux" : `/${locale}/blog/zen-of-cmux`;
return {
title: t("metaTitle"),
description: t("metaDescription"),
Expand All @@ -18,14 +18,13 @@ export async function generateMetadata({ params }: { params: Promise<{ locale: s
description: t("metaDescription"),
type: "article",
publishedTime: "2026-02-27T00:00:00Z",
url,
},
twitter: {
card: "summary_large_image",
title: t("metaTitle"),
description: t("metaDescription"),
},
alternates: { canonical: url },
alternates: buildAlternates(locale, "/blog/zen-of-cmux"),
};
}

Expand Down
3 changes: 2 additions & 1 deletion web/app/[locale]/community/page.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { useTranslations } from "next-intl";
import { getTranslations } from "next-intl/server";
import { buildAlternates } from "../../../i18n/seo";
import { SiteHeader } from "../components/site-header";

export async function generateMetadata({ params }: { params: Promise<{ locale: string }> }) {
Expand All @@ -8,7 +9,7 @@ export async function generateMetadata({ params }: { params: Promise<{ locale: s
return {
title: t("metaTitle"),
description: t("metaDescription"),
alternates: { canonical: "./" },
alternates: buildAlternates(locale, "/community"),
};
}

Expand Down
2 changes: 2 additions & 0 deletions web/app/[locale]/docs/api/page.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { useTranslations } from "next-intl";
import { getTranslations } from "next-intl/server";
import { buildAlternates } from "../../../../i18n/seo";
import { CodeBlock } from "../../components/code-block";
import { Callout } from "../../components/callout";

Expand All @@ -9,6 +10,7 @@ export async function generateMetadata({ params }: { params: Promise<{ locale: s
return {
title: t("metaTitle"),
description: t("metaDescription"),
alternates: buildAlternates(locale, "/docs/api"),
};
}

Expand Down
2 changes: 2 additions & 0 deletions web/app/[locale]/docs/browser-automation/page.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { useTranslations } from "next-intl";
import { getTranslations } from "next-intl/server";
import { buildAlternates } from "../../../../i18n/seo";
import { CodeBlock } from "../../components/code-block";

export async function generateMetadata({ params }: { params: Promise<{ locale: string }> }) {
Expand All @@ -8,6 +9,7 @@ export async function generateMetadata({ params }: { params: Promise<{ locale: s
return {
title: t("metaTitle"),
description: t("metaDescription"),
alternates: buildAlternates(locale, "/docs/browser-automation"),
};
}

Expand Down
2 changes: 2 additions & 0 deletions web/app/[locale]/docs/changelog/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import path from "path";
import Image from "next/image";
import { useTranslations } from "next-intl";
import { getTranslations } from "next-intl/server";
import { buildAlternates } from "../../../../i18n/seo";
import { changelogMedia, type VersionMedia } from "./changelog-media";

/** Read PNG dimensions from the IHDR chunk (bytes 16-23). */
Expand All @@ -21,6 +22,7 @@ export async function generateMetadata({ params }: { params: Promise<{ locale: s
return {
title: t("metaTitle"),
description: t("metaDescription"),
alternates: buildAlternates(locale, "/docs/changelog"),
};
}

Expand Down
2 changes: 2 additions & 0 deletions web/app/[locale]/docs/concepts/page.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { useTranslations } from "next-intl";
import { getTranslations } from "next-intl/server";
import { buildAlternates } from "../../../../i18n/seo";
import { CodeBlock } from "../../components/code-block";

export async function generateMetadata({ params }: { params: Promise<{ locale: string }> }) {
Expand All @@ -8,6 +9,7 @@ export async function generateMetadata({ params }: { params: Promise<{ locale: s
return {
title: t("metaTitle"),
description: t("metaDescription"),
alternates: buildAlternates(locale, "/docs/concepts"),
};
}

Expand Down
2 changes: 2 additions & 0 deletions web/app/[locale]/docs/configuration/page.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { useTranslations } from "next-intl";
import { getTranslations } from "next-intl/server";
import { buildAlternates } from "../../../../i18n/seo";
import { CodeBlock } from "../../components/code-block";
import { Callout } from "../../components/callout";

Expand All @@ -9,6 +10,7 @@ export async function generateMetadata({ params }: { params: Promise<{ locale: s
return {
title: t("metaTitle"),
description: t("metaDescription"),
alternates: buildAlternates(locale, "/docs/configuration"),
};
}

Expand Down
2 changes: 2 additions & 0 deletions web/app/[locale]/docs/custom-commands/page.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { useTranslations } from "next-intl";
import { getTranslations } from "next-intl/server";
import { buildAlternates } from "../../../../i18n/seo";
import { CodeBlock } from "../../components/code-block";
import { Callout } from "../../components/callout";

Expand All @@ -9,6 +10,7 @@ export async function generateMetadata({ params }: { params: Promise<{ locale: s
return {
title: t("metaTitle"),
description: t("metaDescription"),
alternates: buildAlternates(locale, "/docs/custom-commands"),
};
}

Expand Down
2 changes: 2 additions & 0 deletions web/app/[locale]/docs/getting-started/page.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { useTranslations } from "next-intl";
import { getTranslations } from "next-intl/server";
import { buildAlternates } from "../../../../i18n/seo";
import { CodeBlock } from "../../components/code-block";
import { Callout } from "../../components/callout";
import { DownloadButton } from "../../components/download-button";
Expand All @@ -10,6 +11,7 @@ export async function generateMetadata({ params }: { params: Promise<{ locale: s
return {
title: t("metaTitle"),
description: t("metaDescription"),
alternates: buildAlternates(locale, "/docs/getting-started"),
};
}

Expand Down
2 changes: 2 additions & 0 deletions web/app/[locale]/docs/keyboard-shortcuts/page.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { useTranslations } from "next-intl";
import { getTranslations } from "next-intl/server";
import { buildAlternates } from "../../../../i18n/seo";
import { KeyboardShortcuts } from "../../keyboard-shortcuts";

export async function generateMetadata({ params }: { params: Promise<{ locale: string }> }) {
Expand All @@ -8,6 +9,7 @@ export async function generateMetadata({ params }: { params: Promise<{ locale: s
return {
title: t("metaTitle"),
description: t("metaDescription"),
alternates: buildAlternates(locale, "/docs/keyboard-shortcuts"),
};
}

Expand Down
5 changes: 2 additions & 3 deletions web/app/[locale]/docs/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { getTranslations } from "next-intl/server";
import { buildAlternates } from "../../../i18n/seo";
import { DocsNav } from "./docs-nav";
import { SiteHeader } from "../components/site-header";

Expand All @@ -18,9 +19,7 @@ export async function generateMetadata({
siteName: "cmux",
type: "article" as const,
},
alternates: {
canonical: "./",
},
alternates: buildAlternates(locale, "/docs"),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve per-page canonicals in docs layout

Setting alternates to buildAlternates(locale, "/docs") at the layout level makes every docs descendant (for example /docs/getting-started, /docs/api, etc.) emit canonical/hreflang URLs for /docs, because those pages do not define their own alternates. This causes multiple distinct docs pages to advertise the same canonical target, which can collapse indexing and undo the SEO fix for the docs section.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Docs sub-pages inherit wrong canonical from layout

The docs layout sets alternates: buildAlternates(locale, "/docs"), which Next.js App Router will merge into any docs page that doesn't return its own alternates. Looking at the individual docs pages (getting-started/page.tsx, configuration/page.tsx, api/page.tsx, etc.), they each define generateMetadata returning only { title, description } — no alternates. Next.js shallow-merges metadata up the segment tree, so all those sub-pages will inherit canonical: "https://cmux.com/docs" instead of their own URL.

Before this PR the layout used canonical: "./", which resolved against metadataBase: "https://cmux.com" to the homepage — also wrong. This PR improves it (now points to /docs rather than /), but the sub-pages still don't get self-referencing canonicals, which was one of the primary goals listed in the PR description.

To fully fix this, each docs page's generateMetadata should include its own buildAlternates call:

// e.g. getting-started/page.tsx
alternates: buildAlternates(locale, "/docs/getting-started"),

The same gap applies to concepts, configuration, api, notifications, changelog, keyboard-shortcuts, browser-automation, and custom-commands.

};
}

Expand Down
2 changes: 2 additions & 0 deletions web/app/[locale]/docs/notifications/page.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { useTranslations } from "next-intl";
import { getTranslations } from "next-intl/server";
import { buildAlternates } from "../../../../i18n/seo";
import { CodeBlock } from "../../components/code-block";
import { Callout } from "../../components/callout";

Expand All @@ -9,6 +10,7 @@ export async function generateMetadata({ params }: { params: Promise<{ locale: s
return {
title: t("metaTitle"),
description: t("metaDescription"),
alternates: buildAlternates(locale, "/docs/notifications"),
};
}

Expand Down
7 changes: 4 additions & 3 deletions web/app/[locale]/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
} from "next-intl/server";
import { notFound } from "next/navigation";
import { routing } from "../../i18n/routing";
import { buildAlternates } from "../../i18n/seo";
import { Providers } from "./providers";
import { DevPanel } from "./components/spacing-control";
import { SiteFooter } from "./components/site-footer";
Expand All @@ -30,8 +31,7 @@ export async function generateMetadata({
}): Promise<Metadata> {
const { locale } = await params;
const t = await getTranslations({ locale, namespace: "meta" });
const url =
locale === "en" ? "https://cmux.com" : `https://cmux.com/${locale}`;
const alternates = buildAlternates(locale, "");
return {
title: t("title"),
description: t("description"),
Expand All @@ -52,7 +52,7 @@ export async function generateMetadata({
openGraph: {
title: t("title"),
description: t("ogDescription"),
url,
url: alternates.canonical,
siteName: "cmux",
type: "website",
},
Expand All @@ -61,6 +61,7 @@ export async function generateMetadata({
title: t("title"),
description: t("ogDescription"),
},
alternates,
metadataBase: new URL("https://cmux.com"),
};
}
Expand Down
3 changes: 2 additions & 1 deletion web/app/[locale]/nightly/page.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { useTranslations } from "next-intl";
import { getTranslations } from "next-intl/server";
import { buildAlternates } from "../../../i18n/seo";
import { SiteHeader } from "../components/site-header";

export async function generateMetadata({
Expand All @@ -12,7 +13,7 @@ export async function generateMetadata({
return {
title: t("metaTitle"),
description: t("metaDescription"),
alternates: { canonical: "./" },
alternates: buildAlternates(locale, "/nightly"),
};
}

Expand Down
Loading
Loading