Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 3 additions & 6 deletions apps/website/app/blog/page.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { DEFAULT_OG_IMAGE, DEFAULT_TWITTER_IMAGE } from "@/lib/social-image";
import {
getListedBlogPosts,
getFeaturedBlogPost,
Expand Down Expand Up @@ -34,18 +35,14 @@ export const metadata = {
siteName: "xmcp",
type: "website",
locale: "en_US",
images: {
url: "/xmcp-og.png",
width: 1200,
height: 630,
},
images: DEFAULT_OG_IMAGE,
},
twitter: {
card: "summary_large_image",
title: "xmcp Blog - MCP Guides, Releases, and Engineering Notes",
description:
"Read xmcp guides, release notes, and engineering articles about building TypeScript MCP servers and production-ready agent tools.",
images: "/xmcp-og.png",
images: DEFAULT_TWITTER_IMAGE,
},
};

Expand Down
5 changes: 3 additions & 2 deletions apps/website/app/changelog/page.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { DEFAULT_OG_IMAGE, DEFAULT_TWITTER_IMAGE } from "@/lib/social-image";
import type { Metadata } from "next";
import { ReleaseNotes } from "@/components/changelog/release-notes";
import { SITE_URL } from "@/lib/base-url";
Expand All @@ -22,13 +23,13 @@ export const metadata: Metadata = {
description,
url: `${SITE_URL}/changelog`,
type: "website",
images: "/xmcp-og.png",
images: DEFAULT_OG_IMAGE,
},
twitter: {
card: "summary_large_image",
title,
description,
images: "/xmcp-og.png",
images: DEFAULT_TWITTER_IMAGE,
},
};

Expand Down
34 changes: 28 additions & 6 deletions apps/website/app/design-system.css
Original file line number Diff line number Diff line change
Expand Up @@ -639,10 +639,14 @@ body .hero-specimen + figure.shiki {
margin-top: 0;
}
/* Product mock over the dust backdrop, cut off by the frame's bottom edge. */
.hero-mock {
.hero-dashboard-player {
position: absolute;
z-index: 1;
inset: 9% 6% -12px;
}
.hero-mock {
position: absolute;
inset: 0;
display: flex;
flex-direction: column;
overflow: hidden;
Expand Down Expand Up @@ -729,7 +733,7 @@ body .hero-specimen + figure.shiki {
.hero-mock-stats {
display: grid;
/* Keep metric positions independent of the selected tool and its figures. */
grid-template-columns: minmax(0, 1fr) 4ch minmax(0, 1fr);
grid-template-columns: minmax(0, 1fr) 7ch minmax(0, 1fr);
gap: 12px;
padding-top: 12px;
border-top: 1px solid var(--site-rule);
Expand All @@ -738,9 +742,12 @@ body .hero-specimen + figure.shiki {
min-width: 0;
}
.hero-mock-stats .hero-mock-muted {
white-space: nowrap;
}
/* Only variable tool names can truncate; the metric labels stay readable. */
.hero-mock-stats > span:first-child .hero-mock-muted {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.hero-mock-figure {
font-size: 19px;
Expand Down Expand Up @@ -815,9 +822,7 @@ body .hero-specimen + figure.shiki {
overflow: hidden;
border-radius: 1.5px 1.5px 0 0;
opacity: 0.85;
transition:
height 500ms var(--site-ease),
opacity 200ms var(--site-ease);
transition: opacity 200ms var(--site-ease);
}
.hero-mock-plot[data-hovering] .hero-mock-fill {
opacity: 0.25;
Expand Down Expand Up @@ -885,6 +890,8 @@ a.hero-mock-run:hover {
}
.hero-scene-size {
width: 100%;
/* Keep the tool and middleware rows visible when the frame gets narrow. */
min-height: 460px;
aspect-ratio: 350 / 243;
}
/* Give the inverted scene an explicit backdrop inside its fixed layer. */
Expand Down Expand Up @@ -914,6 +921,21 @@ a.hero-mock-run:hover {
padding-block: var(--site-section-gap);
text-align: center;
}
/* Keep error recovery in one viewport, without the marketing footer. */
body:has(.site-not-found) :is(.site-prefooter, .site-footer) {
display: none;
}
body .site-not-found {
/* Fill the body's remaining height, including the mobile header's border. */
flex: 1;
padding-block: 24px;
align-content: center;
}
.site-not-found .prefooter-particles > div {
width: clamp(48px, 16dvh, 127px);
height: auto;
aspect-ratio: 127 / 154;
}
body .site-prefooter .site-page-title {
margin: 0;
font-size: 24px;
Expand Down
16 changes: 4 additions & 12 deletions apps/website/app/faq/page.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { DEFAULT_OG_IMAGE, DEFAULT_TWITTER_IMAGE } from "@/lib/social-image";
import { Metadata } from "next";
import Link from "next/link";
import { FAQ_ITEMS } from "@/content/faq";
import { JsonLd } from "@/components/seo/json-ld";
import { AccordionItem } from "@/components/ui/accordion-item";
import { getBreadcrumbSchema, getFaqSchema } from "@/lib/structured-data";
import { getBaseUrl, SITE_URL } from "@/lib/base-url";
import { SITE_URL } from "@/lib/base-url";

export const dynamic = "force-static";

Expand All @@ -13,7 +14,6 @@ const DESCRIPTION =
"Answers to common questions about xmcp: what it is, how to create a project, tools, resources and prompts, transports, framework adapters, authentication, deployment, and monetization.";

const FAQ_URL = `${SITE_URL}/faq`;
const baseUrl = getBaseUrl();

export const metadata: Metadata = {
title: TITLE,
Expand All @@ -31,21 +31,13 @@ export const metadata: Metadata = {
siteName: "xmcp",
type: "website",
locale: "en_US",
images: {
url: `${baseUrl}/xmcp-og.png`,
width: 1200,
height: 630,
},
images: DEFAULT_OG_IMAGE,
},
twitter: {
card: "summary_large_image",
title: TITLE,
description: DESCRIPTION,
images: {
url: `${baseUrl}/xmcp-og.png`,
width: 1200,
height: 630,
},
images: DEFAULT_TWITTER_IMAGE,
},
};

Expand Down
15 changes: 3 additions & 12 deletions apps/website/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { DEFAULT_OG_IMAGE, DEFAULT_TWITTER_IMAGE } from "@/lib/social-image";
import "./globals.css";
import { PersistentHero } from "@/components/home/hero/persistent-hero";
import { Analytics } from "@vercel/analytics/next";
Expand Down Expand Up @@ -73,12 +74,7 @@ export const metadata: Metadata = {
},
openGraph: {
siteName: "xmcp",
images: {
url: "/xmcp-og.png",
width: 1200,
height: 630,
alt: "xmcp — The TypeScript MCP framework",
},
images: DEFAULT_OG_IMAGE,
url: "https://xmcp.dev",
type: "website",
locale: "en_US",
Expand All @@ -93,12 +89,7 @@ export const metadata: Metadata = {
title: "xmcp — The TypeScript MCP framework",
description:
"xmcp is the TypeScript framework for building, shipping, and scaling Model Context Protocol servers — tools, prompts, resources, auth, transports, and monetization out of the box.",
images: {
url: "/xmcp-og.png",
width: 1200,
height: 630,
alt: "xmcp — The TypeScript MCP framework",
},
images: DEFAULT_TWITTER_IMAGE,
},
manifest: "/site.webmanifest",
appleWebApp: {
Expand Down
53 changes: 34 additions & 19 deletions apps/website/app/not-found.tsx
Original file line number Diff line number Diff line change
@@ -1,28 +1,43 @@
import { Button } from "@/components/ui/button";
import { PrefooterArtwork } from "@/components/layout/prefooter";
import Link from "next/link";

export const dynamic = "force-static";

export default async function Page() {
export default function NotFound() {
return (
<main
id="main-content"
className="min-h-[calc(100vh-12rem)] flex flex-col items-center justify-center gap-4"
>
<h1 className="display text-center text-balance z-10 text-gradient">
404 - Page Not Found
</h1>
<p className="text-center text-balance z-10 text-gradient">
The page you are looking for does not exist.
</p>
<div className="flex items-center justify-center gap-2 mt-2">
<Button variant="primary" asChild>
<Link href="/">Go back home</Link>
</Button>
<Button variant="secondary" asChild>
<Link href="/docs">Read the docs</Link>
</Button>
</div>
<main id="main-content" className="site-page-grid site-not-found">
<section
className="flex flex-col items-center justify-center gap-[clamp(12px,3dvh,32px)] text-center"
aria-labelledby="not-found-title"
>
<PrefooterArtwork />
<div className="flex flex-col items-center gap-4">
<h1 id="not-found-title" className="display text-balance">
This page isn’t here.
</h1>
<p className="listing-description max-w-md text-balance">
It may have moved, or the link might be incorrect. Head back home or
find what you need in the documentation.
</p>
</div>
<div className="flex flex-wrap justify-center gap-3">
<Button
variant="primary"
className="min-h-11 focus-visible:ring-2 focus-visible:ring-brand-white focus-visible:ring-offset-4 focus-visible:ring-offset-brand-black"
asChild
>
<Link href="/">Go back home</Link>
</Button>
<Button
variant="secondary"
className="min-h-11 focus-visible:ring-2 focus-visible:ring-brand-white focus-visible:ring-offset-4 focus-visible:ring-offset-brand-black"
asChild
>
<Link href="/docs">Read the docs</Link>
</Button>
</div>
</section>
</main>
);
}
9 changes: 3 additions & 6 deletions apps/website/app/showcase/page.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { DEFAULT_OG_IMAGE, DEFAULT_TWITTER_IMAGE } from "@/lib/social-image";
import { Metadata } from "next";
import { ShowcaseCards } from "@/components/showcase/cards";
import { ShowcaseSubmissionCard } from "@/components/showcase/submission-card";
Expand All @@ -24,18 +25,14 @@ export const metadata: Metadata = {
siteName: "xmcp",
type: "website",
locale: "en_US",
images: {
url: "/xmcp-og.png",
width: 1200,
height: 630,
},
images: DEFAULT_OG_IMAGE,
},
twitter: {
card: "summary_large_image",
title: "Showcase - xmcp",
description:
"Browse production-ready MCP servers built by the xmcp community and submit your own server to be featured in the showcase alongside other developers building AI tooling.",
images: "/xmcp-og.png",
images: DEFAULT_TWITTER_IMAGE,
},
};

Expand Down
9 changes: 3 additions & 6 deletions apps/website/app/telemetry/page.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { DEFAULT_OG_IMAGE, DEFAULT_TWITTER_IMAGE } from "@/lib/social-image";
import type { Metadata } from "next";
import { DocsBody } from "@/components/layout/page";
import { JsonLd } from "@/components/seo/json-ld";
Expand All @@ -21,18 +22,14 @@ export const metadata: Metadata = {
siteName: "xmcp",
type: "website",
locale: "en_US",
images: {
url: "/xmcp-og.png",
width: 1200,
height: 630,
},
images: DEFAULT_OG_IMAGE,
},
twitter: {
card: "summary_large_image",
title: "Telemetry - xmcp",
description:
"xmcp collects anonymous build and run telemetry to prioritize transport, adapter, and compiler fixes. Learn exactly what we collect, why it matters, and how to opt out at any time.",
images: "/xmcp-og.png",
images: DEFAULT_TWITTER_IMAGE,
},
};

Expand Down
17 changes: 12 additions & 5 deletions apps/website/components/blog/artwork.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,28 @@
"use client";

import Image from "next/image";
import ParticleImage from "@/components/particles/image";
import { getBlogImageProps } from "@/lib/blog-images";
import type { getBlogImageProps } from "@/lib/blog-images";
import { useHeroArtworkReady } from "@/components/home/hero/persistent-hero";

/** Keep the original optimized cover available before the shader is ready. */
export function BlogArtwork({
src,
image,
alt,
sizes,
loading = "lazy",
preload = false,
prepareAfterHero = false,
}: {
src: string;
image: ReturnType<typeof getBlogImageProps>;
alt: string;
sizes: string;
loading?: "eager" | "lazy";
preload?: boolean;
prepareAfterHero?: boolean;
}) {
const image = getBlogImageProps(src);
const heroReady = useHeroArtworkReady();
const prepare = prepareAfterHero && heroReady;
const texture =
typeof image.src === "string"
? image.src
Expand All @@ -33,14 +39,15 @@ export function BlogArtwork({
className="h-full w-full"
preset="blog"
fadeIn
prepare={prepare}
fallback={
<Image
{...image}
alt=""
fill
sizes={sizes}
preload={preload}
loading={preload ? undefined : loading}
loading={preload ? undefined : prepare ? "eager" : loading}
className="object-cover"
/>
}
Expand Down
3 changes: 2 additions & 1 deletion apps/website/components/blog/hero.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { ContentCard } from "@/components/ui/content-card";
import { BlogArtwork } from "./artwork";
import { getBlogImageProps } from "@/lib/blog-images";
import { type BlogPost } from "../../utils/blog";
import { Tag } from "../ui/tag";

Expand All @@ -18,7 +19,7 @@ export function BlogHero({ featuredPost }: BlogHeroProps) {
media={
image ? (
<BlogArtwork
src={image}
image={getBlogImageProps(image)}
alt={featuredPost.title}
sizes="(min-width: 768px) 350px, (min-width: 740px) 700px, calc(100vw - 40px)"
preload
Expand Down
Loading
Loading