Repository navigation
Fix Ahrefs SEO audit issues - #7561
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
📝 WalkthroughWalkthroughChangesLocalized SEO metadata
Remote tmux localization
Presentation and cache rules
Estimated code review effort: 4 (Complex) | ~60 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 23 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (23 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Greptile SummaryThis PR improves SEO metadata and discovery for public web pages. The main changes are:
Confidence Score: 5/5This looks safe to merge.
Important Files Changed
Reviews (12): Last reviewed commit: "Fallback OG tagline when locale font is ..." | Re-trigger Greptile |
| const shortDescriptionSuffixes: Record<string, string> = { | ||
| en: "Built for AI coding agents on macOS.", | ||
| ja: "macOS の AI コーディングエージェント向けです。", | ||
| "zh-CN": "面向 macOS 上的 AI 编码代理。", | ||
| "zh-TW": "面向 macOS 上的 AI 程式碼代理。", | ||
| ko: "macOS의 AI 코딩 에이전트를 위해 설계되었습니다.", | ||
| de: "Für KI-Coding-Agenten auf macOS entwickelt.", | ||
| es: "Creado para agentes de codificación con IA en macOS.", | ||
| fr: "Conçu pour les agents de codage IA sur macOS.", | ||
| it: "Creato per agenti di codifica IA su macOS.", | ||
| da: "Bygget til AI-kodeagenter på macOS.", | ||
| pl: "Stworzone dla agentów kodowania AI na macOS.", | ||
| ru: "Создано для AI-агентов программирования на macOS.", | ||
| bs: "Napravljeno za AI agente za kodiranje na macOS-u.", | ||
| ar: "مصمم لوكلاء البرمجة بالذكاء الاصطناعي على macOS.", | ||
| no: "Laget for AI-kodeagenter på macOS.", | ||
| "pt-BR": "Criado para agentes de código com IA no macOS.", | ||
| th: "สร้างมาเพื่อเอเจนต์เขียนโค้ด AI บน macOS.", | ||
| tr: "macOS'taki AI kodlama ajanları için tasarlandı.", | ||
| km: "បង្កើតសម្រាប់ភ្នាក់ងារ AI សរសេរកូដលើ macOS។", | ||
| uk: "Створено для AI-агентів програмування на macOS.", | ||
| }; |
There was a problem hiding this comment.
Metadata Copy Bypasses Locales
seoDescription() now appends these strings to localized meta, Open Graph, and Twitter descriptions across many production pages, but the copy lives in a hardcoded TypeScript table instead of the locale message catalogs. Translation updates and locale audits can miss this user-facing metadata, so supported locales can ship stale or inconsistent SEO text.
Rule Used: Flag production user-facing text that is not fully... (source)
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
| url: DEFAULT_OG_IMAGE, | ||
| width: 1200, | ||
| height: 630, | ||
| alt: "cmux - The terminal built for multitasking", |
There was a problem hiding this comment.
openGraphDefaults() is spread into localized pages across the site, so this hardcoded English og:image:alt is emitted for non-English routes too. Because the string is not read from locale messages, those pages cannot provide translated image alt metadata for the supported locales.
Rule Used: Flag production user-facing text that is not fully... (source)
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
7012b28 to
091f83e
Compare
8e2020e to
3b4b0cc
Compare
3b4b0cc to
94cd1fa
Compare
51f95a7 to
d5bec9e
Compare
There was a problem hiding this comment.
Actionable comments posted: 9
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@web/app/`[locale]/(landing)/agents/claude-code/page.tsx:
- Around line 9-27: Extract the duplicated metadata composition from
generateMetadata into a shared async buildPageMetadata helper in the SEO module,
accepting locale, namespace, path, optional type, and availableLocales. Have the
helper perform translations, alternates, title/description, Open Graph, and
Twitter metadata construction, then update generateMetadata and other migrated
pages to call it with their page-specific values.
In `@web/app/`[locale]/(landing)/blog/cmd-shift-u/page.tsx:
- Around line 16-19: Replace the hardcoded keywords in the blog page metadata
with localized values from the blog.cmdShiftU namespace using the existing
next-intl pattern, such as t.raw("metaKeywords"). Add the corresponding
metaKeywords array to both web/messages/en.json and web/messages/ja.json,
ensuring the page’s metadata loader provides the translator.
In `@web/app/`[locale]/(landing)/blog/show-hn-launch/page.tsx:
- Around line 19-23: Replace the hardcoded keywords in the blog page metadata
with localized values from the blog.showHnLaunch translation namespace, using
the page’s existing next-intl translation pattern (such as
t.raw("metaKeywords")). Add the corresponding metaKeywords array to both
web/messages/en.json and web/messages/ja.json.
In `@web/app/`[locale]/(landing)/blog/zen-of-cmux/page.tsx:
- Around line 3-28: Extract the repeated metadata assembly into a shared
buildPageMetadata helper in seo.ts, accepting locale, path, title, description,
optional type, and availableLocales. Have it construct alternates, localized
description, openGraph, and twitter fields, then update generateMetadata and the
other duplicated page implementations to call this helper while preserving
article-specific fields such as publishedTime.
In `@web/app/`[locale]/(landing)/community/page.tsx:
- Around line 17-30: Extract the repeated metadata assembly into a shared
buildPageMetadata helper in the seo module, accepting locale, path, title,
rawDescription, and optional type. Move alternates, localized description,
openGraph defaults, canonical URL, and twitterSummary orchestration into this
helper, then simplify each page’s generateMetadata—including the community
page—to call it directly and update imports accordingly.
In `@web/app/`[locale]/(landing)/opengraph-image.tsx:
- Line 9: Update the route-level Open Graph metadata associated with the
exported alt value in opengraph-image.tsx so it uses the current locale’s
translation source, or remove the static alt export and rely on
openGraphDefaults as the authoritative localized metadata source; do not leave
an English-only value applied to every locale route.
In `@web/app/lib/agent-page-paths.ts`:
- Line 160: Add the /docs/remote-tmux route to the landing page registry in
guides/page.tsx, and add matching landing.links entries to both en.json and
ja.json, using the existing agentReadablePages and sitemap.ts entries as the
reference.
In `@web/security-headers.ts`:
- Around line 18-19: The hardcoded locale matcher in localePrefix duplicates the
canonical locale list. Import or derive the locale values from the routing
configuration in i18n/routing.ts, then construct localePrefix from that shared
source and update related next.config.ts and security-headers.test.ts usage to
reference the same definition.
- Around line 21-43: Add unprefixed English equivalents for the marketing routes
in publicMarketingSources, including docs, blog, agents, guides, compare, and
each listed static marketing page, so default-locale bare paths receive
publicMarketingCacheHeaders alongside localized paths.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: 624ace30-b61f-4646-bb9f-9cc34dc0579f
📒 Files selected for processing (80)
web/app/[locale]/(landing)/agents/aider/page.tsxweb/app/[locale]/(landing)/agents/amp/page.tsxweb/app/[locale]/(landing)/agents/claude-code/page.tsxweb/app/[locale]/(landing)/agents/codex/page.tsxweb/app/[locale]/(landing)/agents/cursor-cli/page.tsxweb/app/[locale]/(landing)/agents/gemini-cli/page.tsxweb/app/[locale]/(landing)/agents/opencode/page.tsxweb/app/[locale]/(landing)/agents/page.tsxweb/app/[locale]/(landing)/assets/page.tsxweb/app/[locale]/(landing)/best-terminal-for-mac/page.tsxweb/app/[locale]/(landing)/blog/claude-code-best-worktree-manager/page.tsxweb/app/[locale]/(landing)/blog/cmd-shift-u/page.tsxweb/app/[locale]/(landing)/blog/cmux-claude-teams/page.tsxweb/app/[locale]/(landing)/blog/cmux-finder/page.tsxweb/app/[locale]/(landing)/blog/cmux-history/page.tsxweb/app/[locale]/(landing)/blog/cmux-home/page.tsxweb/app/[locale]/(landing)/blog/cmux-omo/page.tsxweb/app/[locale]/(landing)/blog/cmux-ssh/page.tsxweb/app/[locale]/(landing)/blog/cmux-vault/page.tsxweb/app/[locale]/(landing)/blog/gpl/page.tsxweb/app/[locale]/(landing)/blog/introducing-cmux/page.tsxweb/app/[locale]/(landing)/blog/layout.tsxweb/app/[locale]/(landing)/blog/markdown-viewer/page.tsxweb/app/[locale]/(landing)/blog/page.tsxweb/app/[locale]/(landing)/blog/passkey-auth/page.tsxweb/app/[locale]/(landing)/blog/session-restore/page.tsxweb/app/[locale]/(landing)/blog/show-hn-launch/page.tsxweb/app/[locale]/(landing)/blog/task-manager/page.tsxweb/app/[locale]/(landing)/blog/unread-shortcuts/page.tsxweb/app/[locale]/(landing)/blog/zen-of-cmux/page.tsxweb/app/[locale]/(landing)/built-on-ghostty/page.tsxweb/app/[locale]/(landing)/community/page.tsxweb/app/[locale]/(landing)/compare/[slug]/page.tsxweb/app/[locale]/(landing)/compare/page.tsxweb/app/[locale]/(landing)/deeplink/[kind]/page.tsxweb/app/[locale]/(landing)/docs/agent-integrations/claude-code-teams/page.tsxweb/app/[locale]/(landing)/docs/agent-integrations/oh-my-claudecode/page.tsxweb/app/[locale]/(landing)/docs/agent-integrations/oh-my-codex/page.tsxweb/app/[locale]/(landing)/docs/agent-integrations/oh-my-opencode/page.tsxweb/app/[locale]/(landing)/docs/api/page.tsxweb/app/[locale]/(landing)/docs/browser-automation/page.tsxweb/app/[locale]/(landing)/docs/changelog/page.tsxweb/app/[locale]/(landing)/docs/concepts/page.tsxweb/app/[locale]/(landing)/docs/configuration/page.tsxweb/app/[locale]/(landing)/docs/custom-commands/page.tsxweb/app/[locale]/(landing)/docs/dock/page.tsxweb/app/[locale]/(landing)/docs/getting-started/page.tsxweb/app/[locale]/(landing)/docs/ios/page.tsxweb/app/[locale]/(landing)/docs/keyboard-shortcuts/page.tsxweb/app/[locale]/(landing)/docs/layout.tsxweb/app/[locale]/(landing)/docs/notifications/page.tsxweb/app/[locale]/(landing)/docs/remote-tmux/page.tsxweb/app/[locale]/(landing)/docs/session-restore/page.tsxweb/app/[locale]/(landing)/docs/skills/page.tsxweb/app/[locale]/(landing)/docs/ssh/page.tsxweb/app/[locale]/(landing)/docs/task-manager/page.tsxweb/app/[locale]/(landing)/docs/textbox/page.tsxweb/app/[locale]/(landing)/docs/vault/page.tsxweb/app/[locale]/(landing)/docs/workspace-groups/page.tsxweb/app/[locale]/(landing)/download/confirmation/page.tsxweb/app/[locale]/(landing)/guides/page.tsxweb/app/[locale]/(landing)/ios/page.tsxweb/app/[locale]/(landing)/nightly/page.tsxweb/app/[locale]/(landing)/opengraph-image.tsxweb/app/[locale]/(landing)/page.tsxweb/app/[locale]/(landing)/wall-of-love/page.tsxweb/app/[locale]/components/site-footer.tsxweb/app/[locale]/dashboard/subrouter/page.tsxweb/app/[locale]/docs/base/page.tsxweb/app/[locale]/enterprise/page.tsxweb/app/[locale]/layout.tsxweb/app/[locale]/pricing/page.tsxweb/app/lib/agent-page-paths.tsweb/app/sitemap.tsweb/i18n/seo.tsweb/proxy.tsweb/security-headers.tsweb/tests/agent-page-variants.test.tsweb/tests/security-headers.test.tsweb/tests/seo.test.ts
| export async function generateMetadata({ params }: { params: Promise<{ locale: string }> }) { | ||
| const { locale } = await params; | ||
| const t = await getTranslations({ locale, namespace: "landing.claude" }); | ||
| const alternates = buildAlternates(locale, "/agents/claude-code"); | ||
| const title = t("metaTitle"); | ||
| const description = seoDescription(locale, t("metaDescription")); | ||
| return { | ||
| title: t("metaTitle"), | ||
| description: t("metaDescription"), | ||
| alternates: buildAlternates(locale, "/agents/claude-code"), | ||
| title, | ||
| description, | ||
| alternates, | ||
| openGraph: { | ||
| ...openGraphDefaults(locale, "website"), | ||
| title, | ||
| description, | ||
| url: alternates.canonical, | ||
| }, | ||
| twitter: twitterSummary(locale, title, description), | ||
| }; | ||
| } |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🔵 Trivial | 🏗️ Heavy lift
Extract the repeated generateMetadata boilerplate into a shared helper.
The alternates → title/description → { title, description, alternates, openGraph, twitter } composition here is duplicated verbatim (only namespace/path/type differ) across dozens of migrated pages in this PR. Consider a single buildPageMetadata(locale, { namespace, path, type, availableLocales }) in @/i18n/seo that each page calls with 3-4 arguments, reducing ~15 lines of copy-paste per page and eliminating the risk of one page silently drifting from the pattern during future edits.
♻️ Sketch of a consolidated helper (in `web/i18n/seo.ts`)
export async function buildPageMetadata(
locale: string,
namespace: string,
path: string,
type: "website" | "article" = "website",
availableLocales?: readonly string[],
) {
const t = await getTranslations({ locale, namespace });
const alternates = buildAlternates(locale, path, availableLocales);
const title = t("metaTitle");
const description = seoDescription(locale, t("metaDescription"));
return {
title,
description,
alternates,
openGraph: { ...openGraphDefaults(locale, type), title, description, url: alternates.canonical },
twitter: twitterSummary(locale, title, description),
};
}🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@web/app/`[locale]/(landing)/agents/claude-code/page.tsx around lines 9 - 27,
Extract the duplicated metadata composition from generateMetadata into a shared
async buildPageMetadata helper in the SEO module, accepting locale, namespace,
path, optional type, and availableLocales. Have the helper perform translations,
alternates, title/description, Open Graph, and Twitter metadata construction,
then update generateMetadata and other migrated pages to call it with their
page-specific values.
| keywords: [ | ||
| "cmux", "terminal", "macOS", "notifications", "AI coding agents", | ||
| "keyboard shortcuts", "developer tools", "workflow", | ||
| ], |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win
Use localized keywords for consistency with other blog pages.
The keywords array is hardcoded in English, while other blog pages in this PR (markdown-viewer, passkey-auth, session-restore, task-manager, unread-shortcuts) use t.raw("metaKeywords") for localized keywords. Per coding guidelines, web metadata must use next-intl or another locale-specific source. The same pattern appears in show-hn-launch/page.tsx.
As per coding guidelines, web/**/*.{ts,tsx,md,mdx,json}: "User-facing web UI text, API copy, rendered markdown, changelog text, metadata, route copy, and message keys must use next-intl or another locale-specific source and be represented for every supported locale."
♻️ Suggested fix: use localized keywords
- keywords: [
- "cmux", "terminal", "macOS", "notifications", "AI coding agents",
- "keyboard shortcuts", "developer tools", "workflow",
- ],
+ const rawKeywords = t.raw("metaKeywords");
+ const keywords = Array.isArray(rawKeywords)
+ ? rawKeywords.filter((keyword): keyword is string => typeof keyword === "string")
+ : [];Add the metaKeywords array to the blog.cmdShiftU namespace in web/messages/en.json and web/messages/ja.json.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@web/app/`[locale]/(landing)/blog/cmd-shift-u/page.tsx around lines 16 - 19,
Replace the hardcoded keywords in the blog page metadata with localized values
from the blog.cmdShiftU namespace using the existing next-intl pattern, such as
t.raw("metaKeywords"). Add the corresponding metaKeywords array to both
web/messages/en.json and web/messages/ja.json, ensuring the page’s metadata
loader provides the translator.
Source: Coding guidelines
| keywords: [ | ||
| "cmux", "Show HN", "Hacker News", "terminal", "macOS", "Ghostty", | ||
| "libghostty", "AI coding agents", "Claude Code", "Codex", "launch", | ||
| "vertical tabs", "notification rings", | ||
| ], |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win
Use localized keywords for consistency with other blog pages.
Same as cmd-shift-u/page.tsx — the keywords array is hardcoded in English while other blog pages in this PR use t.raw("metaKeywords") for localized keywords. Per coding guidelines, web metadata must use next-intl or another locale-specific source.
As per coding guidelines, web/**/*.{ts,tsx,md,mdx,json}: "metadata ... must use next-intl or another locale-specific source and be represented for every supported locale."
♻️ Suggested fix: use localized keywords
- keywords: [
- "cmux", "Show HN", "Hacker News", "terminal", "macOS", "Ghostty",
- "libghostty", "AI coding agents", "Claude Code", "Codex", "launch",
- "vertical tabs", "notification rings",
- ],
+ const rawKeywords = t.raw("metaKeywords");
+ const keywords = Array.isArray(rawKeywords)
+ ? rawKeywords.filter((keyword): keyword is string => typeof keyword === "string")
+ : [];Add the metaKeywords array to the blog.showHnLaunch namespace in web/messages/en.json and web/messages/ja.json.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@web/app/`[locale]/(landing)/blog/show-hn-launch/page.tsx around lines 19 -
23, Replace the hardcoded keywords in the blog page metadata with localized
values from the blog.showHnLaunch translation namespace, using the page’s
existing next-intl translation pattern (such as t.raw("metaKeywords")). Add the
corresponding metaKeywords array to both web/messages/en.json and
web/messages/ja.json.
Source: Coding guidelines
| 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.zenOfCmux" }); | ||
| const alternates = buildAlternates(locale, "/blog/zen-of-cmux"); | ||
| const title = t("metaTitle"); | ||
| const description = seoDescription(locale, t("metaDescription")); | ||
| return { | ||
| title: t("metaTitle"), | ||
| description: t("metaDescription"), | ||
| title, | ||
| description, | ||
| keywords: [ | ||
| "cmux", "terminal", "macOS", "CLI", "composable", | ||
| "developer tools", "AI coding agents", "workflow", | ||
| ], | ||
| openGraph: { | ||
| title: t("metaTitle"), | ||
| description: t("metaDescription"), | ||
| type: "article", | ||
| ...openGraphDefaults(locale, "article"), | ||
| title, | ||
| description, | ||
| url: alternates.canonical, | ||
| publishedTime: "2026-02-27T00:00:00Z", | ||
| }, | ||
| twitter: { | ||
| card: "summary_large_image", | ||
| title: t("metaTitle"), | ||
| description: t("metaDescription"), | ||
| }, | ||
| alternates: buildAlternates(locale, "/blog/zen-of-cmux"), | ||
| twitter: twitterSummary(locale, title, description), | ||
| alternates, |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟠 Major | 🏗️ Heavy lift
LGTM on the metadata migration itself, but flag the recurring duplication pattern.
The alternates → title/description → openGraph/twitter construction here is copy-pasted nearly verbatim across dozens of page files in this PR (built-on-ghostty, guides, ios, nightly, wall-of-love, dashboard/subrouter, docs/base, and many more per the stack outline). Consider extracting a single buildPageMetadata(locale, path, { title, description, type, availableLocales }) helper into web/i18n/seo.ts that returns the full { title, description, alternates, openGraph, twitter } object, so each page only supplies its translated title/description and og type. This centralizes future changes (e.g. adding a new social field) to one place instead of 60+ call sites.
♻️ Sketch of a consolidating helper
// web/i18n/seo.ts
export function buildPageMetadata(
locale: string,
path: string,
opts: { title: string; description: string; type?: "website" | "article"; availableLocales?: readonly string[] },
) {
const alternates = buildAlternates(locale, path, opts.availableLocales);
const description = seoDescription(locale, opts.description);
return {
title: opts.title,
description,
alternates,
openGraph: {
...openGraphDefaults(locale, opts.type ?? "website"),
title: opts.title,
description,
url: alternates.canonical,
},
twitter: twitterSummary(locale, opts.title, description),
};
}🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@web/app/`[locale]/(landing)/blog/zen-of-cmux/page.tsx around lines 3 - 28,
Extract the repeated metadata assembly into a shared buildPageMetadata helper in
seo.ts, accepting locale, path, title, description, optional type, and
availableLocales. Have it construct alternates, localized description,
openGraph, and twitter fields, then update generateMetadata and the other
duplicated page implementations to call this helper while preserving
article-specific fields such as publishedTime.
| const alternates = buildAlternates(locale, "/community"); | ||
| const title = t("metaTitle"); | ||
| const description = seoDescription(locale, t("metaDescription")); | ||
| return { | ||
| title: t("metaTitle"), | ||
| description: t("metaDescription"), | ||
| alternates: buildAlternates(locale, "/community"), | ||
| title, | ||
| description, | ||
| alternates, | ||
| openGraph: { | ||
| ...openGraphDefaults(locale, "website"), | ||
| title, | ||
| description, | ||
| url: alternates.canonical, | ||
| }, | ||
| twitter: twitterSummary(locale, title, description), |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win
Consider extracting a shared buildPageMetadata helper to eliminate duplicated orchestration.
The same generateMetadata body pattern—compute alternates, derive title/description, spread openGraphDefaults, set url: alternates.canonical, and call twitterSummary—is repeated across all 60+ pages in this PR. A single higher-level helper in @/i18n/seo would centralize the orchestration and reduce each page's generateMetadata to a one-liner.
♻️ Proposed shared helper and simplified call site
Add to web/i18n/seo.ts:
export function buildPageMetadata(
locale: string,
path: string,
title: string,
rawDescription: string,
type: "website" | "article" = "website",
) {
const alternates = buildAlternates(locale, path);
const description = seoDescription(locale, rawDescription);
return {
title,
description,
alternates,
openGraph: {
...openGraphDefaults(locale, type),
title,
description,
url: alternates.canonical,
},
twitter: twitterSummary(locale, title, description),
};
}Then each page becomes:
export async function generateMetadata({ params }: { params: Promise<{ locale: string }> }) {
const { locale } = await params;
const t = await getTranslations({ locale, namespace: "community" });
- const alternates = buildAlternates(locale, "/community");
- const title = t("metaTitle");
- const description = seoDescription(locale, t("metaDescription"));
- return {
- title,
- description,
- alternates,
- openGraph: {
- ...openGraphDefaults(locale, "website"),
- title,
- description,
- url: alternates.canonical,
- },
- twitter: twitterSummary(locale, title, description),
- };
+ return buildPageMetadata(locale, "/community", t("metaTitle"), t("metaDescription"), "website");
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const alternates = buildAlternates(locale, "/community"); | |
| const title = t("metaTitle"); | |
| const description = seoDescription(locale, t("metaDescription")); | |
| return { | |
| title: t("metaTitle"), | |
| description: t("metaDescription"), | |
| alternates: buildAlternates(locale, "/community"), | |
| title, | |
| description, | |
| alternates, | |
| openGraph: { | |
| ...openGraphDefaults(locale, "website"), | |
| title, | |
| description, | |
| url: alternates.canonical, | |
| }, | |
| twitter: twitterSummary(locale, title, description), | |
| const alternates = buildAlternates(locale, "/community"); | |
| const title = t("metaTitle"); | |
| const description = seoDescription(locale, t("metaDescription")); | |
| return { | |
| title, | |
| description, | |
| alternates, | |
| openGraph: { | |
| ...openGraphDefaults(locale, "website"), | |
| title, | |
| description, | |
| url: alternates.canonical, | |
| }, | |
| twitter: twitterSummary(locale, title, description), |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@web/app/`[locale]/(landing)/community/page.tsx around lines 17 - 30, Extract
the repeated metadata assembly into a shared buildPageMetadata helper in the seo
module, accepting locale, path, title, rawDescription, and optional type. Move
alternates, localized description, openGraph defaults, canonical URL, and
twitterSummary orchestration into this helper, then simplify each page’s
generateMetadata—including the community page—to call it directly and update
imports accordingly.
| const localePrefix = | ||
| ":locale(ja|zh-CN|zh-TW|ko|de|es|fr|it|da|pl|ru|bs|ar|no|pt-BR|th|tr|km|uk)"; |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Confirm routing.ts exports a locale list and where localePrefix is duplicated
fd -t f 'routing.ts' web/i18n
rg -nP '(locales|localePrefix)\b' web/i18n/routing.ts web/next.config.ts web/security-headers.tsRepository: manaflow-ai/cmux
Length of output: 1217
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '\n== routing.ts ==\n'
cat -n web/i18n/routing.ts | sed -n '1,120p'
printf '\n== next.config.ts ==\n'
cat -n web/next.config.ts | sed -n '1,120p'
printf '\n== security-headers.ts ==\n'
cat -n web/security-headers.ts | sed -n '1,120p'
printf '\n== security-headers test ==\n'
cat -n web/tests/security-headers.test.ts | sed -n '1,200p'Repository: manaflow-ai/cmux
Length of output: 7998
Derive the locale matcher from web/i18n/routing.ts. localePrefix is hardcoded here and duplicated in web/next.config.ts and web/tests/security-headers.test.ts, while the canonical locale list lives in web/i18n/routing.ts. Pulling it from that source keeps redirects, cache rules, and the test in sync when locales change.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@web/security-headers.ts` around lines 18 - 19, The hardcoded locale matcher
in localePrefix duplicates the canonical locale list. Import or derive the
locale values from the routing configuration in i18n/routing.ts, then construct
localePrefix from that shared source and update related next.config.ts and
security-headers.test.ts usage to reference the same definition.
d5bec9e to
d64ce91
Compare
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 2 potential issues.
There are 5 total unresolved issues (including 3 from previous reviews).
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit d64ce91. Configure here.
d64ce91 to
87b3f4e
Compare
87b3f4e to
1c397f8
Compare
There was a problem hiding this comment.
Actionable comments posted: 3
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (3)
web/tests/security-headers.test.ts (1)
22-42: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick winAvoid duplicating the locale alternation in the test
web/tests/security-headers.test.tshardcodes the same locale list asweb/security-headers.ts, so adding or removing a locale inweb/i18n/routing.tsstill leaves two manual copies to update. Derive the alternation from the shared routing config (excluding the default locale) so the route config and test stay in sync.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@web/tests/security-headers.test.ts` around lines 22 - 42, The test hardcodes the locale alternation instead of using the shared routing configuration. Update the test near the “caches only explicit-locale public marketing pages at the edge” case to derive the localized route pattern from the locales exported by the shared i18n routing config, excluding the default locale, and use that generated pattern when matching `securityHeaderRules`; avoid duplicating locale values.web/app/[locale]/(landing)/blog/cmux-history/page.tsx (1)
21-36: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick winUse localized keywords for consistency with other blog pages.
The
keywordsarray is hardcoded in English, while other blog pages in this PR uset.raw("metaKeywords")for localized keywords. Per coding guidelines, web metadata must usenext-intlor another locale-specific source. As per coding guidelines,web/**/*.{ts,tsx,md,mdx,json}: "User-facing web UI text, API copy, rendered markdown, changelog text, metadata, route copy, and message keys must usenext-intlor another locale-specific source and be represented for every supported locale."♻️ Suggested fix: use localized keywords
- keywords: [ - "cmux", - "history", - "reopen closed terminal", - "restore closed tab", - "reopen terminal tab", - "reopen closed workspace", - "terminal history", - "Cmd+Shift+T", - "focus history", - "Claude Code", - "Codex", - "OpenCode", - "macOS", - "AI coding agents", - ], + const rawKeywords = t.raw("metaKeywords"); + const keywords = Array.isArray(rawKeywords) + ? rawKeywords.filter((keyword): keyword is string => typeof keyword === "string") + : [];Add the
metaKeywordsarray to theblog.cmuxHistorynamespace inweb/messages/en.jsonandweb/messages/ja.json.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@web/app/`[locale]/(landing)/blog/cmux-history/page.tsx around lines 21 - 36, Replace the hardcoded English keywords in the blog page metadata with the localized `t.raw("metaKeywords")` value, following the pattern used by other blog pages. Add the corresponding `metaKeywords` array to the `blog.cmuxHistory` namespace in both `en.json` and `ja.json`, ensuring every supported locale has translations.Source: Coding guidelines
web/app/[locale]/(landing)/blog/gpl/page.tsx (1)
16-19: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick winUse localized keywords for consistency with other blog pages.
The
keywordsarray is hardcoded in English whileblog/cmux-vault/page.tsxin this same PR usest.raw("metaKeywords")for localized keywords. Per coding guidelines, web metadata must usenext-intlor another locale-specific source and be represented for every supported locale.♻️ Suggested fix: use localized keywords
- keywords: [ - "cmux", "GPL", "AGPL", "open source", "license", - "terminal", "macOS", "copyleft", - ], + keywords,Add before the
returnstatement:+ const rawKeywords = t.raw("metaKeywords"); + const keywords = Array.isArray(rawKeywords) + ? rawKeywords.filter((keyword): keyword is string => typeof keyword === "string") + : [];Add the
metaKeywordsarray to theblog.gplnamespace inweb/messages/en.jsonandweb/messages/ja.json.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@web/app/`[locale]/(landing)/blog/gpl/page.tsx around lines 16 - 19, Replace the hardcoded keywords in the GPL page metadata with a localized `metaKeywords` value obtained via `next-intl` (for example, `t.raw("metaKeywords")`), following `blog/cmux-vault/page.tsx`; add the corresponding `metaKeywords` array to the `blog.gpl` namespace in both `web/messages/en.json` and `web/messages/ja.json`.Sources: Coding guidelines, Path instructions
♻️ Duplicate comments (1)
web/app/[locale]/(landing)/blog/show-hn-launch/page.tsx (1)
19-23: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick winHardcoded English keywords still not localized — previously flagged.
This issue was raised in a prior review comment but remains unaddressed. The
keywordsarray at lines 19-23 is hardcoded in English whileblog/cmux-vault/page.tsxusest.raw("metaKeywords")for localized keywords. Per coding guidelines, web metadata must usenext-intlor another locale-specific source.♻️ Suggested fix: use localized keywords
- keywords: [ - "cmux", "Show HN", "Hacker News", "terminal", "macOS", "Ghostty", - "libghostty", "AI coding agents", "Claude Code", "Codex", "launch", - "vertical tabs", "notification rings", - ], + keywords,Add before the
returnstatement:+ const rawKeywords = t.raw("metaKeywords"); + const keywords = Array.isArray(rawKeywords) + ? rawKeywords.filter((keyword): keyword is string => typeof keyword === "string") + : [];Add the
metaKeywordsarray to theblog.showHnLaunchnamespace inweb/messages/en.jsonandweb/messages/ja.json.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@web/app/`[locale]/(landing)/blog/show-hn-launch/page.tsx around lines 19 - 23, Replace the hardcoded keywords in the page’s metadata with localized values retrieved via the existing next-intl translation mechanism, such as t.raw("metaKeywords") before the return. Add the corresponding metaKeywords array to the blog.showHnLaunch namespaces in both en.json and ja.json, matching the pattern used by blog/cmux-vault.Sources: Coding guidelines, Path instructions
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@web/app/`[locale]/(landing)/opengraph-image.tsx:
- Around line 82-84: Make the locale font request in the Promise expression used
by the OG image generation non-fatal: check res.ok before converting the
response to an ArrayBuffer, return null for non-OK responses, and catch network
errors by returning null. Preserve the existing localeFont and localeFontData
guard so ImageResponse proceeds without the font when fetching fails.
- Line 9: Update the Open Graph metadata in the landing-page image module so it
no longer hard-codes English text: use the locale-aware `openGraphImage(locale)`
or `openGraphDefaults(locale, ...)` from `web/i18n/seo.ts`, or remove the
redundant `alt` export if the shared metadata already supplies it.
In `@web/i18n/seo.ts`:
- Around line 4-5: Remove the unused DEFAULT_OG_IMAGE constant from seo.ts,
leaving DEFAULT_OG_IMAGE_PATH and the locale-aware helpers unchanged.
---
Outside diff comments:
In `@web/app/`[locale]/(landing)/blog/cmux-history/page.tsx:
- Around line 21-36: Replace the hardcoded English keywords in the blog page
metadata with the localized `t.raw("metaKeywords")` value, following the pattern
used by other blog pages. Add the corresponding `metaKeywords` array to the
`blog.cmuxHistory` namespace in both `en.json` and `ja.json`, ensuring every
supported locale has translations.
In `@web/app/`[locale]/(landing)/blog/gpl/page.tsx:
- Around line 16-19: Replace the hardcoded keywords in the GPL page metadata
with a localized `metaKeywords` value obtained via `next-intl` (for example,
`t.raw("metaKeywords")`), following `blog/cmux-vault/page.tsx`; add the
corresponding `metaKeywords` array to the `blog.gpl` namespace in both
`web/messages/en.json` and `web/messages/ja.json`.
In `@web/tests/security-headers.test.ts`:
- Around line 22-42: The test hardcodes the locale alternation instead of using
the shared routing configuration. Update the test near the “caches only
explicit-locale public marketing pages at the edge” case to derive the localized
route pattern from the locales exported by the shared i18n routing config,
excluding the default locale, and use that generated pattern when matching
`securityHeaderRules`; avoid duplicating locale values.
---
Duplicate comments:
In `@web/app/`[locale]/(landing)/blog/show-hn-launch/page.tsx:
- Around line 19-23: Replace the hardcoded keywords in the page’s metadata with
localized values retrieved via the existing next-intl translation mechanism,
such as t.raw("metaKeywords") before the return. Add the corresponding
metaKeywords array to the blog.showHnLaunch namespaces in both en.json and
ja.json, matching the pattern used by blog/cmux-vault.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: e5c0aa12-9c5e-4f68-85ad-4261987d4945
📒 Files selected for processing (71)
web/app/[locale]/(landing)/agents/aider/page.tsxweb/app/[locale]/(landing)/agents/amp/page.tsxweb/app/[locale]/(landing)/agents/claude-code/page.tsxweb/app/[locale]/(landing)/agents/codex/page.tsxweb/app/[locale]/(landing)/agents/cursor-cli/page.tsxweb/app/[locale]/(landing)/agents/gemini-cli/page.tsxweb/app/[locale]/(landing)/agents/opencode/page.tsxweb/app/[locale]/(landing)/agents/page.tsxweb/app/[locale]/(landing)/assets/page.tsxweb/app/[locale]/(landing)/best-terminal-for-mac/page.tsxweb/app/[locale]/(landing)/blog/claude-code-best-worktree-manager/page.tsxweb/app/[locale]/(landing)/blog/cmd-shift-u/page.tsxweb/app/[locale]/(landing)/blog/cmux-claude-teams/page.tsxweb/app/[locale]/(landing)/blog/cmux-finder/page.tsxweb/app/[locale]/(landing)/blog/cmux-history/page.tsxweb/app/[locale]/(landing)/blog/cmux-home/page.tsxweb/app/[locale]/(landing)/blog/cmux-omo/page.tsxweb/app/[locale]/(landing)/blog/cmux-ssh/page.tsxweb/app/[locale]/(landing)/blog/cmux-vault/page.tsxweb/app/[locale]/(landing)/blog/gpl/page.tsxweb/app/[locale]/(landing)/blog/introducing-cmux/page.tsxweb/app/[locale]/(landing)/blog/markdown-viewer/page.tsxweb/app/[locale]/(landing)/blog/page.tsxweb/app/[locale]/(landing)/blog/passkey-auth/page.tsxweb/app/[locale]/(landing)/blog/session-restore/page.tsxweb/app/[locale]/(landing)/blog/show-hn-launch/page.tsxweb/app/[locale]/(landing)/blog/task-manager/page.tsxweb/app/[locale]/(landing)/blog/unread-shortcuts/page.tsxweb/app/[locale]/(landing)/blog/zen-of-cmux/page.tsxweb/app/[locale]/(landing)/built-on-ghostty/page.tsxweb/app/[locale]/(landing)/community/page.tsxweb/app/[locale]/(landing)/compare/[slug]/page.tsxweb/app/[locale]/(landing)/compare/page.tsxweb/app/[locale]/(landing)/docs/agent-integrations/claude-code-teams/page.tsxweb/app/[locale]/(landing)/docs/agent-integrations/oh-my-claudecode/page.tsxweb/app/[locale]/(landing)/docs/agent-integrations/oh-my-codex/page.tsxweb/app/[locale]/(landing)/docs/agent-integrations/oh-my-opencode/page.tsxweb/app/[locale]/(landing)/docs/api/page.tsxweb/app/[locale]/(landing)/docs/browser-automation/page.tsxweb/app/[locale]/(landing)/docs/changelog/page.tsxweb/app/[locale]/(landing)/docs/concepts/page.tsxweb/app/[locale]/(landing)/docs/configuration/page.tsxweb/app/[locale]/(landing)/docs/custom-commands/page.tsxweb/app/[locale]/(landing)/docs/dock/page.tsxweb/app/[locale]/(landing)/docs/getting-started/page.tsxweb/app/[locale]/(landing)/docs/ios/page.tsxweb/app/[locale]/(landing)/docs/keyboard-shortcuts/page.tsxweb/app/[locale]/(landing)/docs/notifications/page.tsxweb/app/[locale]/(landing)/docs/remote-tmux/page.tsxweb/app/[locale]/(landing)/docs/session-restore/page.tsxweb/app/[locale]/(landing)/docs/skills/page.tsxweb/app/[locale]/(landing)/docs/ssh/page.tsxweb/app/[locale]/(landing)/docs/task-manager/page.tsxweb/app/[locale]/(landing)/docs/textbox/page.tsxweb/app/[locale]/(landing)/docs/vault/page.tsxweb/app/[locale]/(landing)/docs/workspace-groups/page.tsxweb/app/[locale]/(landing)/download/confirmation/page.tsxweb/app/[locale]/(landing)/guides/page.tsxweb/app/[locale]/(landing)/ios/page.tsxweb/app/[locale]/(landing)/nightly/page.tsxweb/app/[locale]/(landing)/opengraph-image.tsxweb/app/[locale]/(landing)/wall-of-love/page.tsxweb/app/[locale]/dashboard/subrouter/page.tsxweb/app/[locale]/docs/base/page.tsxweb/app/[locale]/enterprise/page.tsxweb/app/[locale]/layout.tsxweb/i18n/seo.tsweb/proxy.tsweb/security-headers.tsweb/tests/security-headers.test.tsweb/tests/seo.test.ts
💤 Files with no reviewable changes (1)
- web/security-headers.ts

Summary
Verification
Ahrefs mapping
Need help on this PR? Tag
/codesmithwith what you need. Autofix is disabled.Note
Low Risk
Broad but mechanical metadata and routing changes with dedicated tests; no auth, payments, or core app logic touched.
Overview
Addresses Ahrefs SEO findings by centralizing metadata in
@/i18n/seoand rolling it out across marketing, blog, docs, agents, compare, and related pages.Metadata:
seoDescriptionpads short copy with localized suffixes;openGraphDefaultsandtwitterSummarysupply locale-aware OG/Twitter images, URLs, and types. Many blog posts drop hard-codedkeywordsin favor of the shared OG/Twitter pattern. Root layout drops the globalkeywordslist and invalid SoftwareApplication JSON-LD in favor of Organization and WebSite schema.OG image:
opengraph-imageis locale-aware (taglines, optional Noto fonts, English fallback) and uses safer font fetching.Routing & links: Middleware redirects unsupported
/<locale>/docs/remote-tmuxto English and rewrites bare/docs/remote-tmux; guides/sitemap/llms.txtadd Remote tmux where locales allow; footer legal links use plain anchors; homepage remote-tmux FAQ usesNextLinkwhen docs aren’t localized.Performance: Edge
Cache-Controlon explicit-locale public marketing paths (not unprefixed English-only routes).Tests: New
seo.test.tsplus updates to security-headers and agent-page-variants tests.Reviewed by Cursor Bugbot for commit e440330. Bugbot is set up for automated code reviews on this repo. Configure here.
Summary by cubic
Fixes Ahrefs SEO issues by centralizing metadata and routing. Adds locale-aware OG/Twitter defaults, correct canonicals/alternates, normalized descriptions, and edge caching for localized pages, with more reliable OG image rendering.
New Features
@/i18n/seo(seoDescription,openGraphDefaults,twitterSummary,openGraphImageTagline, canonicals/alternates) rolled out across landing, blog, compare, docs, guides, enterprise, and pricing; blog/docs layouts now use OG defaults./sitemap.xmland/llms.txtinclude Remote tmux per locale;opengraph-imageloads locale fonts with a timeout and falls back to a default tagline if fonts aren’t available.Bug Fixes
seoDescription; ensured complete OG/Twitter fields, correct OG types (article/website), canonicals, andhreflangalternates./<locale>/docs/remote-tmuxto/en/docs/remote-tmux; bare/docs/remote-tmuxrewrites to English. Footer legal links stay unlocalized; homepage FAQ links useNextLinkwhen docs aren’t localized. Tests added for SEO helpers, cache rules, and agent page variants.Written for commit e440330. Summary will update on new commits.
Summary by CodeRabbit
Improvements
New Features
Bug Fixes
Tests