Skip to content

Commit c5bd27d

Browse files
Merge pull request #396 from basementstudio/canary
Website polish & content
2 parents 3a42482 + 2c4dbc5 commit c5bd27d

File tree

76 files changed

+1092
-301
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

76 files changed

+1092
-301
lines changed

apps/website/.gitignore

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,4 +41,7 @@ yarn-error.log*
4141
next-env.d.ts
4242
.env*.local
4343
xmcp-adapter
44-
/.source/
44+
/.source/
45+
# xmcp
46+
.xmcp
47+
xmcp-env.d.ts

apps/website/app/api/og/[...path]/route.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -269,7 +269,7 @@ export async function GET(
269269
<div
270270
style={{
271271
position: "absolute",
272-
bottom: "64px",
272+
bottom: "108px",
273273
left: "64px",
274274
right: "64px",
275275
display: "flex",

apps/website/app/blog/[...slug]/page.tsx

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,48 @@
1-
import { blogSource } from "../../../lib/source";
1+
import { blogSource } from "@/lib/source";
22
import { DocsBody, DocsTitle } from "@/components/layout/page";
33
import { notFound } from "next/navigation";
44
import { getMDXComponents } from "@/components/mdx-components";
55
import type { Metadata } from "next";
66
import { createRelativeLink } from "fumadocs-ui/mdx";
77
import { CodeBlock } from "@/components/codeblock";
88
import { BlogPage } from "@/components/layout/blog";
9-
import { getBlogMetadata } from "@/utils/blog";
9+
import { getBlogMetadata, resolveAuthors } from "@/utils/blog";
10+
import { PostAuthors } from "@/components/blog/post-authors";
1011
import { getBaseUrl } from "@/lib/base-url";
1112

1213
export default async function Page(props: PageProps<"/blog/[...slug]">) {
1314
const params = await props.params;
1415
const page = blogSource.getPage(params.slug);
1516
if (!page) notFound();
1617

18+
const slug = Array.isArray(params.slug) ? params.slug.join("/") : params.slug;
1719
const MDX = page.data.body;
20+
const authors = resolveAuthors(page.data.authors);
1821

1922
return (
20-
<BlogPage toc={page.data.toc}>
21-
<DocsTitle>{page.data.title}</DocsTitle>
22-
<DocsBody className="w-full">
23+
<BlogPage toc={page.data.toc} slug={slug}>
24+
<div className="flex flex-col gap-4">
25+
{page.data.date && (
26+
<time
27+
dateTime={page.data.date}
28+
className="text-xs uppercase tracking-wide text-brand-neutral-50"
29+
>
30+
{new Date(page.data.date).toLocaleDateString("en-US", {
31+
month: "short",
32+
day: "numeric",
33+
year: "numeric",
34+
})}
35+
</time>
36+
)}
37+
<DocsTitle>{page.data.title}</DocsTitle>
38+
{page.data.description && (
39+
<p className="text-base text-brand-neutral-50 max-w-3xl pb-1">
40+
{page.data.description}
41+
</p>
42+
)}
43+
<PostAuthors authors={authors} />
44+
</div>
45+
<DocsBody className="w-full border-t border-white/20 pt-4">
2346
<MDX
2447
components={getMDXComponents({
2548
// this allows you to link to other pages with relative file paths

apps/website/app/docs/[[...slug]]/page.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,15 @@ export default async function Page(props: PageProps<"/docs/[[...slug]]">) {
2020
if (!page) notFound();
2121

2222
const MDX = page.data.body;
23+
const displayTitle =
24+
(page.data as { displayTitle?: string })?.displayTitle || page.data.title;
2325

2426
return (
2527
<DocsPage
2628
toc={page.data.toc}
2729
pageActions={<PageActions markdownUrl={`${page.url}.mdx`} />}
2830
>
29-
<DocsTitle>{page.data.title}</DocsTitle>
31+
<DocsTitle>{displayTitle}</DocsTitle>
3032
<DocsDescription>{page.data.description}</DocsDescription>
3133
<DocsBody className="border-t border-white/20 pt-4">
3234
<MDX

apps/website/app/mcp/route.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import { xmcpHandler } from '@xmcp/adapter';
2+
3+
export { xmcpHandler as GET, xmcpHandler as POST };
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
"use client";
2+
3+
import { useCopyButton } from "fumadocs-ui/utils/use-copy-button";
4+
import { useState } from "react";
5+
import { cn } from "@/lib/utils";
6+
7+
interface CopyUrlButtonProps {
8+
url: string;
9+
className?: string;
10+
}
11+
12+
export function CopyUrlButton({ url, className }: CopyUrlButtonProps) {
13+
const [isCopying, setIsCopying] = useState(false);
14+
const [copied, triggerCopy] = useCopyButton(async () => {
15+
setIsCopying(true);
16+
try {
17+
await navigator.clipboard.writeText(url);
18+
} finally {
19+
setIsCopying(false);
20+
}
21+
});
22+
23+
return (
24+
<button
25+
type="button"
26+
onClick={triggerCopy}
27+
disabled={isCopying}
28+
className={cn(
29+
"relative inline-flex items-center text-sm font-medium text-brand-neutral-100 disabled:opacity-60 cursor-pointer",
30+
"min-w-[76px] justify-start",
31+
className
32+
)}
33+
aria-live="polite"
34+
>
35+
<span
36+
className={cn(
37+
"block transition-all duration-300 ease-out",
38+
copied ? "opacity-0 -translate-y-1" : "opacity-100 translate-y-0"
39+
)}
40+
aria-hidden={copied}
41+
>
42+
Copy URL
43+
</span>
44+
<span
45+
className={cn(
46+
"absolute left-0 block transition-all duration-300 ease-out pointer-events-none",
47+
copied ? "opacity-100 translate-y-0" : "opacity-0 translate-y-1"
48+
)}
49+
aria-hidden={!copied}
50+
>
51+
Copied
52+
</span>
53+
</button>
54+
);
55+
}

apps/website/components/blog/hero.tsx

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,19 +13,18 @@ export function BlogHero({ featuredPost }: BlogHeroProps) {
1313
return (
1414
<Link href={`/blog/${featuredPost.slug}`} className="block col-span-12">
1515
<section className="relative group overflow-visible mb-2">
16-
<div className="relative border border-brand-neutral-500 group-hover:border-brand-neutral-300 w-full flex flex-col md:flex-row">
17-
<div className="w-full md:w-1/2 md:border-r border-brand-neutral-500 overflow-hidden mb-4 md:mb-0 group-hover:border-brand-neutral-300">
16+
<div className="relative border border-brand-neutral-500 group-hover:border-brand-neutral-300 transition-colors duration-200 w-full flex flex-col md:flex-row">
17+
<div className="w-full md:w-1/2 md:border-r border-brand-neutral-500 overflow-hidden mb-4 md:mb-0 group-hover:border-brand-neutral-300 transition-colors duration-200 relative aspect-video">
1818
{image ? (
1919
<Image
2020
src={image}
2121
alt={featuredPost.title}
22-
width={0}
23-
height={0}
22+
fill
2423
sizes="(max-width: 768px) 100vw, 50vw"
25-
className="w-full h-auto"
24+
className="object-cover"
2625
/>
2726
) : (
28-
<div className="w-full aspect-video flex items-center justify-center text-brand-neutral-200 text-sm">
27+
<div className="w-full h-full flex items-center justify-center text-brand-neutral-200 text-sm">
2928
No preview image
3029
</div>
3130
)}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import Image from "next/image";
2+
import { type BlogAuthor } from "@/utils/blog";
3+
import Link from "next/link";
4+
import { Icons } from "../icons";
5+
6+
interface PostAuthorsProps {
7+
authors: BlogAuthor[];
8+
}
9+
10+
export function PostAuthors({ authors }: PostAuthorsProps) {
11+
if (authors.length === 0) return null;
12+
13+
return (
14+
<div className="flex flex-wrap items-center gap-4 text-sm text-brand-neutral-100">
15+
{authors.map((author) => (
16+
<Link
17+
key={author.id}
18+
href={author.xUrl}
19+
target="_blank"
20+
rel="noreferrer"
21+
className="group flex items-center gap-3 hover:text-brand-white transition-colors"
22+
>
23+
<Image
24+
src={author.profilePicture}
25+
alt={`${author.name} avatar`}
26+
width={40}
27+
height={40}
28+
className="h-10 w-10 rounded-full border border-brand-neutral-500 bg-brand-neutral-800 object-cover"
29+
loading="lazy"
30+
/>
31+
<div className="flex flex-col leading-5">
32+
<span className="text-brand-white font-medium">{author.name}</span>
33+
<span className="relative inline-flex h-5 items-center text-brand-neutral-200">
34+
<span className="transition-all duration-300 ease-out opacity-100 translate-y-0 group-hover:opacity-0 group-hover:-translate-y-1">
35+
{author.role ?? "Author"}
36+
</span>
37+
<span className="absolute left-0 transition-all duration-300 ease-out opacity-0 translate-y-1 group-hover:opacity-100 group-hover:translate-y-0 inline-flex items-center gap-1">
38+
<Icons.x className="size-2.5 text-brand-neutral-200" />
39+
<span className="text-brand-neutral-200">{author.xHandle}</span>
40+
</span>
41+
</span>
42+
</div>
43+
</Link>
44+
))}
45+
</div>
46+
);
47+
}

apps/website/components/home/blog/index.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,10 @@ export const BlogCard = ({ post }: { post: BlogPost }) => {
4444
post.textureImage ||
4545
TEXTURE_IMAGES[
4646
Math.abs(
47-
Array.from(post.slug).reduce((hash, char) => hash + char.charCodeAt(0), 0)
47+
Array.from(post.slug).reduce(
48+
(hash, char) => hash + char.charCodeAt(0),
49+
0
50+
)
4851
) % TEXTURE_IMAGES.length
4952
];
5053

0 commit comments

Comments
 (0)