From b843394d0f9e0e0069823901e5d2354362c3ad61 Mon Sep 17 00:00:00 2001 From: Marco Sadjadi Date: Thu, 21 May 2026 19:16:40 +0200 Subject: [PATCH] =?UTF-8?q?feat(web):=20full=20SEO=20stack=20=E2=80=94=20m?= =?UTF-8?q?etadata,=20JSON-LD,=20sitemap,=20robots,=20OG=20image?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ported and adapted from the BuildMyDiscord SEO setup: - lib/seo.ts — single source for site constants, the FAQ data (shared by the rendered FAQ and the FAQPage schema so they never drift) and JSON-LD builders. - Rich root metadata: title template, keywords, Open Graph, Twitter card, robots directives, canonical. - JSON-LD: Organization + WebSite + SoftwareApplication sitewide, FAQPage on the landing page. No AggregateRating — there are no real reviews yet. - app/robots.ts — allow all, explicit allow-list for AI answer-engine crawlers (GPTBot, ClaudeBot, PerplexityBot, …), disallow private routes. - app/sitemap.ts — every public marketing + docs route. - app/opengraph-image.tsx — monochrome on-brand 1200x630 share card. - app/manifest.ts + public/llms.txt. - Per-page metadata for pricing, changelog, security, privacy, terms, docs, templates and status. - opengraph-image + apple-icon pinned to the edge runtime — next/og crashes during a Node-runtime prerender. Verified: next build passes; /robots.txt, /sitemap.xml, /manifest.webmanifest and /opengraph-image all generate. Co-Authored-By: Claude Opus 4.7 (1M context) --- apps/web/app/(marketing)/changelog/page.tsx | 8 +- apps/web/app/(marketing)/page.tsx | 129 ++++++------ apps/web/app/(marketing)/pricing/page.tsx | 12 +- apps/web/app/(marketing)/privacy/page.tsx | 16 +- apps/web/app/(marketing)/security/page.tsx | 21 +- apps/web/app/(marketing)/status/layout.tsx | 14 ++ apps/web/app/(marketing)/terms/page.tsx | 9 +- apps/web/app/apple-icon.tsx | 57 +++--- apps/web/app/docs/layout.tsx | 10 +- apps/web/app/layout.tsx | 52 ++++- apps/web/app/manifest.ts | 15 ++ apps/web/app/opengraph-image.tsx | 107 ++++++++++ apps/web/app/robots.ts | 38 ++++ apps/web/app/sitemap.ts | 36 ++++ apps/web/app/templates/layout.tsx | 14 ++ apps/web/components/json-ld.tsx | 13 ++ apps/web/lib/seo.ts | 210 ++++++++++++++++++++ apps/web/public/llms.txt | 40 ++++ 18 files changed, 689 insertions(+), 112 deletions(-) create mode 100644 apps/web/app/(marketing)/status/layout.tsx create mode 100644 apps/web/app/manifest.ts create mode 100644 apps/web/app/opengraph-image.tsx create mode 100644 apps/web/app/robots.ts create mode 100644 apps/web/app/sitemap.ts create mode 100644 apps/web/app/templates/layout.tsx create mode 100644 apps/web/components/json-ld.tsx create mode 100644 apps/web/lib/seo.ts create mode 100644 apps/web/public/llms.txt diff --git a/apps/web/app/(marketing)/changelog/page.tsx b/apps/web/app/(marketing)/changelog/page.tsx index e203302..33c8a4d 100644 --- a/apps/web/app/(marketing)/changelog/page.tsx +++ b/apps/web/app/(marketing)/changelog/page.tsx @@ -1,6 +1,12 @@ import { CodeBlock } from '@/components/code-block'; +import { pageMetadata } from '@/lib/seo'; -export const metadata = { title: 'Changelog — BuildMyMCPServer' }; +export const metadata = pageMetadata({ + title: 'Changelog', + description: + 'Product updates and release notes for BuildMyMCPServer — new features, fixes and improvements to the MCP server platform.', + path: '/changelog', +}); interface Release { version: string; diff --git a/apps/web/app/(marketing)/page.tsx b/apps/web/app/(marketing)/page.tsx index c984cca..a5dd698 100644 --- a/apps/web/app/(marketing)/page.tsx +++ b/apps/web/app/(marketing)/page.tsx @@ -1,5 +1,7 @@ -import Link from 'next/link'; import { CodeBlock } from '@/components/code-block'; +import { JsonLd } from '@/components/json-ld'; +import { FAQ, faqJsonLd } from '@/lib/seo'; +import Link from 'next/link'; const PROMPT_EXAMPLE = `Create an MCP server that searches our Notion workspace. Tools: search_pages, get_page_content. @@ -45,62 +47,37 @@ const MARKETPLACE_POINTS: { t: string; d: string }[] = [ }, ]; -const FAQ: { q: string; a: string }[] = [ - { - q: 'What is MCP?', - a: 'Model Context Protocol — an open standard from Anthropic for connecting AI assistants to external tools, data and APIs over a transport like Streamable HTTP.', - }, - { - q: 'Do I need to write code?', - a: 'No. You describe the tool in natural language. We generate the TypeScript server, run static checks, build a Docker image and deploy it to a public OAuth-protected URL.', - }, - { - q: 'Which clients work?', - a: 'Claude Desktop, Cursor, ChatGPT Custom Connectors, VS Code Copilot, Continue.dev — anything that speaks the MCP spec.', - }, - { - q: 'How is auth handled?', - a: 'Every generated server is an OAuth 2.1 Resource Server. Our control plane is the Authorization Server (PKCE + Dynamic Client Registration + Resource Indicators per RFC 8707).', - }, - { - q: 'Can I self-host?', - a: 'Yes. The runner is a plain Docker container; the control plane is open to BYO Postgres + Redis. See the self-hosting guide in docs.', - }, - { - q: 'What about secrets?', - a: 'AES-256-GCM at rest in Postgres, injected as environment variables into the runtime container. Never logged, never echoed back.', - }, - { - q: 'Cold starts?', - a: 'No cold starts. Containers stay warm. Sub-50ms tool-call overhead on average for in-region requests.', - }, - { - q: 'Rate limits?', - a: 'Default 100 requests/min/IP per tool. Configurable per server. Quota enforced at the Traefik layer before hitting your container.', - }, - { - q: 'How fast is generation?', - a: 'Spec → image → live URL typically completes in 45-90 seconds.', - }, - { - q: 'Logs and metrics?', - a: 'Live log streaming to the dashboard, structured tool-call metrics (P50/P95/P99 latency, error rate, per-tool throughput) — all retained for 30 days.', - }, - { - q: 'What if I cancel?', - a: 'You can export the full TypeScript source of every server you built. No vendor lock-in.', - }, - { - q: 'Custom domain?', - a: 'Pro plan and above. Add a CNAME, we provision Let’s Encrypt automatically.', - }, -]; - const TIERS = [ - { name: 'Hobby', price: '€0', tag: 'Forever free', features: ['1 server', '100k calls/mo', 'BMM subdomain', 'Community support'] }, - { name: 'Pro', price: '€49', tag: '/ month', features: ['5 servers', '1M calls/mo', 'Custom domain', 'Priority build queue', 'Email support'] }, - { name: 'Team', price: '€149', tag: '/ month', features: ['25 servers', '10M calls/mo', 'RBAC + audit log', 'SLA 99.9%', 'Slack support'] }, - { name: 'Enterprise', price: '€499+', tag: '/ month', features: ['Unlimited', 'BYOC', 'SSO / SAML', 'Dedicated cluster', 'Customer success'] }, + { + name: 'Hobby', + price: '€0', + tag: 'Forever free', + features: ['1 server', '100k calls/mo', 'BMM subdomain', 'Community support'], + }, + { + name: 'Pro', + price: '€49', + tag: '/ month', + features: [ + '5 servers', + '1M calls/mo', + 'Custom domain', + 'Priority build queue', + 'Email support', + ], + }, + { + name: 'Team', + price: '€149', + tag: '/ month', + features: ['25 servers', '10M calls/mo', 'RBAC + audit log', 'SLA 99.9%', 'Slack support'], + }, + { + name: 'Enterprise', + price: '€499+', + tag: '/ month', + features: ['Unlimited', 'BYOC', 'SSO / SAML', 'Dedicated cluster', 'Customer success'], + }, ]; export default function Landing() { @@ -176,12 +153,26 @@ export default function Landing() {
{[ - { n: '01', t: 'Describe your tool', d: 'A sentence is enough. List your secrets and which APIs to call.' }, - { n: '02', t: 'We generate, check, deploy', d: 'Claude writes the spec. We render TypeScript, run static checks, build a container, deploy to your subdomain.' }, - { n: '03', t: 'Install in your client', d: 'Copy the snippet into Claude Desktop, Cursor or ChatGPT. OAuth flow on first use.' }, + { + n: '01', + t: 'Describe your tool', + d: 'A sentence is enough. List your secrets and which APIs to call.', + }, + { + n: '02', + t: 'We generate, check, deploy', + d: 'Claude writes the spec. We render TypeScript, run static checks, build a container, deploy to your subdomain.', + }, + { + n: '03', + t: 'Install in your client', + d: 'Copy the snippet into Claude Desktop, Cursor or ChatGPT. OAuth flow on first use.', + }, ].map((s) => (
-
{s.n}
+
+ {s.n} +

{s.t}

{s.d}

@@ -211,16 +202,23 @@ export default function Landing() {
-

Built for the work you actually have

+

+ Built for the work you actually have +

Anything with an HTTP API or a database, in minutes.

{EXAMPLES.map((e) => ( -
+
{e.title}
-

{e.desc}

+

+ {e.desc} +

))}
@@ -273,7 +271,9 @@ export default function Landing() { key={t.name} className={`panel p-5 ${i === 1 ? 'border-[--color-accent]/40' : ''}`} > -
{t.name}
+
+ {t.name} +
{t.price} {t.tag} @@ -291,6 +291,7 @@ export default function Landing() { {/* FAQ */}
+

FAQ

diff --git a/apps/web/app/(marketing)/pricing/page.tsx b/apps/web/app/(marketing)/pricing/page.tsx index 040f5fb..798bf24 100644 --- a/apps/web/app/(marketing)/pricing/page.tsx +++ b/apps/web/app/(marketing)/pricing/page.tsx @@ -1,6 +1,12 @@ +import { pageMetadata } from '@/lib/seo'; import Link from 'next/link'; -export const metadata = { title: 'Pricing — BuildMyMCPServer' }; +export const metadata = pageMetadata({ + title: 'Pricing', + description: + 'BuildMyMCPServer pricing — start free with one hosted MCP server, scale to Pro, Team and Enterprise. Pay for tool calls, not boilerplate.', + path: '/pricing', +}); const TIERS = [ { @@ -113,7 +119,9 @@ export default function Pricing() { {t.price} {t.tag}
-

{t.description}

+

+ {t.description} +

    {t.features.map((f) => (
  • — {f}
  • diff --git a/apps/web/app/(marketing)/privacy/page.tsx b/apps/web/app/(marketing)/privacy/page.tsx index 85c6ece..16a0be3 100644 --- a/apps/web/app/(marketing)/privacy/page.tsx +++ b/apps/web/app/(marketing)/privacy/page.tsx @@ -1,4 +1,11 @@ -export const metadata = { title: 'Privacy — BuildMyMCPServer' }; +import { pageMetadata } from '@/lib/seo'; + +export const metadata = pageMetadata({ + title: 'Privacy', + description: + 'BuildMyMCPServer privacy policy — what data we collect, how it is used, and the rights you have over it.', + path: '/privacy', +}); const SECTIONS = [ { @@ -29,7 +36,7 @@ const SECTIONS = [ { h: 'Subprocessors', p: [ - 'Anthropic (generation) — only the prompt text you send. Anthropic\'s data-retention policy applies.', + "Anthropic (generation) — only the prompt text you send. Anthropic's data-retention policy applies.", 'Hetzner (compute).', 'Backblaze (encrypted backups).', 'Stripe (billing).', @@ -91,7 +98,10 @@ export default function Privacy() {

    Contact

    Data controller: BuildMyMCPServer. Email{' '} - + privacy@buildmymcpserver.com {' '} for any of the above. diff --git a/apps/web/app/(marketing)/security/page.tsx b/apps/web/app/(marketing)/security/page.tsx index a5e4d0b..51c76d1 100644 --- a/apps/web/app/(marketing)/security/page.tsx +++ b/apps/web/app/(marketing)/security/page.tsx @@ -1,7 +1,13 @@ -import Link from 'next/link'; import { CodeBlock } from '@/components/code-block'; +import { pageMetadata } from '@/lib/seo'; +import Link from 'next/link'; -export const metadata = { title: 'Security — BuildMyMCPServer' }; +export const metadata = pageMetadata({ + title: 'Security', + description: + 'How BuildMyMCPServer secures your MCP servers — per-server Docker isolation, AES-256-GCM encrypted secrets, OAuth 2.1 and a hardened control plane.', + path: '/security', +}); const PILLARS = [ { @@ -18,7 +24,7 @@ const PILLARS = [ }, { title: 'No token passthrough', - body: 'When a tool calls a downstream API, it uses its own server-side credentials — not the user\'s OAuth token. Tokens never leak across trust boundaries. This is mandated by the MCP authorization spec.', + body: "When a tool calls a downstream API, it uses its own server-side credentials — not the user's OAuth token. Tokens never leak across trust boundaries. This is mandated by the MCP authorization spec.", }, { title: 'Static security checks', @@ -49,8 +55,8 @@ export default function Security() { Built like infrastructure.

    - We host code generated by an LLM, on behalf of customers, that exposes their internal - APIs to AI clients. The threat model is real. Here is what we do about it. + We host code generated by an LLM, on behalf of customers, that exposes their internal APIs + to AI clients. The threat model is real. Here is what we do about it.

    @@ -67,7 +73,10 @@ export default function Security() {

    Disclosure

    Found a vulnerability? Email{' '} - + security@buildmymcpserver.com {' '} with a clear reproduction. We respond within 48h. We do not run a paid bounty yet, but we diff --git a/apps/web/app/(marketing)/status/layout.tsx b/apps/web/app/(marketing)/status/layout.tsx new file mode 100644 index 0000000..7172d88 --- /dev/null +++ b/apps/web/app/(marketing)/status/layout.tsx @@ -0,0 +1,14 @@ +import { pageMetadata } from '@/lib/seo'; + +// status/page.tsx is a client component and cannot export metadata itself — +// this layout carries it. +export const metadata = pageMetadata({ + title: 'Status', + description: + 'Live operational status of BuildMyMCPServer — control plane, build pipeline and hosted MCP servers.', + path: '/status', +}); + +export default function StatusLayout({ children }: { children: React.ReactNode }) { + return children; +} diff --git a/apps/web/app/(marketing)/terms/page.tsx b/apps/web/app/(marketing)/terms/page.tsx index 574a1af..d933c65 100644 --- a/apps/web/app/(marketing)/terms/page.tsx +++ b/apps/web/app/(marketing)/terms/page.tsx @@ -1,4 +1,11 @@ -export const metadata = { title: 'Terms — BuildMyMCPServer' }; +import { pageMetadata } from '@/lib/seo'; + +export const metadata = pageMetadata({ + title: 'Terms', + description: + 'BuildMyMCPServer terms of service — the agreement that governs use of the platform.', + path: '/terms', +}); const SECTIONS = [ { diff --git a/apps/web/app/apple-icon.tsx b/apps/web/app/apple-icon.tsx index 0101bca..3490682 100644 --- a/apps/web/app/apple-icon.tsx +++ b/apps/web/app/apple-icon.tsx @@ -1,40 +1,37 @@ import { ImageResponse } from 'next/og'; +// Edge runtime — see opengraph-image.tsx: avoids the next/og fileURLToPath +// crash during a Node-runtime prerender (notably on Windows builds). +export const runtime = 'edge'; + export const size = { width: 180, height: 180 }; export const contentType = 'image/png'; export default function AppleIcon() { return new ImageResponse( - ( -

    - - BuildMyMCPServer - - -
    - ), +
    + + BuildMyMCPServer + + +
    , { ...size }, ); } diff --git a/apps/web/app/docs/layout.tsx b/apps/web/app/docs/layout.tsx index 6a27328..970f9cc 100644 --- a/apps/web/app/docs/layout.tsx +++ b/apps/web/app/docs/layout.tsx @@ -1,5 +1,13 @@ -import Link from 'next/link'; import { Logo } from '@/components/logo'; +import { pageMetadata } from '@/lib/seo'; +import Link from 'next/link'; + +export const metadata = pageMetadata({ + title: 'Docs', + description: + 'BuildMyMCPServer documentation — quickstart, MCP concepts, the OAuth 2.1 flow, authoring tools, self-hosting and the API reference.', + path: '/docs', +}); const SECTIONS: { heading: string; items: { href: string; label: string }[] }[] = [ { diff --git a/apps/web/app/layout.tsx b/apps/web/app/layout.tsx index 1f41aab..e6b8402 100644 --- a/apps/web/app/layout.tsx +++ b/apps/web/app/layout.tsx @@ -1,14 +1,57 @@ +import { JsonLd } from '@/components/json-ld'; import { SiteBanner } from '@/components/site-banner'; +import { + SEO_KEYWORDS, + SITE_DESCRIPTION, + SITE_NAME, + SITE_TAGLINE, + SITE_URL, + siteJsonLd, +} from '@/lib/seo'; import { GeistMono } from 'geist/font/mono'; import { GeistSans } from 'geist/font/sans'; import type { Metadata } from 'next'; import './globals.css'; +const TITLE = `${SITE_NAME} — ${SITE_TAGLINE}`; + export const metadata: Metadata = { - title: 'BuildMyMCPServer — Describe your tool. We host the server.', - description: - 'From prompt to production MCP server in 60 seconds. OAuth 2.1, Streamable HTTP, ready for Claude, Cursor & ChatGPT.', - metadataBase: new URL(process.env.NEXT_PUBLIC_APP_URL ?? 'http://localhost:3001'), + metadataBase: new URL(SITE_URL), + title: { + default: TITLE, + template: `%s | ${SITE_NAME}`, + }, + description: SITE_DESCRIPTION, + applicationName: SITE_NAME, + keywords: SEO_KEYWORDS, + authors: [{ name: SITE_NAME }], + creator: SITE_NAME, + publisher: SITE_NAME, + alternates: { canonical: '/' }, + openGraph: { + type: 'website', + locale: 'en_US', + url: SITE_URL, + siteName: SITE_NAME, + title: TITLE, + description: SITE_DESCRIPTION, + }, + twitter: { + card: 'summary_large_image', + title: TITLE, + description: SITE_DESCRIPTION, + }, + robots: { + index: true, + follow: true, + googleBot: { + index: true, + follow: true, + 'max-image-preview': 'large', + 'max-snippet': -1, + 'max-video-preview': -1, + }, + }, }; export default function RootLayout({ children }: { children: React.ReactNode }) { @@ -19,6 +62,7 @@ export default function RootLayout({ children }: { children: React.ReactNode }) suppressHydrationWarning > + {children} diff --git a/apps/web/app/manifest.ts b/apps/web/app/manifest.ts new file mode 100644 index 0000000..b82ac5f --- /dev/null +++ b/apps/web/app/manifest.ts @@ -0,0 +1,15 @@ +import { SITE_DESCRIPTION, SITE_NAME } from '@/lib/seo'; +import type { MetadataRoute } from 'next'; + +export default function manifest(): MetadataRoute.Manifest { + return { + name: SITE_NAME, + short_name: 'BuildMyMCP', + description: SITE_DESCRIPTION, + start_url: '/', + display: 'standalone', + background_color: '#0a0a0b', + theme_color: '#0a0a0b', + icons: [{ src: '/icon.svg', sizes: 'any', type: 'image/svg+xml' }], + }; +} diff --git a/apps/web/app/opengraph-image.tsx b/apps/web/app/opengraph-image.tsx new file mode 100644 index 0000000..75967c4 --- /dev/null +++ b/apps/web/app/opengraph-image.tsx @@ -0,0 +1,107 @@ +import { ImageResponse } from 'next/og'; + +// Edge runtime: next/og loads its assets via fileURLToPath, which throws an +// "Invalid URL" during a Node-runtime prerender. The edge runtime sidesteps +// that and is how the OG image route builds reliably. +export const runtime = 'edge'; + +// Sitewide Open Graph / social-share card. Monochrome to match the brand — +// flat colours only, a single indigo accent, no gradients. +export const alt = 'BuildMyMCPServer — Describe your tool. We host the MCP server.'; +export const size = { width: 1200, height: 630 }; +export const contentType = 'image/png'; + +export default function Image() { + return new ImageResponse( +
    +
    +
    + M +
    +
    + BuildMyMCPServer +
    +
    + +
    +
    + Describe your tool. +
    +
    + We host the MCP server. +
    +
    + Prompt to a hosted, OAuth 2.1-protected MCP server in 60 seconds. +
    +
    + +
    +
    +
    + OAuth 2.1 · Streamable HTTP · Docker-isolated +
    +
    buildmymcpserver.com
    +
    +
    , + { ...size }, + ); +} diff --git a/apps/web/app/robots.ts b/apps/web/app/robots.ts new file mode 100644 index 0000000..567c7a6 --- /dev/null +++ b/apps/web/app/robots.ts @@ -0,0 +1,38 @@ +import { SITE_URL } from '@/lib/seo'; +import type { MetadataRoute } from 'next'; + +// AI search/answer crawlers — explicitly allowed so the product surfaces in +// ChatGPT, Claude, Perplexity, Google AI and similar answer engines. +const AI_CRAWLERS = [ + 'GPTBot', + 'OAI-SearchBot', + 'ChatGPT-User', + 'ClaudeBot', + 'anthropic-ai', + 'Claude-Web', + 'PerplexityBot', + 'Perplexity-User', + 'Google-Extended', + 'CCBot', + 'Applebot-Extended', + 'Amazonbot', + 'meta-externalagent', + 'DuckAssistBot', +]; + +export default function robots(): MetadataRoute.Robots { + return { + rules: [ + { + userAgent: '*', + allow: '/', + disallow: ['/api/', '/admin', '/dashboard', '/login'], + }, + // Aggressive scraper with no search value. + { userAgent: 'Bytespider', disallow: '/' }, + { userAgent: AI_CRAWLERS, allow: '/', disallow: ['/api/', '/admin', '/dashboard'] }, + ], + sitemap: `${SITE_URL}/sitemap.xml`, + host: SITE_URL, + }; +} diff --git a/apps/web/app/sitemap.ts b/apps/web/app/sitemap.ts new file mode 100644 index 0000000..b8b327e --- /dev/null +++ b/apps/web/app/sitemap.ts @@ -0,0 +1,36 @@ +import { SITE_URL } from '@/lib/seo'; +import type { MetadataRoute } from 'next'; + +type Entry = { + path: string; + priority: number; + changeFrequency: MetadataRoute.Sitemap[number]['changeFrequency']; +}; + +const ROUTES: Entry[] = [ + { path: '/', priority: 1.0, changeFrequency: 'weekly' }, + { path: '/pricing', priority: 0.9, changeFrequency: 'weekly' }, + { path: '/templates', priority: 0.9, changeFrequency: 'daily' }, + { path: '/docs', priority: 0.8, changeFrequency: 'weekly' }, + { path: '/docs/concepts', priority: 0.7, changeFrequency: 'monthly' }, + { path: '/docs/oauth', priority: 0.7, changeFrequency: 'monthly' }, + { path: '/docs/authoring', priority: 0.7, changeFrequency: 'monthly' }, + { path: '/docs/api-reference', priority: 0.7, changeFrequency: 'monthly' }, + { path: '/docs/self-hosting', priority: 0.7, changeFrequency: 'monthly' }, + { path: '/docs/faq', priority: 0.6, changeFrequency: 'monthly' }, + { path: '/changelog', priority: 0.6, changeFrequency: 'weekly' }, + { path: '/security', priority: 0.5, changeFrequency: 'monthly' }, + { path: '/status', priority: 0.4, changeFrequency: 'weekly' }, + { path: '/privacy', priority: 0.3, changeFrequency: 'yearly' }, + { path: '/terms', priority: 0.3, changeFrequency: 'yearly' }, +]; + +export default function sitemap(): MetadataRoute.Sitemap { + const now = new Date(); + return ROUTES.map((r) => ({ + url: `${SITE_URL}${r.path}`, + lastModified: now, + changeFrequency: r.changeFrequency, + priority: r.priority, + })); +} diff --git a/apps/web/app/templates/layout.tsx b/apps/web/app/templates/layout.tsx new file mode 100644 index 0000000..7dc22c2 --- /dev/null +++ b/apps/web/app/templates/layout.tsx @@ -0,0 +1,14 @@ +import { pageMetadata } from '@/lib/seo'; + +// templates/page.tsx is a client component and cannot export metadata itself — +// this layout carries it. +export const metadata = pageMetadata({ + title: 'Template marketplace', + description: + 'Browse the BuildMyMCPServer template marketplace — fork a ready-made MCP server, add your own credentials and deploy in seconds.', + path: '/templates', +}); + +export default function TemplatesLayout({ children }: { children: React.ReactNode }) { + return children; +} diff --git a/apps/web/components/json-ld.tsx b/apps/web/components/json-ld.tsx new file mode 100644 index 0000000..6138051 --- /dev/null +++ b/apps/web/components/json-ld.tsx @@ -0,0 +1,13 @@ +// Renders a JSON-LD structured-data script. The `<` escape prevents a +// `` sequence in the data from breaking out of the tag. +export function JsonLd({ data }: { data: object }) { + return ( +