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>
117 lines
2.7 KiB
TypeScript
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" },
|
|
],
|
|
};
|