fix(web): remove turbostarter CTA popup + ship claudemesh OG image
Some checks failed
CI / Tests / 🧪 Test (push) Has been cancelled

Two visible launch-polish issues:

1. BuyCtaDialog popup was firing on an exponential backoff schedule
   (15s, 30s, 60s, …) pushing users toward turbostarter.dev/#pricing +
   Discord. Wrong product, wrong audience. Fully removed: mount point
   in [locale]/layout.tsx + the component file + localStorage keys will
   self-prune on next visit.

2. WhatsApp/Slack/Twitter link previews were pulling the TurboStarter
   boilerplate opengraph-image.png (from Jan 8). Replaced with a 1200×630
   claudemesh OG: "CLAUDEMESH" pixel wordmark left side, hero mesh
   composition (6 Claude Code terminals + pixel-crab hub + orange
   energy lattice + vaporwave grid floor) right side, "peer mesh for
   Claude Code sessions" tagline in mono beneath wordmark.

3. Default metadata description swapped from the dangling
   `common:product.description` i18n key (which rendered as the key
   itself because the key doesn't exist in our trimmed translations)
   to a hardcoded claudemesh description.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Alejandro Gutiérrez
2026-04-04 23:11:34 +01:00
parent fa23525c46
commit 533dcc11f6
4 changed files with 1 additions and 122 deletions

View File

@@ -7,7 +7,6 @@ import { Providers } from "~/lib/providers/providers";
import { ImpersonatingBanner } from "~/modules/admin/users/user/impersonating-banner";
import { BaseLayout } from "~/modules/common/layout/base";
import { Toaster } from "~/modules/common/toast";
import { BuyCtaDialog } from "~/modules/marketing/layout/buy-cta-dialog";
export function generateStaticParams() {
return config.locales.map((locale) => ({ locale }));
@@ -33,7 +32,6 @@ export default async function RootLayout({
<Providers locale={locale}>
<ImpersonatingBanner />
{children}
<BuyCtaDialog />
<Toaster />
</Providers>
</BaseLayout>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 947 KiB

View File

@@ -49,7 +49,7 @@ export const getMetadata =
(
{
title,
description = "common:product.description",
description = "Connect your Claude Code sessions to each other. Zero config. End-to-end encrypted. Peer mesh for Claude Code teams.",
url,
canonical,
images = [DEFAULT_IMAGE],

View File

@@ -1,119 +0,0 @@
"use client";
import { useEffect, useRef, useState } from "react";
import { useTranslation } from "@turbostarter/i18n";
import { cn } from "@turbostarter/ui";
import { buttonVariants } from "@turbostarter/ui-web/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from "@turbostarter/ui-web/dialog";
import { Icons } from "@turbostarter/ui-web/icons";
const MIN_DELAY_MS = 15_000;
const STORAGE_LAST_SHOWN_AT = "buyCtaDialog:lastShownAt";
const STORAGE_PREV_DELAY_MS = "buyCtaDialog:prevDelayMs";
export const BuyCtaDialog = () => {
const { t } = useTranslation(["common", "marketing"]);
const [open, setOpen] = useState(false);
const timeoutIdRef = useRef<number | null>(null);
useEffect(() => {
const scheduleNext = () => {
const now = Date.now();
const storedLastShown = Number(
window.localStorage.getItem(STORAGE_LAST_SHOWN_AT) ?? "0",
);
const prevDelayMs = Number(
window.localStorage.getItem(STORAGE_PREV_DELAY_MS) ?? "0",
);
const nextDelay = Math.max(
MIN_DELAY_MS,
prevDelayMs ? prevDelayMs * 2 : MIN_DELAY_MS,
);
const baseNextShow = storedLastShown
? storedLastShown + nextDelay
: now + nextDelay;
const delayFromNow = Math.max(MIN_DELAY_MS, baseNextShow - now);
if (timeoutIdRef.current) {
window.clearTimeout(timeoutIdRef.current);
}
timeoutIdRef.current = window.setTimeout(() => {
setOpen(true);
const shownAt = Date.now();
window.localStorage.setItem(STORAGE_LAST_SHOWN_AT, String(shownAt));
window.localStorage.setItem(STORAGE_PREV_DELAY_MS, String(nextDelay));
scheduleNext();
}, delayFromNow);
};
scheduleNext();
return () => {
if (timeoutIdRef.current) {
window.clearTimeout(timeoutIdRef.current);
}
};
}, []);
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogContent className="max-w-md">
<DialogHeader className="space-y-3">
<DialogTitle>{t("cta.buy.question")}</DialogTitle>
<DialogDescription className="text-foreground text-base">
{t("cta.buy.description")}
</DialogDescription>
</DialogHeader>
<a
href="https://turbostarter.dev/#pricing"
target="_blank"
rel="noopener noreferrer"
className={cn(buttonVariants(), "gap-2")}
>
<Icons.Code className="size-4" />
{t("cta.buy.button")}
</a>
<div className="bg-border relative -mx-6 my-3 h-px">
<span className="bg-background text-muted-foreground absolute left-1/2 -translate-x-1/2 -translate-y-1/2 px-3 text-sm">
{t("or")}
</span>
</div>
<div className="flex flex-col gap-4">
<p>{t("cta.buy.join.description")}</p>
<a
className={cn(
buttonVariants(),
"gap-2 bg-[#5865F2] px-7 no-underline hover:bg-[#5865F2]/95",
)}
href="https://discord.gg/KjpK2uk3JP"
rel="noopener noreferrer"
target="_blank"
>
<Icons.Discord className="size-[1.35rem] text-white" />
<span className="font-semibold text-white">
{t("cta.buy.join.button")}
</span>
</a>
</div>
</DialogContent>
</Dialog>
);
};