feat(web): marketing landing page with Anthropic design system
Landing page at / matching claude.com/product/claude-code structure: hero, surfaces, pricing, laptop-to-laptop, features, meets-you, faq, cta, + floating "Latest news" toaster. Motion-based scroll reveals. Design system extracted from claude.com via playwriter reverse-engineering: - Self-hosted Anthropic Sans/Serif/Mono fonts (6 woff2 files) - --cm-* tokens in globals.css (clay #d97757, gray-050..900, fluid clamps) - Serif display, Sans UI, Mono terminals & section markers - Italic clay phrases for emphasis Header rewritten for design consistency: claudemesh wordmark (mesh glyph + serif), dark bg, nav (Docs · Pricing · Changelog · GitHub), "Start free" CTA. Free-first messaging: hero subhead "Free and open-source. Forever.", primary CTA "Start free", pricing defaults to Solo=Free. Fixes: - packages/api: comment out aiRouter (module removed in1f094c4) - packages/db/schema/mesh.ts: rename memberRelations → meshMemberRelations (missed inbeeaa3brename pass, caught via web build — ack'd by BotMou) - credits/{api,server,index}: stub out @turbostarter/ai/credits/utils - remove (marketing)/legal/[slug] route and common/mdx.tsx (cms-backed) - sitemap: drop blog/legal enumeration (cms removed) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,76 +0,0 @@
|
||||
import { notFound } from "next/navigation";
|
||||
|
||||
import {
|
||||
CollectionType,
|
||||
getContentItemBySlug,
|
||||
getContentItems,
|
||||
} from "@turbostarter/cms";
|
||||
import { getTranslation } from "@turbostarter/i18n/server";
|
||||
|
||||
import { getMetadata } from "~/lib/metadata";
|
||||
import { Mdx } from "~/modules/common/mdx";
|
||||
import {
|
||||
Section,
|
||||
SectionBadge,
|
||||
SectionDescription,
|
||||
SectionHeader,
|
||||
SectionTitle,
|
||||
} from "~/modules/marketing/layout/section";
|
||||
|
||||
interface PageParams {
|
||||
params: Promise<{
|
||||
slug: string;
|
||||
locale: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
export default async function Page({ params }: PageParams) {
|
||||
const item = getContentItemBySlug({
|
||||
collection: CollectionType.LEGAL,
|
||||
slug: (await params).slug,
|
||||
locale: (await params).locale,
|
||||
});
|
||||
|
||||
if (!item) {
|
||||
return notFound();
|
||||
}
|
||||
|
||||
const { t } = await getTranslation({ ns: "common" });
|
||||
|
||||
return (
|
||||
<Section>
|
||||
<SectionHeader>
|
||||
<SectionBadge>{t("legal.label")}</SectionBadge>
|
||||
<SectionTitle as="h1">{item.title}</SectionTitle>
|
||||
<SectionDescription>{item.description}</SectionDescription>
|
||||
</SectionHeader>
|
||||
<Mdx mdx={item.mdx} />
|
||||
</Section>
|
||||
);
|
||||
}
|
||||
|
||||
export function generateStaticParams() {
|
||||
return getContentItems({ collection: CollectionType.LEGAL }).items.map(
|
||||
({ slug, locale }) => ({
|
||||
slug,
|
||||
locale,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
export async function generateMetadata({ params }: PageParams) {
|
||||
const item = getContentItemBySlug({
|
||||
collection: CollectionType.LEGAL,
|
||||
slug: (await params).slug,
|
||||
locale: (await params).locale,
|
||||
});
|
||||
|
||||
if (!item) {
|
||||
return notFound();
|
||||
}
|
||||
|
||||
return getMetadata({
|
||||
title: item.title,
|
||||
description: item.description,
|
||||
})({ params });
|
||||
}
|
||||
@@ -1,42 +1,29 @@
|
||||
"use client";
|
||||
|
||||
import { useTranslation } from "@turbostarter/i18n";
|
||||
import { buttonVariants } from "@turbostarter/ui-web/button";
|
||||
import { Icons } from "@turbostarter/ui-web/icons";
|
||||
|
||||
import { pathsConfig } from "~/config/paths";
|
||||
import { TurboLink } from "~/modules/common/turbo-link";
|
||||
import { Hero } from "~/modules/marketing/home/hero";
|
||||
import { Surfaces } from "~/modules/marketing/home/surfaces";
|
||||
import { Pricing } from "~/modules/marketing/home/pricing";
|
||||
import { LaptopToLaptop } from "~/modules/marketing/home/laptop-to-laptop";
|
||||
import { Features } from "~/modules/marketing/home/features";
|
||||
import { MeetsYou } from "~/modules/marketing/home/meets-you";
|
||||
import { FAQ } from "~/modules/marketing/home/faq";
|
||||
import { CallToAction } from "~/modules/marketing/home/cta";
|
||||
import { LatestNewsToaster } from "~/modules/marketing/home/toaster";
|
||||
|
||||
const HomePage = () => {
|
||||
const { t } = useTranslation("common");
|
||||
|
||||
return (
|
||||
<main className="flex min-h-[calc(100vh-4rem)] flex-col items-center justify-center px-4">
|
||||
<div className="mx-auto max-w-3xl text-center">
|
||||
<h1 className="text-4xl font-bold tracking-tight sm:text-6xl">
|
||||
{t("home.title", { defaultValue: "Welcome to TurboStarter" })}
|
||||
</h1>
|
||||
<p className="mt-6 text-lg leading-8 text-muted-foreground">
|
||||
{t("home.description", { defaultValue: "The fastest way to build your next SaaS. Authentication, billing, database, and UI components — all pre-configured and ready to go." })}
|
||||
</p>
|
||||
<div className="mt-10 flex items-center justify-center gap-x-6">
|
||||
<TurboLink
|
||||
href={pathsConfig.auth.login}
|
||||
className={buttonVariants({ size: "lg" })}
|
||||
>
|
||||
{t("home.getStarted", { defaultValue: "Get Started" })}
|
||||
<Icons.ArrowRight className="ml-2 size-4" />
|
||||
</TurboLink>
|
||||
<TurboLink
|
||||
href="https://turbostarter.dev/docs"
|
||||
className={buttonVariants({ variant: "outline", size: "lg" })}
|
||||
target="_blank"
|
||||
>
|
||||
{t("home.documentation", { defaultValue: "Documentation" })}
|
||||
</TurboLink>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
<div
|
||||
className="bg-[var(--cm-bg)] text-[var(--cm-fg)] antialiased"
|
||||
style={{ fontFamily: "var(--cm-font-sans)" }}
|
||||
>
|
||||
<Hero />
|
||||
<Surfaces />
|
||||
<Pricing />
|
||||
<LaptopToLaptop />
|
||||
<Features />
|
||||
<MeetsYou />
|
||||
<FAQ />
|
||||
<CallToAction />
|
||||
<LatestNewsToaster />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { CollectionType, getContentItems } from "@turbostarter/cms";
|
||||
import { getPathname, config } from "@turbostarter/i18n";
|
||||
|
||||
import { appConfig } from "~/config/app";
|
||||
@@ -52,29 +51,5 @@ export default function sitemap(): MetadataRoute.Sitemap {
|
||||
changeFrequency: "monthly",
|
||||
priority: 0.8,
|
||||
},
|
||||
{
|
||||
...getEntry(pathsConfig.marketing.blog.index),
|
||||
lastModified: new Date(),
|
||||
changeFrequency: "monthly",
|
||||
priority: 0.8,
|
||||
},
|
||||
...getContentItems({
|
||||
collection: CollectionType.BLOG,
|
||||
locale: appConfig.locale,
|
||||
}).items.map<MetadataRoute.Sitemap[number]>((post) => ({
|
||||
...getEntry(pathsConfig.marketing.blog.post(post.slug)),
|
||||
lastModified: new Date(post.lastModifiedAt),
|
||||
changeFrequency: "monthly",
|
||||
priority: 0.7,
|
||||
})),
|
||||
...getContentItems({
|
||||
collection: CollectionType.LEGAL,
|
||||
locale: appConfig.locale,
|
||||
}).items.map<MetadataRoute.Sitemap[number]>((post) => ({
|
||||
...getEntry(pathsConfig.marketing.legal(post.slug)),
|
||||
lastModified: new Date(post.lastModifiedAt),
|
||||
changeFrequency: "yearly",
|
||||
priority: 0.5,
|
||||
})),
|
||||
];
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user