feat(web): full SEO stack — metadata, JSON-LD, sitemap, robots, OG image
Some checks failed
Deploy to Production / deploy (push) Failing after 46s

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) <noreply@anthropic.com>
This commit is contained in:
Marco Sadjadi
2026-05-21 19:16:40 +02:00
parent 617886352c
commit b843394d0f
18 changed files with 689 additions and 112 deletions

View File

@@ -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;

View File

@@ -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 Lets 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() {
</div>
<div className="grid gap-6 md:grid-cols-3">
{[
{ 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) => (
<div key={s.n} className="panel p-5">
<div className="mono text-[11px] tracking-widest text-[--color-fg-subtle]">{s.n}</div>
<div className="mono text-[11px] tracking-widest text-[--color-fg-subtle]">
{s.n}
</div>
<h3 className="mt-4 text-[15px] font-semibold tracking-tight">{s.t}</h3>
<p className="mt-2 text-[13px] leading-relaxed text-[--color-fg-muted]">{s.d}</p>
</div>
@@ -211,16 +202,23 @@ export default function Landing() {
<section className="border-b border-[--color-border] py-20">
<div className="mx-auto max-w-6xl px-6">
<div className="mb-10 max-w-2xl">
<h2 className="text-[28px] font-semibold tracking-tight">Built for the work you actually have</h2>
<h2 className="text-[28px] font-semibold tracking-tight">
Built for the work you actually have
</h2>
<p className="mt-2 text-[14px] text-[--color-fg-muted]">
Anything with an HTTP API or a database, in minutes.
</p>
</div>
<div className="grid gap-3 md:grid-cols-3">
{EXAMPLES.map((e) => (
<div key={e.title} className="panel p-4 transition-colors hover:border-[--color-border-strong]">
<div
key={e.title}
className="panel p-4 transition-colors hover:border-[--color-border-strong]"
>
<div className="text-[13px] font-semibold tracking-tight">{e.title}</div>
<p className="mt-1 text-[12.5px] leading-relaxed text-[--color-fg-muted]">{e.desc}</p>
<p className="mt-1 text-[12.5px] leading-relaxed text-[--color-fg-muted]">
{e.desc}
</p>
</div>
))}
</div>
@@ -273,7 +271,9 @@ export default function Landing() {
key={t.name}
className={`panel p-5 ${i === 1 ? 'border-[--color-accent]/40' : ''}`}
>
<div className="text-[12px] uppercase tracking-wider text-[--color-fg-subtle]">{t.name}</div>
<div className="text-[12px] uppercase tracking-wider text-[--color-fg-subtle]">
{t.name}
</div>
<div className="mt-2 flex items-baseline gap-1">
<span className="text-[26px] font-semibold tracking-tight">{t.price}</span>
<span className="text-[12px] text-[--color-fg-subtle]">{t.tag}</span>
@@ -291,6 +291,7 @@ export default function Landing() {
{/* FAQ */}
<section className="py-20">
<JsonLd data={faqJsonLd()} />
<div className="mx-auto max-w-6xl px-6">
<h2 className="text-[28px] font-semibold tracking-tight">FAQ</h2>
<div className="mt-8 grid gap-x-12 gap-y-6 md:grid-cols-2">

View File

@@ -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() {
<span className="text-[28px] font-semibold tracking-tight">{t.price}</span>
<span className="text-[12px] text-[--color-fg-subtle]">{t.tag}</span>
</div>
<p className="mt-2 text-[12px] leading-relaxed text-[--color-fg-muted]">{t.description}</p>
<p className="mt-2 text-[12px] leading-relaxed text-[--color-fg-muted]">
{t.description}
</p>
<ul className="mt-4 space-y-1.5 text-[12.5px] text-[--color-fg-muted]">
{t.features.map((f) => (
<li key={f}> {f}</li>

View File

@@ -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() {
<h2 className="text-[16px] font-semibold tracking-tight">Contact</h2>
<p className="mt-3 text-[13.5px] leading-relaxed text-[--color-fg-muted]">
Data controller: BuildMyMCPServer. Email{' '}
<a className="text-[--color-accent] underline" href="mailto:privacy@buildmymcpserver.com">
<a
className="text-[--color-accent] underline"
href="mailto:privacy@buildmymcpserver.com"
>
privacy@buildmymcpserver.com
</a>{' '}
for any of the above.

View File

@@ -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.
</h1>
<p className="mt-3 text-[14px] leading-relaxed text-[--color-fg-muted]">
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.
</p>
</header>
@@ -67,7 +73,10 @@ export default function Security() {
<h2 className="text-[18px] font-semibold tracking-tight">Disclosure</h2>
<p className="mt-2 text-[13.5px] leading-relaxed text-[--color-fg-muted]">
Found a vulnerability? Email{' '}
<a className="text-[--color-accent] underline" href="mailto:security@buildmymcpserver.com">
<a
className="text-[--color-accent] underline"
href="mailto:security@buildmymcpserver.com"
>
security@buildmymcpserver.com
</a>{' '}
with a clear reproduction. We respond within 48h. We do not run a paid bounty yet, but we

View File

@@ -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;
}

View File

@@ -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 = [
{

View File

@@ -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(
(
<div
style={{
width: '100%',
height: '100%',
background: '#6366F1',
borderRadius: 38,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
<svg
width="120"
height="120"
viewBox="0 0 32 32"
xmlns="http://www.w3.org/2000/svg"
>
<title>BuildMyMCPServer</title>
<path
d="M8.5 22.5V9.5L16 16L23.5 9.5V22.5"
stroke="#FFFFFF"
strokeWidth={2.4}
strokeLinecap="round"
strokeLinejoin="round"
fill="none"
/>
</svg>
</div>
),
<div
style={{
width: '100%',
height: '100%',
background: '#6366F1',
borderRadius: 38,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
<svg width="120" height="120" viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">
<title>BuildMyMCPServer</title>
<path
d="M8.5 22.5V9.5L16 16L23.5 9.5V22.5"
stroke="#FFFFFF"
strokeWidth={2.4}
strokeLinecap="round"
strokeLinejoin="round"
fill="none"
/>
</svg>
</div>,
{ ...size },
);
}

View File

@@ -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 }[] }[] = [
{

View File

@@ -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
>
<body>
<JsonLd data={siteJsonLd()} />
<SiteBanner />
{children}
</body>

15
apps/web/app/manifest.ts Normal file
View File

@@ -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' }],
};
}

View File

@@ -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(
<div
style={{
width: '100%',
height: '100%',
display: 'flex',
flexDirection: 'column',
justifyContent: 'space-between',
backgroundColor: '#0a0a0b',
padding: '72px',
fontFamily: 'sans-serif',
}}
>
<div style={{ display: 'flex', alignItems: 'center', gap: '16px' }}>
<div
style={{
width: '46px',
height: '46px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
border: '2px solid #fafafa',
borderRadius: '9px',
color: '#fafafa',
fontSize: '26px',
fontWeight: 700,
}}
>
M
</div>
<div
style={{ color: '#fafafa', fontSize: '28px', fontWeight: 600, letterSpacing: '-0.02em' }}
>
BuildMyMCPServer
</div>
</div>
<div style={{ display: 'flex', flexDirection: 'column' }}>
<div
style={{
color: '#fafafa',
fontSize: '74px',
fontWeight: 700,
lineHeight: 1.05,
letterSpacing: '-0.03em',
}}
>
Describe your tool.
</div>
<div
style={{
color: '#fafafa',
fontSize: '74px',
fontWeight: 700,
lineHeight: 1.05,
letterSpacing: '-0.03em',
}}
>
We host the MCP server.
</div>
<div
style={{
color: '#a1a1aa',
fontSize: '27px',
marginTop: '26px',
letterSpacing: '-0.01em',
}}
>
Prompt to a hosted, OAuth 2.1-protected MCP server in 60 seconds.
</div>
</div>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<div
style={{
display: 'flex',
alignItems: 'center',
gap: '11px',
color: '#71717a',
fontSize: '22px',
}}
>
<div
style={{ width: '9px', height: '9px', borderRadius: '9px', backgroundColor: '#6366f1' }}
/>
OAuth 2.1 · Streamable HTTP · Docker-isolated
</div>
<div style={{ color: '#71717a', fontSize: '22px' }}>buildmymcpserver.com</div>
</div>
</div>,
{ ...size },
);
}

38
apps/web/app/robots.ts Normal file
View File

@@ -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,
};
}

36
apps/web/app/sitemap.ts Normal file
View File

@@ -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,
}));
}

View File

@@ -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;
}