+ {t(`posts.${slug}.title`)} +
+ ++ {t(`posts.${slug}.summary`)} +
+ +diff --git a/web/app/(legal)/eula/page.tsx b/web/app/[locale]/(legal)/eula/page.tsx similarity index 99% rename from web/app/(legal)/eula/page.tsx rename to web/app/[locale]/(legal)/eula/page.tsx index 114f32bb..85676b2d 100644 --- a/web/app/(legal)/eula/page.tsx +++ b/web/app/[locale]/(legal)/eula/page.tsx @@ -8,7 +8,7 @@ export const metadata: Metadata = { export default function EulaPage() { return ( <> -
Last updated: December 2, 2025
diff --git a/web/app/(legal)/layout.tsx b/web/app/[locale]/(legal)/layout.tsx similarity index 100% rename from web/app/(legal)/layout.tsx rename to web/app/[locale]/(legal)/layout.tsx diff --git a/web/app/(legal)/privacy-policy/page.tsx b/web/app/[locale]/(legal)/privacy-policy/page.tsx similarity index 97% rename from web/app/(legal)/privacy-policy/page.tsx rename to web/app/[locale]/(legal)/privacy-policy/page.tsx index 982b07f6..fc945d36 100644 --- a/web/app/(legal)/privacy-policy/page.tsx +++ b/web/app/[locale]/(legal)/privacy-policy/page.tsx @@ -1,4 +1,5 @@ import type { Metadata } from "next"; +import { Link } from "../../../../i18n/navigation"; export const metadata: Metadata = { title: "Privacy Policy — cmux", @@ -29,7 +30,7 @@ export default function PrivacyPolicyPage() {
By using our Service, you accept this Privacy Policy and our{" "} - Terms of Service, and you consent to + Terms of Service, and you consent to our collection, storage, use and disclosure of your information as described here.
diff --git a/web/app/(legal)/terms-of-service/page.tsx b/web/app/[locale]/(legal)/terms-of-service/page.tsx similarity index 100% rename from web/app/(legal)/terms-of-service/page.tsx rename to web/app/[locale]/(legal)/terms-of-service/page.tsx diff --git a/web/app/assets/images.d.ts b/web/app/[locale]/assets/images.d.ts similarity index 100% rename from web/app/assets/images.d.ts rename to web/app/[locale]/assets/images.d.ts diff --git a/web/app/assets/landing-image.png b/web/app/[locale]/assets/landing-image.png similarity index 100% rename from web/app/assets/landing-image.png rename to web/app/[locale]/assets/landing-image.png diff --git a/web/app/[locale]/blog/cmd-shift-u/page.tsx b/web/app/[locale]/blog/cmd-shift-u/page.tsx new file mode 100644 index 00000000..815d4790 --- /dev/null +++ b/web/app/[locale]/blog/cmd-shift-u/page.tsx @@ -0,0 +1,74 @@ +import { useTranslations } from "next-intl"; +import { getTranslations } from "next-intl/server"; +import { Link } from "../../../../i18n/navigation"; + +export async function generateMetadata({ params }: { params: Promise<{ locale: string }> }) { + const { locale } = await params; + const t = await getTranslations({ locale, namespace: "blog.cmdShiftU" }); + const url = locale === "en" ? "/blog/cmd-shift-u" : `/${locale}/blog/cmd-shift-u`; + return { + title: t("metaTitle"), + description: t("metaDescription"), + keywords: [ + "cmux", "terminal", "macOS", "notifications", "AI coding agents", + "keyboard shortcuts", "developer tools", "workflow", + ], + openGraph: { + title: t("metaTitle"), + description: t("metaDescription"), + type: "article", + publishedTime: "2026-03-04T00:00:00Z", + url, + }, + twitter: { + card: "summary", + title: t("metaTitle"), + description: t("metaDescription"), + }, + alternates: { canonical: url }, + }; +} + +export default function CmdShiftUPage() { + const t = useTranslations("blog.posts.cmdShiftU"); + const tc = useTranslations("common"); + + return ( + <> +{t("p1")}
+ + + ++ {t.rich("p2", { + link: (chunks) => ( + {chunks} + ), + })} +
+ > + ); +} diff --git a/web/app/[locale]/blog/introducing-cmux/page.tsx b/web/app/[locale]/blog/introducing-cmux/page.tsx new file mode 100644 index 00000000..ef5a7ddc --- /dev/null +++ b/web/app/[locale]/blog/introducing-cmux/page.tsx @@ -0,0 +1,72 @@ +import { useTranslations } from "next-intl"; +import { getTranslations } from "next-intl/server"; +import { Link } from "../../../../i18n/navigation"; + +export async function generateMetadata({ params }: { params: Promise<{ locale: string }> }) { + const { locale } = await params; + const t = await getTranslations({ locale, namespace: "blog.introducingCmux" }); + const url = locale === "en" ? "/blog/introducing-cmux" : `/${locale}/blog/introducing-cmux`; + return { + title: t("metaTitle"), + description: t("metaDescription"), + keywords: [ + "cmux", "terminal", "macOS", "Ghostty", "libghostty", + "AI coding agents", "Claude Code", "vertical tabs", "split panes", "socket API", + ], + openGraph: { + title: t("metaTitle"), + description: t("metaDescription"), + type: "article", + publishedTime: "2026-02-12T00:00:00Z", + url, + }, + twitter: { + card: "summary", + title: t("metaTitle"), + description: t("metaDescription"), + }, + alternates: { canonical: url }, + }; +} + +export default function IntroducingCmuxPage() { + const t = useTranslations("blog.posts.introducingCmux"); + const tc = useTranslations("common"); + + return ( + <> +{t("p1")}
+ +{t("whyP")}
+ ++ {t.rich("getStartedP", { + link: (chunks) => {chunks}, + })} +
+ > + ); +} diff --git a/web/app/blog/layout.tsx b/web/app/[locale]/blog/layout.tsx similarity index 51% rename from web/app/blog/layout.tsx rename to web/app/[locale]/blog/layout.tsx index ce32f825..30caa6cc 100644 --- a/web/app/blog/layout.tsx +++ b/web/app/[locale]/blog/layout.tsx @@ -1,21 +1,29 @@ -import type { Metadata } from "next"; +import { getTranslations } from "next-intl/server"; import { SiteHeader } from "../components/site-header"; import { BlogPager } from "../components/blog-pager"; import { BlogCTA } from "../components/blog-cta"; -export const metadata: Metadata = { - title: { - template: "%s — cmux blog", - default: "cmux blog", - }, - openGraph: { - siteName: "cmux", - type: "article", - }, - alternates: { - canonical: "./", - }, -}; +export async function generateMetadata({ + params, +}: { + params: Promise<{ locale: string }>; +}) { + const { locale } = await params; + const t = await getTranslations({ locale, namespace: "blog" }); + return { + title: { + template: `%s — ${t("layoutTitle")}`, + default: t("layoutTitle"), + }, + openGraph: { + siteName: "cmux", + type: "article" as const, + }, + alternates: { + canonical: "./", + }, + }; +} export default function BlogLayout({ children, diff --git a/web/app/[locale]/blog/page.tsx b/web/app/[locale]/blog/page.tsx new file mode 100644 index 00000000..1fb32d4f --- /dev/null +++ b/web/app/[locale]/blog/page.tsx @@ -0,0 +1,60 @@ +import { useTranslations } from "next-intl"; +import { getTranslations } from "next-intl/server"; +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" }); + return { + title: t("metaTitle"), + description: t("metaDescription"), + }; +} + +const blogSlugs = [ + "cmdShiftU", + "zenOfCmux", + "showHnLaunch", + "introducingCmux", +] as const; + +const slugToPath: Record+ {t(`posts.${slug}.summary`)} +
+ ++ {t.rich("intro", { + link: (chunks) => ( + {chunks} + ), + })} +
+ +++ +{t("blockquote1")}
+{t("blockquote2")}
+{t("blockquote3")}
+{t("blockquote4")}
+{t("blockquote5")}
+
{t("hitNumber2")}
+ ++ {t.rich("favoriteComment", { + link: (chunks) => ( + {chunks} + ), + })} +
+ + {/* Keep the HN comment blockquote in English as it's a direct quote */} +++ ++ Hey, this looks seriously awesome. Love the ideas here, specifically: + the programmability (I haven't tried it yet, but had been + considering learning tmux partly for this), layered UI, browser w/ + api. Looking forward to giving this a spin. Also want to add that I + really appreciate Mitchell Hashimoto creating libghostty; it feels + like an exciting time to be a terminal user. +
+Some feedback (since you were asking for it elsewhere in the thread!):
++
+- + It's not obvious/easy to open browser dev tools (cmd-alt-i + didn't work), and when I did find it (right click page → + inspect element) none of the controls were visible but I could see + stuff happening when I moved my mouse over the panel +
+- + Would be cool to borrow more of ghostty's behavior: +
++
+- hotkey overrides
+- command palette (cmd-shift-p)
+- cmd-z to "zoom in" to a pane
++ —{" "} + + johnthedebs + +
+
{t("viralJapan")}
+ +{t("translation")}
+ +{t("viralChina")}
+ +{t("extensions")}
+ +{t("scriptable")}
+ ++ {t.rich("cta", { + link: (chunks) => ( + {chunks} + ), + })} +
+ +{t("p1")}
+{t("p2")}
+{t("p3")}
+{t("p4")}
+ > + ); +} diff --git a/web/app/community/page.tsx b/web/app/[locale]/community/page.tsx similarity index 83% rename from web/app/community/page.tsx rename to web/app/[locale]/community/page.tsx index b344ace8..cce06e02 100644 --- a/web/app/community/page.tsx +++ b/web/app/[locale]/community/page.tsx @@ -1,10 +1,15 @@ -import type { Metadata } from "next"; +import { useTranslations } from "next-intl"; +import { getTranslations } from "next-intl/server"; import { SiteHeader } from "../components/site-header"; -export const metadata: Metadata = { - title: "Community — cmux", - description: "Join the cmux community on Discord, Twitter, GitHub, and more", -}; +export async function generateMetadata({ params }: { params: Promise<{ locale: string }> }) { + const { locale } = await params; + const t = await getTranslations({ locale, namespace: "community" }); + return { + title: t("metaTitle"), + description: t("metaDescription"), + }; +} function CommunityLink({ href, @@ -41,23 +46,25 @@ function CommunityLink({ } export default function CommunityPage() { + const t = useTranslations("community"); + return (- Connect with other cmux users and the team behind it. + {t("description")}
- © {year} Manaflow -
++ {t("copyright", { year })} +
+