fix(web): remove turbostarter CTA popup + ship claudemesh OG image
Some checks failed
CI / Tests / 🧪 Test (push) Has been cancelled
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:
@@ -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 |
@@ -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],
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user