Repository navigation
web: add /pricing page (Free / Pro / Enterprise) - #6791
6 commits merged into
Conversation
Free / Pro ($30/mo) / Enterprise tiers with a compare table, Cloud VM sizes table, and FAQ. Leans on cmux-native cloud value: Cloud VMs billed by active compute-hour, cmux Vault session backup + search, unlimited session history, a model gateway with usage/cost analytics, and hosted or self-hosted networking to reach your Mac from the iOS app. Enterprise adds SSO/SAML, self-hosting, audit logs, and SOC 2. Wires Pricing into nav, mobile drawer, and footer. Strings localized in en.json and ja.json. Note: the Pro CTA currently points at the download (no public checkout URL yet); PRO_CTA_URL is the single swap point when billing is live.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Note Reviews pausedIt 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 Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughAdds a localized pricing page with translated metadata and content, plus pricing links in the main nav, mobile drawer, and footer. ChangesPricing page and navigation
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Poem
Important Pre-merge checks failedPlease resolve all errors before merging. Addressing warnings is optional. ❌ Failed checks (1 error, 2 warnings)
✅ Passed checks (22 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 |
| <a href="mailto:founders@manaflow.ai" className={linkClass}> | ||
| {chunks} | ||
| </a> | ||
| ), |
There was a problem hiding this comment.
FAQ help wrong email domain
Medium Severity
The pricing FAQ “email us” link uses founders@manaflow.ai, while SALES_EMAIL, the Enterprise CTA, the site footer contact link, and other marketing pages use founders@manaflow.com. Users asking questions from this section may mail the wrong address.
Reviewed by Cursor Bugbot for commit 1c379a7. Configure here.
| price={t("free.price")} | ||
| period={t("perMonth")} | ||
| > | ||
| <PrimaryLink href={DOWNLOAD_URL}>{t("free.cta")}</PrimaryLink> |
There was a problem hiding this comment.
Free download skips confirmation flow
Low Severity
The Free plan CTA links straight to DOWNLOAD_URL (the GitHub DMG). Site download CTAs normally go through DOWNLOAD_CONFIRMATION_HREF so the in-app confirmation page starts the download, avoids blocked popups, and keeps users on cmux.com with a retry path.
Reviewed by Cursor Bugbot for commit 1c379a7. Configure here.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 1c379a7af0
ℹ️ 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".
| } | ||
| if (value === "false") { | ||
| return ( | ||
| <td className={`${base} text-muted`} aria-label="Not included"> |
There was a problem hiding this comment.
Localize the false-state aria label
In Japanese and any other non-English locale, this screen-reader label stays English because it bypasses next-intl, which violates /workspace/cmux/AGENTS.md's rule that all user-facing strings must be localized. Please add a pricing message such as notIncluded and pass the translated value into CompareCell so the accessible text is localized along with the visible page.
Useful? React with 👍 / 👎.
| }; | ||
| } | ||
|
|
||
| export default function PricingPage() { |
There was a problem hiding this comment.
Because web/app/sitemap.ts builds the sitemap from a hard-coded paths array and there is still no /pricing entry there (rg 'path: "/pricing"' web/app/sitemap.ts returns nothing), this new public marketing route is omitted from every generated localized sitemap and alternate URL set. Add /pricing to that list so crawlers can discover the page consistently with the other nav-linked pages.
Useful? React with 👍 / 👎.
Greptile SummaryAdds a
Confidence Score: 4/5Safe to merge for English and Japanese visitors; the 18 remaining supported locales will hit missing-key errors on the nav, footer, and the pricing page itself until translations are backfilled. The pricing page implementation is clean — feature-flagged Vault copy, single swap-point CTA constants, correct SEO metadata generation — but translations were only added for two of the twenty locales declared in
Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[User visits /pricing] --> B{Locale?}
B -->|en or ja| C[Load pricing namespace from messages]
B -->|18 other locales| D[Missing-key error / broken render]
C --> E[generateMetadata: SEO title + description]
E --> F{SHOW_VAULT?}
F -->|false| G[metaDescriptionNoVault]
F -->|true| H[metaDescription with Vault]
C --> I[PricingPage renders]
I --> J[4 Plan Cards: Free / Pro / Team / Enterprise]
I --> K{SHOW_VAULT?}
K -->|false| L[Filter vault rows from compare table]
K -->|false| M[Filter vault FAQ items]
K -->|true| N[Show all rows and FAQ]
J --> O[Free CTA → DOWNLOAD_URL]
J --> P[Pro CTA → PRO_CTA_URL = DOWNLOAD_URL]
J --> Q[Team CTA → TEAM_CTA_URL = DOWNLOAD_URL]
J --> R[Enterprise CTA → mailto:SALES_EMAIL]
I --> S[Cloud VM sizes table]
I --> T[FAQ section]
T --> U[Help mailto → founders@manaflow.ai ⚠️ domain differs from SALES_EMAIL]
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
A[User visits /pricing] --> B{Locale?}
B -->|en or ja| C[Load pricing namespace from messages]
B -->|18 other locales| D[Missing-key error / broken render]
C --> E[generateMetadata: SEO title + description]
E --> F{SHOW_VAULT?}
F -->|false| G[metaDescriptionNoVault]
F -->|true| H[metaDescription with Vault]
C --> I[PricingPage renders]
I --> J[4 Plan Cards: Free / Pro / Team / Enterprise]
I --> K{SHOW_VAULT?}
K -->|false| L[Filter vault rows from compare table]
K -->|false| M[Filter vault FAQ items]
K -->|true| N[Show all rows and FAQ]
J --> O[Free CTA → DOWNLOAD_URL]
J --> P[Pro CTA → PRO_CTA_URL = DOWNLOAD_URL]
J --> Q[Team CTA → TEAM_CTA_URL = DOWNLOAD_URL]
J --> R[Enterprise CTA → mailto:SALES_EMAIL]
I --> S[Cloud VM sizes table]
I --> T[FAQ section]
T --> U[Help mailto → founders@manaflow.ai ⚠️ domain differs from SALES_EMAIL]
Reviews (6): Last reviewed commit: "pricing: make the sticky compare header ..." | Re-trigger Greptile |
| // Pro CTA points at the download today; swap this for the real checkout URL | ||
| // when the billing flow is public. | ||
| const PRO_CTA_URL = DOWNLOAD_URL; | ||
| const SALES_EMAIL = "founders@manaflow.com"; |
There was a problem hiding this comment.
Email domain mismatch between
SALES_EMAIL and the FAQ help link
SALES_EMAIL is defined as founders@manaflow.com (used for the Enterprise "Contact sales" CTA), but line 211 hardcodes founders@manaflow.ai for the FAQ help mailto link. One of the two domains is wrong, so visitors who click the FAQ help email will reach a different inbox — or a non-existent one — than those who click "Contact sales".
| email: (chunks) => ( | ||
| <a href="mailto:founders@manaflow.ai" className={linkClass}> |
There was a problem hiding this comment.
The FAQ help section hardcodes a different domain (
manaflow.ai) than SALES_EMAIL (manaflow.com). Use the constant so both CTAs always point to the same address and only one place needs updating when the address changes.
| email: (chunks) => ( | |
| <a href="mailto:founders@manaflow.ai" className={linkClass}> | |
| email: (chunks) => ( | |
| <a href={`mailto:${SALES_EMAIL}`} className={linkClass}> |
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!
There was a problem hiding this comment.
Actionable comments posted: 4
🤖 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]/pricing/page.tsx:
- Around line 339-357: Update CompareCell so the true/false branches expose
localized spoken labels instead of only icon/English text. Use next-intl in
pricing/page.tsx to fetch translations for the included and not included states,
then set those labels as the accessible text for the CheckIcon branch and the
excluded td aria-label. Keep the visual output unchanged, and ensure CompareCell
continues to use the localized strings for both screen readers and any fallback
text.
- Around line 200-208: The “GitHub issue” CTA in the pricing page link mapping
currently points to the repository root instead of an issues destination. Update
the GitHub anchor inside the localized link renderer in the pricing page to use
the repository’s issues page or new-issue URL so it matches the help copy; keep
the same link component structure and adjust only the href in the GitHub chunk
handler.
- Around line 317-357: CompareRow/CompareCell are using translatable string
sentinels to decide checkmark vs dash rendering, which is brittle. Update the
pricing compare data and the CompareCell logic to use a non-localized state such
as booleans or a fixed enum for included/excluded, and keep only display text
localized. Make sure the rendering branch in CompareCell no longer depends on
translated "true"/"false" values from the messages JSON files.
- Around line 11-12: The pricing page is using two different contact emails, so
the Enterprise CTA and help link route users to different inboxes; unify them
behind a single source of truth. Update the shared email constant used in
pricing page rendering (including the Enterprise CTA and the help/contact link
paths referenced elsewhere in the file) so all contact points resolve to the
same address, and replace any hardcoded alternate email with that shared
constant.
🪄 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: 46ae59f5-d984-4fce-8e46-ca3f644a1dcd
📒 Files selected for processing (6)
web/app/[locale]/components/nav-links.tsxweb/app/[locale]/components/site-footer.tsxweb/app/[locale]/components/site-header.tsxweb/app/[locale]/pricing/page.tsxweb/messages/en.jsonweb/messages/ja.json
| const SALES_EMAIL = "founders@manaflow.com"; | ||
|
|
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Unify the sales/contact email.
The Enterprise CTA points to founders@manaflow.com, while the help link points to founders@manaflow.ai. At least one of these paths sends users to a different inbox than the rest of the page. Route both through one shared constant/source of truth.
Also applies to: 79-81, 210-211
🤖 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]/pricing/page.tsx around lines 11 - 12, The pricing page is
using two different contact emails, so the Enterprise CTA and help link route
users to different inboxes; unify them behind a single source of truth. Update
the shared email constant used in pricing page rendering (including the
Enterprise CTA and the help/contact link paths referenced elsewhere in the file)
so all contact points resolve to the same address, and replace any hardcoded
alternate email with that shared constant.
| github: (chunks) => ( | ||
| <a | ||
| href="https://github.com/manaflow-ai/cmux" | ||
| target="_blank" | ||
| rel="noopener noreferrer" | ||
| className={linkClass} | ||
| > | ||
| {chunks} | ||
| </a> |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Point the “GitHub issue” CTA at issues, not the repo root.
The localized help copy says users can open a GitHub issue, but this link lands on the repository homepage instead of the issues flow. Use the issues list or new-issue URL so the destination matches the promised action.
🤖 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]/pricing/page.tsx around lines 200 - 208, The “GitHub issue”
CTA in the pricing page link mapping currently points to the repository root
instead of an issues destination. Update the GitHub anchor inside the localized
link renderer in the pricing page to use the repository’s issues page or
new-issue URL so it matches the help copy; keep the same link component
structure and adjust only the href in the GitHub chunk handler.
| function CompareCell({ value }: { value: string }) { | ||
| const base = "px-4 py-3 text-left align-top"; | ||
| if (value === "true") { | ||
| return ( | ||
| <td className={base}> | ||
| <span className="inline-flex text-foreground"> | ||
| <CheckIcon inline /> | ||
| </span> | ||
| </td> | ||
| ); | ||
| } | ||
| if (value === "false") { | ||
| return ( | ||
| <td className={`${base} text-muted`} aria-label="Not included"> | ||
| <span aria-hidden="true">–</span> | ||
| </td> | ||
| ); | ||
| } | ||
| return <td className={`${base} text-[13px] text-muted`}>{value}</td>; |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Give compare cells localized accessible text.
The included branch renders only an aria-hidden checkmark, so those cells are silent to screen readers, and the excluded branch hardcodes English aria-label="Not included". Please provide localized spoken labels for both states and pass them through next-intl. As per path instructions, “Web UI text and metadata added/changed must be fully internationalized for every supported locale via next-intl.”
🤖 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]/pricing/page.tsx around lines 339 - 357, Update CompareCell
so the true/false branches expose localized spoken labels instead of only
icon/English text. Use next-intl in pricing/page.tsx to fetch translations for
the included and not included states, then set those labels as the accessible
text for the CheckIcon branch and the excluded td aria-label. Keep the visual
output unchanged, and ensure CompareCell continues to use the localized strings
for both screen readers and any fallback text.
Source: Path instructions
Vault (cloud session backup, cross-session search, unlimited cross-machine history) isn't shipped yet, so its pricing copy is hidden behind a single SHOW_VAULT flag in page.tsx. Flip to true to restore every Vault entry. - Pro Vault bullets moved to pricing.pro.vaultFeatures (spliced back when on) - Vault-dependent compare rows (session history, Vault backup, cross-session search) and the Vault FAQ marked "vault": true and filtered out - meta description has a no-Vault variant - en.json and ja.json kept in sync Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 5508109888
ℹ️ 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".
| <td className={base}> | ||
| <span className="inline-flex text-foreground"> | ||
| <CheckIcon inline /> |
There was a problem hiding this comment.
Give included cells an accessible label
For comparison rows whose value is "true", the cell's only content is CheckIcon, and that SVG sets aria-hidden="true", so screen-reader users encounter an empty cell for every included feature in the pricing table. Add localized accessible text or an aria-label for the included state, analogous to the false-state cell.
Useful? React with 👍 / 👎.
| return { | ||
| title: t("metaTitle"), | ||
| description: SHOW_VAULT ? t("metaDescription") : t("metaDescriptionNoVault"), | ||
| alternates: buildAlternates(locale, "/pricing"), |
There was a problem hiding this comment.
Add pricing messages before advertising every locale
This call uses buildAlternates with the full web/i18n/routing.ts locale list, and the new localized links expose /pricing under every locale, but this commit only adds pricing/nav.pricing/footer.pricing to en.json and ja.json. Because web/i18n/request.ts deep-merges missing locale keys from English, routes such as /de/pricing and /fr/pricing render English pricing copy while being advertised as localized hreflang pages; either add the new keys to all routed locale bundles or restrict the page/alternates to the locales that have translations.
Useful? React with 👍 / 👎.
New Team tier between Pro and Enterprise: per-seat at $35/user/month, billed to the whole team on one invoice. Features: unified billing, centralized seat management, pooled Cloud VM hours, shared team rules/templates, team-wide model gateway with per-member analytics, centralized admin, priority support. - 4th tier card; tier grid is now md:grid-cols-2 lg:grid-cols-4 - Team column added to the compare table + a "Unified billing and seat management" row; Enterprise now builds on Team - perUserMonth string; team FAQ answer updated - en.json and ja.json kept in sync Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 1cd4250ac7
ℹ️ 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".
| </a> | ||
| ), | ||
| email: (chunks) => ( | ||
| <a href="mailto:founders@manaflow.ai" className={linkClass}> |
There was a problem hiding this comment.
Reuse the founders contact address
When users click the pricing FAQ's “email us” link, it opens founders@manaflow.ai, while this same file defines SALES_EMAIL as founders@manaflow.com and the rest of the site/repo uses the .com founders alias. If only the .com mailbox is monitored, pricing/support inquiries from this new page will be misdirected; reuse SALES_EMAIL here instead.
Useful? React with 👍 / 👎.
- Drop the "Compare plans" section (heading, divider, and comparison table) and its now-dead helpers (ColumnHead, CompareCell, CompareRow type, compareRows read). The compare.* i18n data is left in place, unused. - Tier card row is now sticky under the site header: sticky top-12 (clears the 48px h-12 header) with z-20 (below the header's z-30) and a solid background. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
There are 3 total unresolved issues (including 2 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 5c53722. Configure here.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 5c5372229b
ℹ️ 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".
|
|
||
| {/* Tier cards. Sticky below the 48px (h-12) site header; lower z-index | ||
| than the header (z-30) so the header always wins when they overlap. */} | ||
| <div className="mt-10 grid gap-5 md:grid-cols-2 lg:grid-cols-4 items-stretch sticky top-12 z-20 bg-background pb-5"> |
There was a problem hiding this comment.
Keep pricing cards from sticking on narrow screens
On narrow screens this grid is one column, so the four plan cards are taller than the viewport. Because the entire grid is sticky top-12, once the user scrolls past the heading the whole card stack pins below the header and sits over the Cloud VM sizes/FAQ content; lower cards and the following sections cannot scroll normally. Scope the sticky behavior to a breakpoint where the card grid fits, or make the mobile stack non-sticky.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 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]/pricing/page.tsx:
- Around line 66-68: The sticky pricing card row in the pricing page is
affecting narrow/mobile layouts where the cards stack into a single column and
can obscure later content. Update the container in the pricing page so the
sticky behavior only applies on wide breakpoints, keeping the stacked mobile
layout non-sticky while preserving the existing sticky treatment for desktop
grid layouts. Use the pricing page’s tier-cards wrapper as the target for the
responsive class change.
🪄 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: 079064f6-26fa-4b6f-9302-e2e78b08b469
📒 Files selected for processing (1)
web/app/[locale]/pricing/page.tsx
| {/* Tier cards. Sticky below the 48px (h-12) site header; lower z-index | ||
| than the header (z-30) so the header always wins when they overlap. */} | ||
| <div className="mt-10 grid gap-5 md:grid-cols-2 lg:grid-cols-4 items-stretch sticky top-12 z-20 bg-background pb-5"> |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Scope the sticky pricing row to wide layouts.
sticky top-12 applies on mobile too, where this container is a 1-column stack of cards. That pins the whole stack under the header and can cover the VM sizes/FAQ content for most of the scroll.
Suggested fix
- <div className="mt-10 grid gap-5 md:grid-cols-2 lg:grid-cols-4 items-stretch sticky top-12 z-20 bg-background pb-5">
+ <div className="mt-10 grid gap-5 md:grid-cols-2 lg:grid-cols-4 items-stretch bg-background pb-5 lg:sticky lg:top-12 lg:z-20">📝 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.
| {/* Tier cards. Sticky below the 48px (h-12) site header; lower z-index | |
| than the header (z-30) so the header always wins when they overlap. */} | |
| <div className="mt-10 grid gap-5 md:grid-cols-2 lg:grid-cols-4 items-stretch sticky top-12 z-20 bg-background pb-5"> | |
| {/* Tier cards. Sticky below the 48px (h-12) site header; lower z-index | |
| than the header (z-30) so the header always wins when they overlap. */} | |
| <div className="mt-10 grid gap-5 md:grid-cols-2 lg:grid-cols-4 items-stretch bg-background pb-5 lg:sticky lg:top-12 lg:z-20"> |
🤖 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]/pricing/page.tsx around lines 66 - 68, The sticky pricing
card row in the pricing page is affecting narrow/mobile layouts where the cards
stack into a single column and can obscure later content. Update the container
in the pricing page so the sticky behavior only applies on wide breakpoints,
keeping the stacked mobile layout non-sticky while preserving the existing
sticky treatment for desktop grid layouts. Use the pricing page’s tier-cards
wrapper as the target for the responsive class change.
…tainer - Restore the comparison table (I had removed it) but without the "Compare plans" heading and the top divider, per request. - Make the table HEADER ROW sticky (sticky top-12 under the 48px h-12 site header, z-20 below the header's z-30), not the tier cards. Tier cards are no longer sticky. - Widen the page container max-w-5xl -> max-w-6xl (matches the site header). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 8b9b058815
ℹ️ 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".
| const PRO_CTA_URL = DOWNLOAD_URL; | ||
| // Team is per-seat ($35/user/month). Install is still the entry point, so the | ||
| // Team CTA points at the download today; swap for the team checkout URL once | ||
| // the billing flow is public. | ||
| const TEAM_CTA_URL = DOWNLOAD_URL; |
There was a problem hiding this comment.
Use the shared download flow for pricing CTAs
When users click the pricing page's temporary install CTAs, these URLs go straight to the GitHub DMG instead of the existing /download/confirmation?dl=1 flow used by DownloadButton; the same direct DOWNLOAD_URL is also used for the Free CTA below. That bypasses the guarded confirmation-page download and the cmuxterm_download_clicked PostHog event, so pricing conversions become the only main download entry point that is not tracked and does not show the fallback/help page if the download does not start. Point these install CTAs at DOWNLOAD_CONFIRMATION_HREF or reuse the shared download button until checkout exists.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 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]/pricing/page.tsx:
- Around line 144-145: The compare row list in the pricing page is using an
index-based key in the compareRows map, which can cause incorrect row reuse when
translation content or SHOW_VAULT filtering changes the order. Update the
mapping in the pricing page render to use a stable identifier from each row,
such as row.label, or add a dedicated id field to the compare row data and use
that in the key instead of i.
- Around line 121-160: Restore a visible localized heading for the compare table
in the pricing page section. Use the existing pricing.compare.title translation
in the compare plans section around the table rendered by the pricing page
component, and wire it into the markup as the section heading and/or table
caption so the grid is clearly labeled. Keep the change in the compare table
block that uses compareRows, ColumnHead, and CompareCell.
🪄 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: 4375ebef-2338-4986-b736-dc20376ee10d
📒 Files selected for processing (1)
web/app/[locale]/pricing/page.tsx
| {/* Compare plans (header row is sticky under the 48px h-12 site header) */} | ||
| <section className="mt-16"> | ||
| <div className="overflow-x-auto"> | ||
| <table className="w-full border-collapse text-[15px]"> | ||
| <thead> | ||
| <tr> | ||
| <th className="sticky top-12 z-20 bg-background border-b border-border py-3 pr-4 text-left align-bottom font-medium min-w-[12rem]" /> | ||
| <ColumnHead name={t("free.name")} price={t("free.price")} /> | ||
| <ColumnHead | ||
| name={t("pro.name")} | ||
| price={`${t("pro.price")}${t("perMonth")}`} | ||
| /> | ||
| <ColumnHead | ||
| name={t("team.name")} | ||
| price={`${t("team.price")}${t("perUserMonth")}`} | ||
| /> | ||
| <ColumnHead | ||
| name={t("enterprise.name")} | ||
| price={t("enterprise.price")} | ||
| /> | ||
| </tr> | ||
| </thead> | ||
| <tbody> | ||
| {compareRows.map((row, i) => ( | ||
| <tr key={i} className="border-b border-border"> | ||
| <th | ||
| scope="row" | ||
| className="py-3 pr-4 text-left font-normal align-top" | ||
| > | ||
| {row.label} | ||
| </th> | ||
| <CompareCell value={row.free} /> | ||
| <CompareCell value={row.pro} /> | ||
| <CompareCell value={row.team} /> | ||
| <CompareCell value={row.enterprise} /> | ||
| </tr> | ||
| ))} | ||
| </tbody> | ||
| </table> | ||
| </div> |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Restore a localized heading for the compare table.
This section renders a large pricing table without any visible heading or caption, so the page jumps from the cards straight into an unlabeled grid. The pricing.compare.title translation already exists; wire it back in as the section heading (and ideally the table caption) so the table has an accessible label and a clear scan point.
Suggested fix
- <section className="mt-16">
+ <section
+ className="mt-16"
+ aria-labelledby="pricing-compare-title"
+ >
+ <h2
+ id="pricing-compare-title"
+ className="mb-3 text-xs font-medium tracking-tight text-muted"
+ >
+ {t("compare.title")}
+ </h2>
<div className="overflow-x-auto">
<table className="w-full border-collapse text-[15px]">
+ <caption className="sr-only">{t("compare.title")}</caption>
<thead>📝 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.
| {/* Compare plans (header row is sticky under the 48px h-12 site header) */} | |
| <section className="mt-16"> | |
| <div className="overflow-x-auto"> | |
| <table className="w-full border-collapse text-[15px]"> | |
| <thead> | |
| <tr> | |
| <th className="sticky top-12 z-20 bg-background border-b border-border py-3 pr-4 text-left align-bottom font-medium min-w-[12rem]" /> | |
| <ColumnHead name={t("free.name")} price={t("free.price")} /> | |
| <ColumnHead | |
| name={t("pro.name")} | |
| price={`${t("pro.price")}${t("perMonth")}`} | |
| /> | |
| <ColumnHead | |
| name={t("team.name")} | |
| price={`${t("team.price")}${t("perUserMonth")}`} | |
| /> | |
| <ColumnHead | |
| name={t("enterprise.name")} | |
| price={t("enterprise.price")} | |
| /> | |
| </tr> | |
| </thead> | |
| <tbody> | |
| {compareRows.map((row, i) => ( | |
| <tr key={i} className="border-b border-border"> | |
| <th | |
| scope="row" | |
| className="py-3 pr-4 text-left font-normal align-top" | |
| > | |
| {row.label} | |
| </th> | |
| <CompareCell value={row.free} /> | |
| <CompareCell value={row.pro} /> | |
| <CompareCell value={row.team} /> | |
| <CompareCell value={row.enterprise} /> | |
| </tr> | |
| ))} | |
| </tbody> | |
| </table> | |
| </div> | |
| {/* Compare plans (header row is sticky under the 48px h-12 site header) */} | |
| <section | |
| className="mt-16" | |
| aria-labelledby="pricing-compare-title" | |
| > | |
| <h2 | |
| id="pricing-compare-title" | |
| className="mb-3 text-xs font-medium tracking-tight text-muted" | |
| > | |
| {t("compare.title")} | |
| </h2> | |
| <div className="overflow-x-auto"> | |
| <table className="w-full border-collapse text-[15px]"> | |
| <caption className="sr-only">{t("compare.title")}</caption> | |
| <thead> | |
| <tr> | |
| <th className="sticky top-12 z-20 bg-background border-b border-border py-3 pr-4 text-left align-bottom font-medium min-w-[12rem]" /> | |
| <ColumnHead name={t("free.name")} price={t("free.price")} /> | |
| <ColumnHead | |
| name={t("pro.name")} | |
| price={`${t("pro.price")}${t("perMonth")}`} | |
| /> | |
| <ColumnHead | |
| name={t("team.name")} | |
| price={`${t("team.price")}${t("perUserMonth")}`} | |
| /> | |
| <ColumnHead | |
| name={t("enterprise.name")} | |
| price={t("enterprise.price")} | |
| /> | |
| </tr> | |
| </thead> | |
| <tbody> | |
| {compareRows.map((row, i) => ( | |
| <tr key={i} className="border-b border-border"> | |
| <th | |
| scope="row" | |
| className="py-3 pr-4 text-left font-normal align-top" | |
| > | |
| {row.label} | |
| </th> | |
| <CompareCell value={row.free} /> | |
| <CompareCell value={row.pro} /> | |
| <CompareCell value={row.team} /> | |
| <CompareCell value={row.enterprise} /> | |
| </tr> | |
| ))} | |
| </tbody> | |
| </table> | |
| </div> |
🧰 Tools
🪛 Biome (2.5.0)
[error] 145-145: Avoid using the index of an array as key property in an element.
(lint/suspicious/noArrayIndexKey)
🤖 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]/pricing/page.tsx around lines 121 - 160, Restore a visible
localized heading for the compare table in the pricing page section. Use the
existing pricing.compare.title translation in the compare plans section around
the table rendered by the pricing page component, and wire it into the markup as
the section heading and/or table caption so the grid is clearly labeled. Keep
the change in the compare table block that uses compareRows, ColumnHead, and
CompareCell.
| {compareRows.map((row, i) => ( | ||
| <tr key={i} className="border-b border-border"> |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Use a stable key for compare rows.
compareRows is translation-driven and filtered by SHOW_VAULT, so key={i} will reuse the wrong row DOM when items are inserted, removed, or reordered. Use a stable field such as row.label, or better, add a dedicated id in the message data.
🧰 Tools
🪛 Biome (2.5.0)
[error] 145-145: Avoid using the index of an array as key property in an element.
(lint/suspicious/noArrayIndexKey)
🤖 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]/pricing/page.tsx around lines 144 - 145, The compare row
list in the pricing page is using an index-based key in the compareRows map,
which can cause incorrect row reuse when translation content or SHOW_VAULT
filtering changes the order. Update the mapping in the pricing page render to
use a stable identifier from each row, such as row.label, or add a dedicated id
field to the compare row data and use that in the key instead of i.
Source: Linters/SAST tools
The previous attempt didn't pin in WebKit: the overflow-x-auto wrapper became a scroll container on both axes, so the sticky header anchored to that div (which doesn't scroll) instead of the page. Remove the wrapper and switch the table to border-separate (border-spacing-0), which sticky table headers need; move row separators onto the cells. Verified on localhost: header pins at top:48px under the 48px h-12 site header. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: a9a8e02f4a
ℹ️ 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".
| No overflow-x wrapper: an overflow container becomes a scroll context | ||
| on both axes and would anchor the sticky header to itself instead of | ||
| the page. */} | ||
| <section className="mt-16"> | ||
| <table className="w-full border-separate border-spacing-0 text-[15px]"> |
There was a problem hiding this comment.
Contain compare-table overflow on mobile
On narrow screens, this five-column comparison table cannot fit the viewport (the label column alone has a 12rem minimum and each plan column adds padding), but this section intentionally omits the overflow wrapper used by the Cloud VM sizes table below. Visiting /pricing on a phone therefore makes the entire page horizontally pan instead of containing the scroll to the comparison area, which also affects the FAQ/footer after the table; add a mobile-only overflow wrapper or alternate responsive layout while keeping the desktop sticky behavior.
Useful? React with 👍 / 👎.
The /pricing page from #6791 replaces the hand-rolled one: PRO_CTA_URL points at /api/billing/checkout behind a SHOW_CHECKOUT flag (on in local dev, off in production, NEXT_PUBLIC_CMUX_CHECKOUT_ENABLED forces either way, mirroring the page's SHOW_VAULT pattern), the post-checkout banner mounts above the tier cards, and the Pro card shows $20/month with a billed-yearly note so the page matches the hosted checkout's yearly default. Adds the corner Pro badge to the sidebar footer (opens the same shared pricing URL as Settings, palette, and Help menu). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>


Adds a
/pricingroute to the marketing site.Tiers: Free ($0) / Pro ($30/mo) / Enterprise (Custom), as three equal-height cards, plus a compare table, a Cloud VM sizes table, and an FAQ.
Positioning: leans on cmux-native cloud value rather than a generic enterprise checklist:
Wiring: Pricing added to nav, mobile drawer, and footer. Strings localized in
en.jsonandja.json.Known follow-up: the Pro CTA currently points at the download (no public checkout URL yet).
PRO_CTA_URLinpage.tsxis the single swap point once billing is live. Prices and included-hours are drafts and easy to tune.Need help on this PR? Tag
/codesmithwith what you need. Autofix is disabled.Note
Low Risk
Marketing-site and i18n-only changes with no billing or auth logic; main caveat is published draft prices and Pro/Team CTAs routing to download instead of checkout.
Overview
Adds a new
/pricingmarketing route with Free, Pro ($30/mo), Team ($35/user/mo), and Enterprise tier cards, a sticky-header compare table, Cloud VM size rates, and an FAQ, all driven by newpricingstrings inen.jsonandja.json(including SEO metadata viabuildAlternates).Pricing is linked from the main nav (
nav-links), the mobile drawer (site-header), and the footer product column. Pro and Team CTAs temporarily useDOWNLOAD_URLviaPRO_CTA_URL/TEAM_CTA_URLuntil checkout exists; Enterprise usesmailto:sales.cmux Vault copy (Pro bullets, compare rows, FAQ, meta description) is hidden while
SHOW_VAULTisfalse, so the public page matches what is not shipped yet.Reviewed by Cursor Bugbot for commit a9a8e02. Bugbot is set up for automated code reviews on this repo. Configure here.
Summary by cubic
Adds a localized
/pricingpage with Free, Pro ($30/mo), Team ($35/user/mo), and Enterprise tiers. The compare table is back with a sticky header that now pins correctly under the site header across browsers; page includes Cloud VM sizes and an FAQ, with Vault copy gated bySHOW_VAULT.New Features
/pricingroute with 4 tier cards.en.json,ja.json).Migration
PRO_CTA_URLandTEAM_CTA_URLwhen checkout is live.Written for commit a9a8e02. Summary will update on new commits.
Summary by CodeRabbit