feat: redesign archgw -> plano + website in Next.js (#613)

* feat: redesign archgw -> plano + website

* feat(www): refactor landing page sections, add new diagrams and UI improvements

* feat(www): sections enhanced for clarify & diagrams added

* feat(www): improvements to mobile design, layout of diagrams

* feat(www): clean + typecheck

* feat(www): feedback loop changes

* feat(www): fix type error

* fix lib/utils error

* feat(www): ran biome formatting

* feat(www): graphic changes

* feat(www): web analytics

* fea(www): changes

* feat(www): introduce monorepo

This change brings Turborepo monorepo to independently handle the marketing website, the docs website and any other future use cases for mutli-platform support. They are using internal @katanemo package handlers for the design system and logic.

* fix(www): transpiler failure

* fix(www): tsconfig issue

* fix(www): next.config issue

* feat(docs): hold off on docs

* Delete next.config.ts

* feat(www): content fix

* feat(www): introduce blog

* feat(www): content changes

* Update package-lock.json

* feat: update text

* Update IntroSection.tsx

* feat: Turbopack issue

* fix

* Update IntroSection.tsx

* feat: updated Research page

* refactor(www): text clarity, padding adj.

* format(www)

* fix: add missing lib/ files to git - fixes Vercel GitHub deployment

- Updated .gitignore to properly exclude Python lib/ but include Next.js lib/ directories
- Added packages/ui/src/lib/utils.ts (cn utility function)
- Added apps/www/src/lib/sanity.ts (Sanity client configuration)
- Fixes module resolution errors in Vercel GitHub deployments (case-sensitive filesystem)

* Update .gitignore

* style(www): favicon + metadata

* fix(www): links

* fix(www): add analytics

* fix(www): add

* fix(www): fix links + image

* fix(www): fix links + image

* fix(www): fix links

* fix(www): remove from tools testing.md
This commit is contained in:
Musa 2025-12-18 15:55:15 -08:00 committed by GitHub
parent 48bbc7cce7
commit 0c3efdbef2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
119 changed files with 27142 additions and 266 deletions

View file

@ -0,0 +1,326 @@
import { ImageResponse } from "next/og";
import { NextRequest } from "next/server";
import { client, urlFor } from "@/lib/sanity";
export const runtime = "edge";
// Font loading function that uses the request origin
function loadFont(fileName: string, baseUrl: string) {
return fetch(new URL(`/fonts/${fileName}`, baseUrl)).then((res) => {
if (!res.ok) {
throw new Error(
`Failed to fetch font ${fileName}: ${res.status} ${res.statusText}`,
);
}
return res.arrayBuffer();
});
}
async function getBlogPost(slug: string) {
const query = `*[_type == "blog" && slug.current == $slug && published == true][0] {
_id,
title,
slug,
summary,
publishedAt,
mainImage,
author {
name,
title,
image
}
}`;
const post = await client.fetch(query, { slug });
return post;
}
function formatDate(dateString: string): string {
const date = new Date(dateString);
const day = date.getDate();
const month = date.toLocaleDateString("en-US", { month: "long" });
const year = date.getFullYear();
const getOrdinal = (n: number) => {
const s = ["th", "st", "nd", "rd"];
const v = n % 100;
return n + (s[(v - 20) % 10] || s[v] || s[0]);
};
return `${month} ${getOrdinal(day)}, ${year}`;
}
export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ slug: string }> },
) {
try {
// Get base URL for font loading - use request origin in production
const fontBaseUrl =
process.env.NEXT_PUBLIC_APP_URL ||
(process.env.VERCEL_URL
? `https://${process.env.VERCEL_URL}`
: request.nextUrl.origin);
// Load fonts with error handling
let fontData;
try {
const [
ibmPlexSans,
jetbrainsMonoRegular,
jetbrainsMonoMedium,
jetbrainsMonoBold,
] = await Promise.all([
loadFont("IBMPlexSans-VariableFont_wdth,wght.otf", fontBaseUrl),
loadFont("JetBrainsMono-Regular.otf", fontBaseUrl),
loadFont("JetBrainsMono-Medium.otf", fontBaseUrl),
loadFont("jetbrains-mono-bold.otf", fontBaseUrl),
]).catch((error: Error) => {
console.error("Error loading fonts:", error);
throw new Error(`Failed to load fonts: ${error.message}`);
});
fontData = {
ibmPlexSans,
jetbrainsMonoRegular,
jetbrainsMonoMedium,
jetbrainsMonoBold,
};
} catch (error: unknown) {
const errorMessage =
error instanceof Error ? error.message : "Unknown error";
console.error("Font loading error:", errorMessage);
return new Response(
JSON.stringify({
error: "Failed to load required fonts",
details: errorMessage,
baseUrl: fontBaseUrl,
}),
{ status: 500 },
);
}
const { slug } = await params;
const post = await getBlogPost(slug);
if (!post) {
return new Response(JSON.stringify({ error: "Post not found" }), {
status: 404,
});
}
// Get author image URL if available
let authorImageUrl: string | null = null;
if (post.author?.image) {
authorImageUrl = urlFor(post.author.image).width(120).url();
}
// Use logo PNG
const baseUrl =
process.env.NEXT_PUBLIC_APP_URL ||
(process.env.VERCEL_URL
? `https://${process.env.VERCEL_URL}`
: request.nextUrl.origin);
const logoUrl = `${baseUrl}/Logomark.png`;
return new ImageResponse(
<div
style={{
background: "linear-gradient(to top right, #ffffff, #dcdfff)",
width: "100%",
height: "100%",
display: "flex",
flexDirection: "column",
padding: "60px 80px",
position: "relative",
}}
>
{/* Logo - Top Left - SVG as data URL */}
<div
style={{
position: "absolute",
top: "60px",
left: "80px",
display: "flex",
alignItems: "center",
}}
>
<img
src={logoUrl}
alt="Plano"
width="120"
height="48"
style={{
objectFit: "contain",
}}
/>
</div>
{/* Main Content - Left-Aligned, aligned with logo */}
<div
style={{
display: "flex",
flexDirection: "column",
alignItems: "flex-start",
justifyContent: "center",
flex: 1,
width: "85%",
marginTop: "150px",
}}
>
<div
style={{
display: "flex",
flexDirection: "column",
alignItems: "flex-start",
width: "100%",
}}
>
{/* Title - Left Aligned */}
<h1
style={{
fontSize: "64px",
lineHeight: "1.1",
color: "#000000",
marginBottom: "24px",
letterSpacing: "-0.08em",
fontFamily: "IBM Plex Sans Bold",
textAlign: "left",
}}
>
{post.title}
</h1>
{/* Date - Below Title, Left Aligned */}
{/* {post.publishedAt && (
<div
style={{
fontSize: "20px",
color: "#000000",
marginBottom: "40px",
letterSpacing: "-1.8px",
fontFamily: "IBM Plex Sans Regular",
textAlign: "left",
}}
>
{formatDate(post.publishedAt)}
</div>
)} */}
{/* Author Section - Below Date, Left Aligned */}
{post.author?.name && (
<div
style={{
display: "flex",
alignItems: "center",
gap: "16px",
marginTop: "20px",
}}
>
{authorImageUrl && (
<img
src={authorImageUrl}
alt={post.author.name}
width="48"
height="48"
style={{
borderRadius: "4px",
objectFit: "cover",
}}
/>
)}
<div
style={{
display: "flex",
flexDirection: "column",
alignItems: "flex-start",
}}
>
<div
style={{
fontSize: "20px",
color: "#7780d9",
textTransform: "uppercase",
letterSpacing: "0.09em",
fontFamily: "JetBrains Mono Bold",
textAlign: "left",
}}
>
{post.author.name}
</div>
{post.author.title && (
<div
style={{
fontSize: "14px",
color: "#28327D",
textTransform: "uppercase",
letterSpacing: "0.10em",
fontFamily: "JetBrains Mono Medium",
textAlign: "left",
}}
>
{post.author.title}
</div>
)}
</div>
</div>
)}
</div>
</div>
</div>,
{
width: 1200,
height: 630,
fonts: [
{
name: "IBM Plex Sans Regular",
data: fontData.ibmPlexSans,
style: "normal",
weight: 400,
},
{
name: "IBM Plex Sans Medium",
data: fontData.ibmPlexSans,
style: "normal",
weight: 500,
},
{
name: "IBM Plex Sans Bold",
data: fontData.ibmPlexSans,
style: "normal",
weight: 700,
},
{
name: "JetBrains Mono Regular",
data: fontData.jetbrainsMonoRegular,
style: "normal",
weight: 400,
},
{
name: "JetBrains Mono Medium",
data: fontData.jetbrainsMonoMedium,
style: "normal",
weight: 500,
},
{
name: "JetBrains Mono Bold",
data: fontData.jetbrainsMonoBold,
style: "normal",
weight: 600,
},
],
},
);
} catch (error: unknown) {
const errorMessage =
error instanceof Error ? error.message : "Unknown error";
console.error("Error generating image response:", error);
return new Response(
JSON.stringify({
error: "Failed to generate image",
details: errorMessage,
}),
{ status: 500 },
);
}
}

View file

@ -0,0 +1,120 @@
import { Metadata } from "next";
import { client } from "@/lib/sanity";
type Params = Promise<{ slug: string }>;
interface BlogPost {
_id: string;
title: string;
slug: { current: string };
summary?: string;
publishedAt?: string;
author?: {
name?: string;
title?: string;
image?: any;
};
}
async function getBlogPost(slug: string): Promise<BlogPost | null> {
const query = `*[_type == "blog" && slug.current == $slug && published == true][0] {
_id,
title,
slug,
summary,
publishedAt,
author
}`;
const post = await client.fetch(query, { slug });
return post || null;
}
export async function generateMetadata({
params,
}: {
params: Params;
}): Promise<Metadata> {
try {
const resolvedParams = await params;
const post = await getBlogPost(resolvedParams.slug);
if (!post) {
return {
title: "Post Not Found - Plano",
description: "The requested blog post could not be found.",
};
}
// Get baseUrl - use NEXT_PUBLIC_APP_URL if set, otherwise construct from VERCEL_URL
// Restrict to allowed hosts: localhost:3000, archgw-tau.vercel.app, or plano.katanemo.com
let baseUrl = "http://localhost:3000";
if (process.env.NEXT_PUBLIC_APP_URL) {
const url = process.env.NEXT_PUBLIC_APP_URL;
if (
url.includes("archgw-tau.vercel.app") ||
url.includes("plano.katanemo.com") ||
url.includes("localhost:3000")
) {
baseUrl = url;
}
} else if (process.env.VERCEL_URL) {
const hostname = process.env.VERCEL_URL;
// VERCEL_URL is just the hostname, not the full URL
if (hostname === "archgw-tau.vercel.app") {
baseUrl = `https://${hostname}`;
} else if (hostname === "plano.katanemo.com") {
baseUrl = `https://${hostname}`;
}
}
const ogImageUrl = `${baseUrl}/api/og/${resolvedParams.slug}`;
const metadata: Metadata = {
title: `${post.title} - Plano Blog`,
description: post.summary || "Read more on Plano Blog",
openGraph: {
title: post.title,
description: post.summary || "Read more on Plano Blog",
type: "article",
publishedTime: post.publishedAt,
authors: post.author?.name ? [post.author.name] : undefined,
url: `${baseUrl}/blog/${resolvedParams.slug}`,
siteName: "Plano",
images: [
{
url: ogImageUrl,
width: 1200,
height: 630,
alt: post.title,
},
],
locale: "en_US",
},
twitter: {
card: "summary_large_image",
title: post.title,
description: post.summary || "Read more on Plano Blog",
images: [ogImageUrl],
},
};
return metadata;
} catch (error) {
console.error("Error generating metadata:", error);
return {
title: "Blog Post - Plano",
description: "Read this post on Plano Blog",
};
}
}
interface LayoutProps {
children: React.ReactNode;
params: Params;
}
export default async function Layout({ children, params }: LayoutProps) {
return <>{children}</>;
}

View file

@ -0,0 +1,35 @@
import Link from "next/link";
export default function NotFound() {
return (
<div className="min-h-screen bg-white flex items-center justify-center">
<div className="max-w-md mx-auto px-4 text-center">
<h1 className="text-4xl sm:text-5xl font-normal leading-tight tracking-tighter text-black mb-4">
<span className="font-sans">Post Not Found</span>
</h1>
<p className="text-lg font-sans font-[400] tracking-[-0.5px] text-black/70 mb-8">
The blog post you're looking for doesn't exist or has been removed.
</p>
<Link
href="/blog"
className="inline-flex items-center gap-2 text-base font-medium text-black hover:text-[var(--secondary)] transition-colors"
>
<svg
className="w-4 h-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M15 19l-7-7 7-7"
/>
</svg>
Back to Blog
</Link>
</div>
</div>
);
}

View file

@ -0,0 +1,206 @@
import { client, urlFor } from "@/lib/sanity";
import Image from "next/image";
import Link from "next/link";
import { PortableText } from "@/components/PortableText";
import { notFound } from "next/navigation";
import { UnlockPotentialSection } from "@/components/UnlockPotentialSection";
interface BlogPost {
_id: string;
title: string;
slug: { current: string };
summary?: string;
body?: any[];
bodyHtml?: string;
publishedAt?: string;
mainImage?: any;
mainImageUrl?: string;
author?: {
name?: string;
title?: string;
image?: any;
};
}
async function getBlogPost(slug: string): Promise<BlogPost | null> {
const query = `*[_type == "blog" && slug.current == $slug && published == true][0] {
_id,
title,
slug,
summary,
body[]{
...,
asset->{
_id,
url,
metadata {
dimensions {
width,
height,
aspectRatio
}
}
}
},
bodyHtml,
publishedAt,
mainImage,
mainImageUrl,
author
}`;
const post = await client.fetch(query, { slug });
return post || null;
}
async function getAllBlogSlugs(): Promise<string[]> {
const query = `*[_type == "blog" && published == true] {
"slug": slug.current
}`;
const posts = await client.fetch(query);
return posts.map((post: { slug: string }) => post.slug);
}
export async function generateStaticParams() {
const slugs = await getAllBlogSlugs();
return slugs.map((slug) => ({ slug }));
}
export default async function BlogPostPage({
params,
}: {
params: Promise<{ slug: string }>;
}) {
const { slug } = await params;
const post = await getBlogPost(slug);
if (!post) {
notFound();
}
return (
<article className="min-h-screen">
{/* Featured Image - First */}
{(post.mainImage || post.mainImageUrl) && (
<div className="">
<div className="max-w-[89rem] mx-auto px-4 sm:px-6 lg:px-8 pt-8 sm:pt-12 lg:pt-1 pb-8 sm:pb-12">
<div className="relative aspect-[21/8] w-full overflow-hidden rounded-lg">
{post.mainImage ? (
<Image
src={urlFor(post.mainImage).width(1600).url()}
alt={post.title}
fill
className="object-cover"
priority
/>
) : (
<Image
src={post.mainImageUrl!}
alt={post.title}
fill
className="object-cover"
priority
/>
)}
</div>
</div>
</div>
)}
{/* Content Section */}
<div className="max-w-[58rem] mx-auto px-4 sm:px-6 lg:px-8">
{/* Back to Blog Button */}
<div className="pt-4 sm:pt-6 lg:pt-8 pb-4 sm:pb-6">
<Link
href="/blog"
className="inline-flex items-center gap-2 text-sm font-medium text-black/60 hover:text-black transition-colors"
>
<svg
className="w-4 h-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M15 19l-7-7 7-7"
/>
</svg>
Back to Blog
</Link>
</div>
{/* Author and Date */}
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4 sm:gap-6 pb-4">
{post.author?.name && (
<div className="flex items-center gap-3">
{post.author.image ? (
<div className="relative w-12 h-12 rounded overflow-hidden shrink-0">
<Image
src={urlFor(post.author.image).width(80).url()}
alt={post.author.name}
fill
className="object-cover"
/>
</div>
) : (
<div className="w-12 h-12 rounded bg-[var(--secondary)]/20 shrink-0" />
)}
<div>
<div className="text-lg font-mono font-semibold tracking-wider text-primary uppercase">
{post.author.name}
</div>
{post.author.title && (
<div className="text-sm font-mono font-normal tracking-wider text-[#28327D] uppercase">
{post.author.title}
</div>
)}
</div>
</div>
)}
{post.publishedAt && (
<time
dateTime={post.publishedAt}
className="text-base font-medium tracking-[-0.9px] text-black sm:ml-auto"
>
{new Date(post.publishedAt).toLocaleDateString("en-US", {
year: "numeric",
month: "long",
day: "numeric",
})}
</time>
)}
</div>
{/* Title */}
<div className="pb-6 sm:pb-8 sm:-ml-1.5">
<h1 className="text-4xl sm:text-5xl lg:text-6xl font-medium leading-tight tracking-tighter text-black">
<span className="font-sans">{post.title}</span>
</h1>
</div>
{/* Content */}
<div className="pb-12 sm:pb-16 lg:pb-20 ">
{post.body && post.body.length > 0 ? (
<div className="prose prose-lg max-w-none">
<PortableText content={post.body} />
</div>
) : post.bodyHtml ? (
<div
className="prose prose-lg max-w-none"
dangerouslySetInnerHTML={{ __html: post.bodyHtml }}
/>
) : (
<p className="text-base sm:text-lg font-sans font-normal tracking-[-0.5px] text-black/80">
Content coming soon...
</p>
)}
</div>
</div>
<UnlockPotentialSection variant="transparent" />
</article>
);
}

View file

@ -0,0 +1,119 @@
import { client } from "@/lib/sanity";
import type { Metadata } from "next";
import { UnlockPotentialSection } from "@/components/UnlockPotentialSection";
import { BlogHeader } from "@/components/BlogHeader";
import { FeaturedBlogCard } from "@/components/FeaturedBlogCard";
import { BlogCard } from "@/components/BlogCard";
import { BlogSectionHeader } from "@/components/BlogSectionHeader";
export const metadata: Metadata = {
title: "Blog - Plano",
description: "Latest insights, updates, and stories from Plano",
};
interface BlogPost {
_id: string;
title: string;
slug: { current: string };
summary?: string;
publishedAt?: string;
mainImage?: any;
mainImageUrl?: string;
thumbnailImage?: any;
thumbnailImageUrl?: string;
author?: {
name?: string;
title?: string;
image?: any;
};
featured?: boolean;
}
function formatDate(dateString: string): string {
const date = new Date(dateString);
const day = date.getDate();
const month = date.toLocaleDateString("en-US", { month: "long" });
const year = date.getFullYear();
// Add ordinal suffix
const getOrdinal = (n: number) => {
const s = ["th", "st", "nd", "rd"];
const v = n % 100;
return n + (s[(v - 20) % 10] || s[v] || s[0]);
};
return `${month} ${getOrdinal(day)}, ${year}`;
}
async function getBlogPosts(): Promise<BlogPost[]> {
const query = `*[_type == "blog" && published == true] | order(publishedAt desc) {
_id,
title,
slug,
summary,
publishedAt,
mainImage,
mainImageUrl,
thumbnailImage,
thumbnailImageUrl,
author,
featured
}`;
return await client.fetch(query);
}
export default async function BlogPage() {
const posts = await getBlogPosts();
const featuredPost = posts.find((post) => post.featured) || posts[0];
const recentPosts = posts
.filter((post) => post._id !== featuredPost?._id)
.slice(0, 3);
// Format dates in server component
const featuredPostWithDate = featuredPost
? {
...featuredPost,
formattedDate: featuredPost.publishedAt
? formatDate(featuredPost.publishedAt)
: undefined,
}
: null;
const recentPostsWithDates = recentPosts.map((post) => ({
...post,
formattedDate: post.publishedAt ? formatDate(post.publishedAt) : undefined,
}));
return (
<div className="min-h-screen">
{/* Header Section */}
<BlogHeader />
{/* Featured Post */}
{featuredPostWithDate && (
<section className="">
<div className="max-w-[85rem] mx-auto px-4 sm:px-6 lg:px-8 pb-8 sm:pb-12 lg:pb-0">
<FeaturedBlogCard post={featuredPostWithDate} />
</div>
</section>
)}
{/* Recent Posts Section */}
{recentPostsWithDates.length > 0 && (
<section className="border-b border-black/10 py-8 sm:py-12 lg:py-24">
<div className="max-w-[85rem] mx-auto px-4 sm:px-6 lg:px-8">
<BlogSectionHeader />
<div className="grid sm:grid-cols-2 lg:grid-cols-3 gap-6 lg:gap-8">
{recentPostsWithDates.map((post, index) => (
<BlogCard key={post._id} post={post} index={index} />
))}
</div>
</div>
</section>
)}
{/* Call to Action Section */}
<UnlockPotentialSection variant="transparent" />
</div>
);
}

View file

@ -0,0 +1,16 @@
import React from "react";
export default function DocsPage() {
return (
<section className="px-4 sm:px-6 lg:px-8 py-12 sm:py-16 lg:py-24">
<div className="max-w-[81rem] mx-auto">
<h1 className="text-4xl sm:text-5xl lg:text-7xl font-normal leading-tight tracking-tighter text-black mb-6">
<span className="font-sans">Documentation</span>
</h1>
<p className="text-lg sm:text-xl lg:text-2xl font-sans font-[400] tracking-[-1.2px] text-black/70">
Coming soon...
</p>
</div>
</section>
);
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

View file

@ -0,0 +1,2 @@
/* This file is kept for backwards compatibility but styles are imported from @katanemo/shared-styles */
@import "@katanemo/shared-styles/globals.css";

View file

@ -0,0 +1,39 @@
import type { Metadata } from "next";
import Script from "next/script";
import "@katanemo/shared-styles/globals.css";
import { Analytics } from "@vercel/analytics/next";
import { ConditionalLayout } from "@/components/ConditionalLayout";
export const metadata: Metadata = {
title: "Plano - Delivery Infrastructure for Agentic Apps",
description:
"Build agents faster, and deliver them reliably to production - by offloading the critical plumbing work to Plano!",
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body className="antialiased">
{/* Google tag (gtag.js) */}
<Script
src="https://www.googletagmanager.com/gtag/js?id=G-6J5LQH3Q9G"
strategy="afterInteractive"
/>
<Script id="google-analytics" strategy="afterInteractive">
{`
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'G-6J5LQH3Q9G');
`}
</Script>
<ConditionalLayout>{children}</ConditionalLayout>
<Analytics />
</body>
</html>
);
}

28
apps/www/src/app/page.tsx Normal file
View file

@ -0,0 +1,28 @@
"use client";
import React from "react";
import { Hero } from "@/components/Hero";
import { IntroSection } from "@/components/IntroSection";
import { IdeaToAgentSection } from "@/components/IdeaToAgentSection";
import { UseCasesSection } from "@/components/UseCasesSection";
import { VerticalCarouselSection } from "@/components/VerticalCarouselSection";
import { HowItWorksSection } from "@/components/HowItWorksSection";
import { UnlockPotentialSection } from "@/components/UnlockPotentialSection";
import { LogoCloud } from "@/components/LogoCloud";
export default function Home() {
return (
<>
<Hero />
<LogoCloud />
<IntroSection />
<IdeaToAgentSection />
<UseCasesSection />
<VerticalCarouselSection />
<HowItWorksSection />
<UnlockPotentialSection variant="transparent" />
{/* Rest of the sections will be refactored next */}
</>
);
}

View file

@ -0,0 +1,28 @@
"use client";
import React from "react";
import {
ResearchHero,
ResearchGrid,
ResearchTimeline,
ResearchCTA,
ResearchCapabilities,
ResearchBenchmarks,
ResearchFamily,
} from "@/components/research";
import { UnlockPotentialSection } from "@/components/UnlockPotentialSection";
export default function ResearchPage() {
return (
<>
<ResearchHero />
<ResearchGrid />
<ResearchTimeline />
<ResearchCTA />
<ResearchCapabilities />
<ResearchBenchmarks />
{/* <ResearchFamily /> */}
<UnlockPotentialSection variant="transparent" />
</>
);
}

View file

@ -0,0 +1,8 @@
"use client";
import { NextStudio } from "next-sanity/studio";
import config from "../../../../sanity.config";
export default function StudioPage() {
return <NextStudio config={config} />;
}

View file

@ -0,0 +1,11 @@
export default function StudioLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<div className="fixed inset-0 h-screen w-screen overflow-hidden">
{children}
</div>
);
}