Files
claudemesh/apps/web/src/lib/metadata.ts
Alejandro Gutiérrez 533dcc11f6
Some checks failed
CI / Tests / 🧪 Test (push) Has been cancelled
fix(web): remove turbostarter CTA popup + ship claudemesh OG image
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>
2026-04-04 23:11:34 +01:00

117 lines
2.7 KiB
TypeScript

import { isKey } from "@turbostarter/i18n";
import { getTranslation } from "@turbostarter/i18n/server";
import { appConfig } from "~/config/app";
import type { TranslationKey } from "@turbostarter/i18n";
import type { Metadata, Viewport } from "next";
type OpenGraphType =
| "article"
| "book"
| "music.song"
| "music.album"
| "music.playlist"
| "music.radio_station"
| "profile"
| "website"
| "video.tv_show"
| "video.other"
| "video.movie"
| "video.episode";
interface SeoProps {
readonly title?: TranslationKey | (string & Record<never, never>);
readonly description?: TranslationKey | (string & Record<never, never>);
readonly image?: string;
readonly url?: string;
readonly canonical?: string;
readonly type?: OpenGraphType;
readonly images?: {
url: string;
width: number;
height: number;
alt?: string;
}[];
}
const SITE_NAME_SEPARATOR = " | ";
export const SITE_NAME_TEMPLATE = `%s${SITE_NAME_SEPARATOR}${appConfig.name}`;
const DEFAULT_IMAGE = {
url: `${appConfig.url}/opengraph-image.png`,
width: 1200,
height: 630,
alt: appConfig.name,
};
export const getMetadata =
(
{
title,
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],
type = "website",
} = {} as SeoProps,
) =>
async ({
params,
}: {
params?: Promise<{ locale: string }>;
}): Promise<Metadata> => {
const { t, i18n } = await getTranslation({
locale: (await params)?.locale,
});
const common = {
...(title && {
title: isKey(title, i18n) ? (t(title) as string) : title,
}),
description: isKey(description, i18n)
? (t(description) as string)
: description,
};
return {
...common,
openGraph: {
...common,
url: url ?? canonical ?? appConfig.url,
siteName: appConfig.name,
type,
images,
},
...{
...(canonical && {
alternates: {
canonical,
},
}),
},
twitter: {
card: "summary_large_image" as const,
images,
},
};
};
export const DEFAULT_METADATA: Metadata = {
...(await getMetadata()({
params: Promise.resolve({ locale: appConfig.locale }),
})),
title: {
template: SITE_NAME_TEMPLATE,
default: appConfig.name,
},
metadataBase: appConfig.url ? new URL(appConfig.url) : null,
};
export const DEFAULT_VIEWPORT: Viewport = {
themeColor: [
{ media: "(prefers-color-scheme: light)", color: "#ffffff" },
{ media: "(prefers-color-scheme: dark)", color: "#030712" },
],
};