Skip to content

Fix SEO indexing: hreflang, canonicals, sitemap, trailing slash - #2193

Merged
lawrencecchen merged 7 commits into
mainfrom
task-fix-seo-indexing
Mar 26, 2026
Merged

lawrencecchen merged 7 commits into
mainfrom
task-fix-seo-indexing

Conversation

@lawrencecchen

@lawrencecchen lawrencecchen commented Mar 26, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • Add buildAlternates() utility (i18n/seo.ts) that returns both canonical and languages (hreflang) for a given locale and path, used by every page's metadata
  • Add hreflang tags to all rendered pages (previously only in sitemap XML, not in HTML <link> tags)
  • Add self-referencing canonical URLs to every page (homepage had none, child layouts using "./" were wiping parent hreflang)
  • Expand sitemap to emit a separate entry per locale (was only listing English URLs with hreflang alternates)
  • Add missing /docs/custom-commands to sitemap
  • Remove skipTrailingSlashRedirect: true from next.config.ts so Next.js normalizes trailing slash duplicates
  • Block /_next/ in robots.txt to stop ~200 JS/CSS chunk files from being crawled as pages

Context

Google Search Console reported 380 not-indexed pages vs 86 indexed. The coverage report at https://search.google.com/search-console/index?resource_id=sc-domain:cmux.com showed:

  • 306 pages "Crawled, currently not indexed" (locale variants without proper signals + _next/static chunks)
  • 34 pages "Duplicate, Google chose different canonical" (locale pages without hreflang in HTML)
  • 26 pages "Duplicate without user-selected canonical" (homepage and pages inheriting "./")
  • 8 pages with redirects (expected: /docs redirect, cmux.dev domain redirect)

Testing

  • npm run build passes
  • npx tsc --noEmit passes
  • Verify via Vercel preview that <link rel="canonical"> and <link rel="alternate" hreflang="..."> tags appear in page source
  • After deploy, re-validate in Google Search Console

Related

  • Task: Fix Google Search Console indexing issues for cmux.com

Summary by CodeRabbit

  • New Features

    • Added /docs/custom-commands to the sitemap.
    • Added redirects so localized legal page paths now redirect to the English legal pages.
  • Improvements

    • Sitemap now emits per-locale entries and treats certain legal pages as English-only for search indices.
    • Canonical/alternate link handling updated site-wide to be locale-aware for pages and blog posts.
  • Chores

    • Robots updated to disallow internal framework assets from being crawled.

Google Search Console showed 380 not-indexed vs 86 indexed pages.
Root causes: missing hreflang tags on rendered pages (only in sitemap),
no canonical on homepage, inconsistent canonicals wiping parent hreflang,
sitemap only listing English URLs, trailing slash duplicates, and
_next/static chunks being crawled as pages.

Changes:
- Add buildAlternates() utility for consistent canonical + hreflang
- Add hreflang tags to all pages via alternates.languages in metadata
- Add self-referencing canonical URLs to every page (homepage had none)
- Expand sitemap to emit separate entries for each locale
- Add missing /docs/custom-commands to sitemap
- Remove skipTrailingSlashRedirect to normalize trailing slashes
- Block /_next/ in robots.txt to stop chunk crawling
@vercel

vercel Bot commented Mar 26, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
cmux Ready Ready Preview, Comment Mar 26, 2026 10:02pm

@coderabbitai

coderabbitai Bot commented Mar 26, 2026 •

Copy link
Copy Markdown

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Added a shared SEO helper buildAlternates(locale, path) and updated many locale-scoped pages to use it for canonical/alternate metadata; sitemap and robots were adjusted; middleware (web/proxy.ts) adds an early redirect for localized legal pages; removed skipTrailingSlashRedirect from Next config.

Changes

Cohort / File(s) Summary
SEO helper
web/i18n/seo.ts
New exported buildAlternates(locale, path) returning { canonical, languages } (maps en to root, other locales to /${locale}, adds x-default).
Pages using buildAlternates
web/app/[locale]/blog/.../page.tsx, web/app/[locale]/blog/page.tsx, web/app/[locale]/blog/layout.tsx, web/app/[locale]/docs/.../page.tsx, web/app/[locale]/docs/layout.tsx, web/app/[locale]/community/page.tsx, web/app/[locale]/nightly/page.tsx, web/app/[locale]/wall-of-love/page.tsx, web/app/[locale]/layout.tsx, web/app/[locale]/blog/...
Replaced manual/static canonical handling with alternates: buildAlternates(locale, "<path>"); removed per-file url construction and stopped setting openGraph.url where it was previously used.
Legal pages
web/app/[locale]/(legal)/eula/page.tsx, web/app/[locale]/(legal)/privacy-policy/page.tsx, web/app/[locale]/(legal)/terms-of-service/page.tsx
Switched alternates.canonical from relative "./" to absolute English URLs (https://cmux.com/eula, https://cmux.com/privacy-policy, https://cmux.com/terms-of-service) in exported metadata.
Sitemap
web/app/sitemap.ts
Added /docs/custom-commands; emit English-only entries for legal pages via an englishOnly set and emit per-locale entries for other paths, attaching alternates.languages (including x-default) to each entry.
Robots
web/app/robots.ts
Added Disallow: /_next/ directive to robots output.
Middleware / Proxy
web/proxy.ts
Added early 301 redirect for requests matching /privacy-policy, /terms-of-service, or /eula (rewrites to English-only path) and returns before running i18n middleware.
Next config
web/next.config.ts
Removed skipTrailingSlashRedirect from nextConfig.

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant EdgeMiddleware as middleware (web/proxy.ts)
  participant IntlMiddleware as intlMiddleware
  participant Origin as NextApp

  Client->>EdgeMiddleware: Request /fr/privacy-policy
  EdgeMiddleware->>EdgeMiddleware: detect trailing remainder "/privacy-policy"
  EdgeMiddleware->>Client: 301 Redirect -> /privacy-policy
  Note right of Client: Browser follows 301
  Client->>EdgeMiddleware: Request /privacy-policy
  EdgeMiddleware->>IntlMiddleware: pass-through to i18n middleware
  IntlMiddleware->>Origin: render page (generateMetadata uses buildAlternates)
  Origin-->>Client: HTML with alternates canonical metadata
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐰 I hop through routes and tidy each strand,
I map each locale with a careful hand.
From ./ to full URLs, links now align,
Alternates hum across every language line.
A tiny hop — the web looks fine!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 3.85% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the primary focus of this changeset: fixing SEO indexing through hreflang tags, canonical URLs, sitemap improvements, and trailing slash normalization.
Description check ✅ Passed The description covers all required template sections: a detailed Summary explaining what changed and why, comprehensive Testing section with verification steps, and a complete Checklist. The PR provides clear context from Google Search Console and explains the motivation for each change.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch task-fix-seo-indexing

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

No issues found across 17 files

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a2563d81c2

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

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 👍 / 👎.

@greptile-apps

greptile-apps Bot commented Mar 26, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR systematically addresses Google Search Console's 380 unindexed pages by adding hreflang tags to every rendered page (previously only present in sitemap XML), fixing self-referencing canonical URLs, expanding the sitemap to emit one entry per locale, blocking /_next/ chunks from crawling, and removing skipTrailingSlashRedirect to normalise trailing-slash duplicates. The new buildAlternates() utility in web/i18n/seo.ts is clean, reusable, and correctly handles the x-default fallback across all 18 locales.\n\nKey changes:\n- web/i18n/seo.ts — new buildAlternates(locale, path) utility used by every generateMetadata\n- web/app/[locale]/layout.tsx, legal pages, community, nightly, wall-of-love, blog posts — all switched to absolute buildAlternates() calls\n- web/app/sitemap.ts — emits one entry per locale × path (was: English only with alternates); adds /docs/custom-commands\n- web/app/robots.ts — adds disallow: \"/_next/\" to stop JS/CSS chunks being indexed\n- web/next.config.ts — removes skipTrailingSlashRedirect: true\n\nIssue to address before merging:\n- The docs layout sets alternates: buildAlternates(locale, \"/docs\"), but individual docs sub-pages (getting-started, configuration, api, etc.) define generateMetadata without returning alternates. Next.js will merge the layout's alternates into those pages, giving every docs sub-page canonical: \"https://cmux.com/docs\" rather than their own URL. Each docs page needs alternates: buildAlternates(locale, \"/docs/<slug>\") added to its own generateMetadata.\n\nMinor follow-ups (non-blocking):\n- openGraph.url was removed from all four blog post generateMetadata functions. Without it, social platforms use the fetched URL, which varies by locale — this may split share counts across locale variants.

Confidence Score: 3/5

Safe to merge for most of the site, but the docs layout canonical bug means 8+ docs pages still won't have self-referencing canonicals — the primary goal of this PR.

The new seo.ts utility, sitemap expansion, robots.txt fix, homepage canonical, and all leaf-page changes are correct. However, the docs layout sets canonical to /docs for all docs sub-pages that don't override it, which is directly contrary to the PR's stated goal of giving every page a correct self-referencing canonical.

web/app/[locale]/docs/layout.tsx — and all docs sub-page files (getting-started, configuration, api, notifications, changelog, keyboard-shortcuts, browser-automation, concepts, custom-commands) which lack their own alternates.

Important Files Changed

Filename Overview
web/i18n/seo.ts New utility that centralises canonical + hreflang generation; logic is correct for all locales and the x-default fallback.
web/app/[locale]/docs/layout.tsx Sets canonical to /docs for the entire docs tree; docs sub-pages that don't define their own alternates will inherit this wrong canonical URL.
web/app/sitemap.ts Now emits one entry per locale per path (rather than one English entry with alternates), and adds /docs/custom-commands; all hreflang alternates look correct.
web/app/robots.ts Correctly adds disallow: /_next/ to stop ~200 JS/CSS chunk URLs from being indexed.
web/app/[locale]/layout.tsx Root locale layout now adds buildAlternates(locale, "") for the homepage; canonical and hreflang tags are correct.
web/app/[locale]/blog/layout.tsx Replaces "./" with buildAlternates(locale, "/blog"); safe because individual blog-post pages each override alternates with their own path.
web/app/[locale]/blog/introducing-cmux/page.tsx Switches to buildAlternates; og:url removed which may fragment social-share counts by locale.
web/next.config.ts Removes skipTrailingSlashRedirect: true so Next.js normalises trailing-slash duplicates; correct change.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    SEO["buildAlternates(locale, path)\nweb/i18n/seo.ts"]

    SEO --> RootLayout["[locale]/layout.tsx\nbuildAlternates(locale, '')"]
    SEO --> DocsLayout["docs/layout.tsx\nbuildAlternates(locale, '/docs')"]
    SEO --> BlogLayout["blog/layout.tsx\nbuildAlternates(locale, '/blog')"]
    SEO --> LeafPages["Leaf pages\n(legal, community, nightly, wall-of-love)"]
    SEO --> BlogPosts["Blog post pages\nbuildAlternates(locale, '/blog/...')"]

    DocsLayout -->|"inherited — no override"| DocsSub["docs/getting-started\ndocs/configuration\ndocs/api … etc.\n⚠️ canonical = /docs (wrong)"]
    BlogLayout -->|"overridden by page"| BlogPosts
    BlogPosts -->|"correct self-canonical"| OK1["✅ /blog/introducing-cmux etc."]
    LeafPages -->|"correct self-canonical"| OK2["✅ /eula, /community, /nightly …"]
    RootLayout -->|"correct self-canonical"| OK3["✅ / (homepage)"]
Loading

Comments Outside Diff (1)

  1. web/app/[locale]/blog/introducing-cmux/page.tsx, line 16-20 (link)

    P2 openGraph.url removed — may split social-share counts by locale

    The openGraph.url field (previously set to the locale-aware path, e.g. /ja/blog/introducing-cmux for Japanese) has been dropped across all four blog post pages (introducing-cmux, cmd-shift-u, show-hn-launch, zen-of-cmux). Without an explicit og:url, some social-graph parsers fall back to the URL of the page being fetched. For non-English locales that means each locale variant gets its own OG identity, which can fragment like/share counts across locale URLs rather than consolidating them on the English canonical.

    If consolidating share counts is desired, set og:url to the English canonical (https://cmux.com/blog/introducing-cmux) consistently across all locales. If per-locale counts are acceptable, this removal is fine.

Reviews (1): Last reviewed commit: "Fix SEO indexing: add hreflang, canonica..." | Re-trigger Greptile

alternates: {
canonical: "./",
},
alternates: buildAlternates(locale, "/docs"),

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.

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
web/app/[locale]/layout.tsx (1)

11-11: Avoid duplicated canonical URL construction in metadata.

You currently derive URL twice (openGraph.url and alternates). Consider deriving once from buildAlternates to prevent future drift.

♻️ Suggested refactor
-  const url =
-    locale === "en" ? "https://cmux.com" : `https://cmux.com/${locale}`;
+  const { canonical, languages } = buildAlternates(locale, "");
   return {
@@
     openGraph: {
       title: t("title"),
       description: t("ogDescription"),
-      url,
+      url: canonical,
       siteName: "cmux",
       type: "website",
     },
@@
-    alternates: buildAlternates(locale, ""),
+    alternates: { canonical, languages },
     metadataBase: new URL("https://cmux.com"),
   };

Also applies to: 65-65

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/app/`[locale]/layout.tsx at line 11, The metadata currently constructs
the canonical URL in two places (openGraph.url and alternates); refactor to
derive the canonical URL once from buildAlternates and reuse it for both
openGraph.url and the alternates object to avoid drift. Locate where
buildAlternates is imported/used and compute a single canonical variable (e.g.,
canonicalUrl = buildAlternates(...).canonical or extract the primary URL from
buildAlternates result) then set openGraph.url = canonicalUrl and set alternates
= buildAlternates(...) (or reuse the same result) so both values come from the
same source; update any references in layout.tsx that currently build the URL
twice to use this shared canonicalUrl.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@web/app/`[locale]/layout.tsx:
- Line 11: The metadata currently constructs the canonical URL in two places
(openGraph.url and alternates); refactor to derive the canonical URL once from
buildAlternates and reuse it for both openGraph.url and the alternates object to
avoid drift. Locate where buildAlternates is imported/used and compute a single
canonical variable (e.g., canonicalUrl = buildAlternates(...).canonical or
extract the primary URL from buildAlternates result) then set openGraph.url =
canonicalUrl and set alternates = buildAlternates(...) (or reuse the same
result) so both values come from the same source; update any references in
layout.tsx that currently build the URL twice to use this shared canonicalUrl.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: acbe5d93-f13e-4535-9706-26f204a0be1c

📥 Commits

Reviewing files that changed from the base of the PR and between 8a37815 and a2563d8.

📒 Files selected for processing (17)
  • web/app/[locale]/(legal)/eula/page.tsx
  • web/app/[locale]/(legal)/privacy-policy/page.tsx
  • web/app/[locale]/(legal)/terms-of-service/page.tsx
  • web/app/[locale]/blog/cmd-shift-u/page.tsx
  • web/app/[locale]/blog/introducing-cmux/page.tsx
  • web/app/[locale]/blog/layout.tsx
  • web/app/[locale]/blog/show-hn-launch/page.tsx
  • web/app/[locale]/blog/zen-of-cmux/page.tsx
  • web/app/[locale]/community/page.tsx
  • web/app/[locale]/docs/layout.tsx
  • web/app/[locale]/layout.tsx
  • web/app/[locale]/nightly/page.tsx
  • web/app/[locale]/wall-of-love/page.tsx
  • web/app/robots.ts
  • web/app/sitemap.ts
  • web/i18n/seo.ts
  • web/next.config.ts
💤 Files with no reviewable changes (1)
  • web/next.config.ts

Docs sub-pages and blog index only returned title/description in
generateMetadata, so they inherited the parent layout's alternates
(pointing to /docs or /blog). Now each page sets its own
buildAlternates() with the correct path so canonical and hreflang
point to the actual page URL.
Legal pages (privacy policy, TOS, EULA) are untranslated English content.
Serving them under every locale creates 54 duplicate URLs. Now:
- Middleware 301-redirects /ja/privacy-policy etc. to /privacy-policy
- Sitemap only includes English URLs for legal pages (no locale variants)
- Legal page metadata uses static English-only canonical

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
web/proxy.ts (1)

18-27: Clean implementation that aligns well with sitemap and page metadata.

The logic correctly redirects locale-prefixed legal pages to their English canonical paths, consistent with the englishOnly set in sitemap.ts and the canonical URLs declared in each legal page's metadata.

Minor edge case: Paths with trailing slashes (e.g., /ja/privacy-policy/) won't match the endsWith check and will fall through to intlMiddleware. Since skipTrailingSlashRedirect was removed, Next.js should normalize these, but the redirect order may not guarantee the 301 to the bare path. If trailing slash requests are expected, consider:

Optional: Handle trailing slash variants
   for (const page of legalPages) {
-    if (pathname.endsWith(page) && pathname !== page) {
+    const normalizedPath = pathname.endsWith("/") ? pathname.slice(0, -1) : pathname;
+    if (normalizedPath.endsWith(page) && normalizedPath !== page) {
       const url = request.nextUrl.clone();
       url.pathname = page;
       return NextResponse.redirect(url, 301);
     }
   }

Since the canonical tags are still present on the pages, this is low-impact and can be deferred if trailing slash traffic is minimal.

,

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/proxy.ts` around lines 18 - 27, The redirect logic for locale-prefixed
legal pages (array legalPages checked against request.nextUrl.pathname) misses
paths with trailing slashes like "/ja/privacy-policy/"; normalize the pathname
before the loop (e.g., remove a trailing slash only for matching) or trim the
pathname used in the endsWith and equality checks so pathname.endsWith(page) and
pathname !== page work for both "/ja/privacy-policy" and "/ja/privacy-policy/";
update the code that constructs url.pathname (request.nextUrl.clone()) to set
the canonical page value (page) when a match is found and continue to use
NextResponse.redirect(..., 301) for the redirect.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@web/proxy.ts`:
- Around line 18-27: The redirect logic for locale-prefixed legal pages (array
legalPages checked against request.nextUrl.pathname) misses paths with trailing
slashes like "/ja/privacy-policy/"; normalize the pathname before the loop
(e.g., remove a trailing slash only for matching) or trim the pathname used in
the endsWith and equality checks so pathname.endsWith(page) and pathname !==
page work for both "/ja/privacy-policy" and "/ja/privacy-policy/"; update the
code that constructs url.pathname (request.nextUrl.clone()) to set the canonical
page value (page) when a match is found and continue to use
NextResponse.redirect(..., 301) for the redirect.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 208a2669-6bdb-4cce-8c5d-aa150c4d157d

📥 Commits

Reviewing files that changed from the base of the PR and between 5dbf4ec and 991ecc3.

📒 Files selected for processing (5)
  • web/app/[locale]/(legal)/eula/page.tsx
  • web/app/[locale]/(legal)/privacy-policy/page.tsx
  • web/app/[locale]/(legal)/terms-of-service/page.tsx
  • web/app/sitemap.ts
  • web/proxy.ts
✅ Files skipped from review due to trivial changes (3)
  • web/app/[locale]/(legal)/privacy-policy/page.tsx
  • web/app/[locale]/(legal)/terms-of-service/page.tsx
  • web/app/[locale]/(legal)/eula/page.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • web/app/sitemap.ts

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

3 issues found across 5 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="web/app/[locale]/(legal)/eula/page.tsx">

<violation number="1" location="web/app/[locale]/(legal)/eula/page.tsx:6">
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.</violation>
</file>

<file name="web/app/[locale]/(legal)/terms-of-service/page.tsx">

<violation number="1" location="web/app/[locale]/(legal)/terms-of-service/page.tsx:6">
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.</violation>
</file>

<file name="web/app/[locale]/(legal)/privacy-policy/page.tsx">

<violation number="1" location="web/app/[locale]/(legal)/privacy-policy/page.tsx:7">
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.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

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

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

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

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 991ecc3264

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread web/proxy.ts Outdated
const legalPages = ["/privacy-policy", "/terms-of-service", "/eula"];
const { pathname } = request.nextUrl;
for (const page of legalPages) {
if (pathname.endsWith(page) && pathname !== page) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Limit legal-page redirect to exact locale routes

The new redirect check uses pathname.endsWith(page), which also matches unrelated URLs like /foo/privacy-policy or /docs/eula and permanently redirects them to the legal page instead of returning a 404. This creates soft-404 behavior and can pollute crawl/indexing signals with unexpected 301s from typo or stale links. The redirect should only trigger for true localized legal routes (e.g. /<locale>/privacy-policy) rather than any path suffix match.

Useful? React with 👍 / 👎.

endsWith matched too broadly (e.g. /docs/eula). Now only redirects
when the path after the first segment is an exact legal page match.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: cc1aef4630

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread web/proxy.ts
Comment on lines +24 to +27
if (legalPages.has(rest)) {
const url = request.nextUrl.clone();
url.pathname = rest;
return NextResponse.redirect(url, 301);

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 Prevent locale redirect loop on legal pages

This 301 strips locale prefixes from legal routes (e.g. /ja/privacy-policy → /privacy-policy), but createMiddleware(routing) is configured with localePrefix: "as-needed" and default locale detection behavior, so a non-English locale cookie/header can immediately redirect /privacy-policy back to /ja/privacy-policy. In that context users and crawlers can hit a permanent redirect loop on legal pages instead of reaching content; make this path bypass locale detection or handle legal-page localization in a way that cannot bounce between prefixed and unprefixed URLs.

Useful? React with 👍 / 👎.

Without this, a Japanese user hitting /privacy-policy could be
redirected by next-intl to /ja/privacy-policy, which our middleware
redirects back to /privacy-policy, creating a loop.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

1 issue found across 1 file (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="web/proxy.ts">

<violation number="1" location="web/proxy.ts:23">
P1: Returning `NextResponse.next()` for `/privacy-policy`, `/terms-of-service`, and `/eula` bypasses locale rewriting, but these pages only exist under `app/[locale]`, so canonical legal URLs can break.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Comment thread web/proxy.ts Outdated
Pages live under app/[locale]/, so skipping next-intl entirely
would break route resolution. Rewrite to /en/privacy-policy etc.
so Next.js can resolve the [locale] segment correctly.
@lawrencecchen
lawrencecchen merged commit 46589f5 into main Mar 26, 2026
14 checks passed
@lawrencecchen
lawrencecchen deleted the task-fix-seo-indexing branch March 26, 2026 22:10
@coderabbitai coderabbitai Bot mentioned this pull request Mar 31, 2026
3 tasks
bn-l pushed a commit to bn-l/cmux that referenced this pull request Apr 3, 2026
…flow-ai#2193)

* Fix SEO indexing: add hreflang, canonicals, sitemap per-locale entries

Google Search Console showed 380 not-indexed vs 86 indexed pages.
Root causes: missing hreflang tags on rendered pages (only in sitemap),
no canonical on homepage, inconsistent canonicals wiping parent hreflang,
sitemap only listing English URLs, trailing slash duplicates, and
_next/static chunks being crawled as pages.

Changes:
- Add buildAlternates() utility for consistent canonical + hreflang
- Add hreflang tags to all pages via alternates.languages in metadata
- Add self-referencing canonical URLs to every page (homepage had none)
- Expand sitemap to emit separate entries for each locale
- Add missing /docs/custom-commands to sitemap
- Remove skipTrailingSlashRedirect to normalize trailing slashes
- Block /_next/ in robots.txt to stop chunk crawling

* Add per-page alternates to docs sub-pages and blog index

Docs sub-pages and blog index only returned title/description in
generateMetadata, so they inherited the parent layout's alternates
(pointing to /docs or /blog). Now each page sets its own
buildAlternates() with the correct path so canonical and hreflang
point to the actual page URL.

* Derive openGraph.url from buildAlternates to avoid drift

* Redirect non-English legal pages to English, remove from sitemap

Legal pages (privacy policy, TOS, EULA) are untranslated English content.
Serving them under every locale creates 54 duplicate URLs. Now:
- Middleware 301-redirects /ja/privacy-policy etc. to /privacy-policy
- Sitemap only includes English URLs for legal pages (no locale variants)
- Legal page metadata uses static English-only canonical

* Fix legal page redirect to only match /<locale>/<page> paths

endsWith matched too broadly (e.g. /docs/eula). Now only redirects
when the path after the first segment is an exact legal page match.

* Skip next-intl for legal pages to prevent locale redirect loop

Without this, a Japanese user hitting /privacy-policy could be
redirected by next-intl to /ja/privacy-policy, which our middleware
redirects back to /privacy-policy, creating a loop.

* Rewrite legal pages to /en/ instead of NextResponse.next()

Pages live under app/[locale]/, so skipping next-intl entirely
would break route resolution. Rewrite to /en/privacy-policy etc.
so Next.js can resolve the [locale] segment correctly.

---------

Co-authored-by: Lawrence Chen <lawrencecchen@users.noreply.github.com>

This branch was successfully deployed

1 active deployment
Preview — fe8c44e6 Deployed Mar 26, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant