buildmymcpserver/apps/web/lib/seo.ts
Marco Sadjadi a08f5f05b1 feat(content): 11 new SEO/GEO guide articles with per-article OG images
How-to cluster: create-mcp-server-without-code, claude-desktop-mcp-setup,
chatgpt-mcp-connector (incl. honest Business/Enterprise write-connector plan
limits), rest-api-to-mcp-server.
Comparison cluster: mcp-server-hosting-pricing (5-platform matrix),
composio-alternative, smithery-alternative — house style: explicitly state
where competitors win.
Technical/GEO cluster: mcp-transports-explained (SSE deprecation 2025-03-26),
mcp-oauth-plain-english, mcp-server-security-checklist, and German DACH
article mcp-server-ohne-code-erstellen (articleJsonLd gained optional
inLanguage param).

Every article: pageMetadata canonical, Article JSON-LD with Person author +
image + wordCount, BreadcrumbList, FAQPage where applicable, 1200x630 OG
image via shared helper, internal linking. Registry entries in lib/articles.ts
feed guides index, sitemap and RSS automatically. Product claims sourced only
from lib/seo.ts truth — no invented customers, SLAs or certifications.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SXUwmPVRTD8AKQtio6gCN5
2026-07-08 23:04:19 +02:00

289 lines
10 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Central SEO source of truth — site constants, FAQ data, JSON-LD builders.
// The FAQ array here is rendered on the landing page AND emitted as FAQPage
// structured data, so the two never drift (Google requires them to match).
import type { Metadata } from 'next';
export const SITE_URL = process.env.NEXT_PUBLIC_APP_URL ?? 'https://buildmymcpserver.com';
export const SITE_NAME = 'BuildMyMCPServer';
export const SITE_TAGLINE = 'Describe your tool. We host the MCP server.';
export const SITE_DESCRIPTION =
'From a natural-language prompt to a hosted, OAuth 2.1-protected MCP server in 60 seconds. Streamable HTTP, ready for Claude, Cursor and ChatGPT.';
export const SEO_KEYWORDS = [
'MCP server',
'Model Context Protocol',
'hosted MCP server',
'MCP server hosting',
'MCP server builder',
'create MCP server',
'deploy MCP server',
'MCP server generator',
'OAuth MCP server',
'Streamable HTTP MCP',
'Claude MCP server',
'Cursor MCP',
'ChatGPT connector',
'MCP template marketplace',
];
export interface FaqItem {
q: string;
a: string;
}
// Rendered on the landing page and emitted as FAQPage JSON-LD — keep in sync
// by virtue of being a single export.
export const FAQ: FaqItem[] = [
{
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 — each server runs in a container that stays warm (auto-restarts on failure), so there is no spin-up delay on a tool call.',
},
{
q: 'Rate limits?',
a: 'Every request is gated by OAuth 2.1 before it reaches your container. Configurable per-server rate limiting is on the roadmap.',
},
{
q: 'How fast is generation?',
a: 'Spec to image to live URL typically completes in 45-90 seconds.',
},
{
q: 'Logs and metrics?',
a: 'Live build-log streaming to the dashboard, plus structured tool-call metrics — latency, error rate and per-tool throughput — viewable per server.',
},
{
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: 'On the roadmap. Today every server is reachable at a buildmymcpserver.com URL with TLS; bring-your-own-domain support is coming.',
},
];
const SOFTWARE_FEATURES = [
'Prompt to a deployed MCP server in under 60 seconds',
'OAuth 2.1 authorization server — PKCE, Dynamic Client Registration (RFC 7591), Resource Indicators (RFC 8707)',
'Streamable HTTP transport, compatible with Claude Desktop, Cursor, ChatGPT, VS Code Copilot and Continue.dev',
'Every generated server runs in an isolated Docker container',
'Customer secrets encrypted with AES-256-GCM, injected only at runtime',
'Live build-log streaming to the dashboard',
'Template marketplace — publish your server or fork someone elses',
'Full TypeScript source export, no vendor lock-in',
'Self-hostable control plane with BYO Postgres and Redis',
];
// Enterprise is deliberately absent: its price is "Custom" and schema.org
// Offers with fabricated numeric prices are exactly what commit ee4713f purged.
const OFFERS = [
{
name: 'Hobby',
price: '0',
description: '1 server, 100k tool calls/month, BMM subdomain, community support.',
},
{
name: 'Pro',
price: '49',
description:
'5 servers, 1M tool calls/month, faster Claude analysis, priority build queue, email support.',
},
{
name: 'Team',
price: '199',
description: '25 servers, 10M tool calls/month, audit log, shared Slack support.',
},
];
/** Organization + WebSite + SoftwareApplication graph — emitted sitewide. */
export function siteJsonLd(): object {
return {
'@context': 'https://schema.org',
'@graph': [
{
'@type': 'Organization',
'@id': `${SITE_URL}/#organization`,
name: SITE_NAME,
url: SITE_URL,
logo: { '@type': 'ImageObject', url: `${SITE_URL}/icon.svg` },
foundingDate: '2026',
foundingLocation: {
'@type': 'Place',
address: { '@type': 'PostalAddress', addressCountry: 'CH' },
},
},
{
'@type': 'WebSite',
'@id': `${SITE_URL}/#website`,
url: SITE_URL,
name: SITE_NAME,
publisher: { '@id': `${SITE_URL}/#organization` },
description: SITE_DESCRIPTION,
inLanguage: 'en',
},
{
'@type': 'SoftwareApplication',
'@id': `${SITE_URL}/#software`,
name: SITE_NAME,
applicationCategory: 'DeveloperApplication',
operatingSystem: 'Web Browser',
url: SITE_URL,
inLanguage: 'en',
description:
'BuildMyMCPServer turns a natural-language prompt into a hosted, OAuth 2.1-protected Model Context Protocol (MCP) server. Describe the tools you need; the platform generates a TypeScript server, runs static checks, builds a Docker image and deploys it to a public Streamable HTTP endpoint that Claude, Cursor and ChatGPT can connect to. A template marketplace lets users publish and fork ready-made servers.',
featureList: SOFTWARE_FEATURES,
offers: OFFERS.map((o) => ({
'@type': 'Offer',
name: o.name,
price: o.price,
priceCurrency: 'EUR',
description: o.description,
})),
},
],
};
}
/** FAQPage structured data — emitted on the page that displays the FAQ. */
export function faqJsonLd(items: FaqItem[] = FAQ): object {
return {
'@context': 'https://schema.org',
'@type': 'FAQPage',
mainEntity: items.map((item) => ({
'@type': 'Question',
name: item.q,
acceptedAnswer: { '@type': 'Answer', text: item.a },
})),
};
}
/** TechArticle structured data for /guides/* SEO articles. */
export function articleJsonLd(opts: {
title: string;
description: string;
path: string;
datePublished: string;
dateModified?: string;
/**
* Absolute or site-relative image URL. Defaults to the route's generated
* opengraph-image (1200×630), which satisfies Google Discover's large-image
* requirement once the route ships an opengraph-image.tsx.
*/
image?: string;
/** Named human author. Falls back to the Organization. */
authorName?: string;
wordCount?: number;
/** BCP-47 language tag; the guides corpus is English except where noted. */
inLanguage?: string;
}): object {
const image = opts.image ?? `${SITE_URL}${opts.path}/opengraph-image`;
return {
'@context': 'https://schema.org',
'@type': 'TechArticle',
headline: opts.title,
description: opts.description,
url: `${SITE_URL}${opts.path}`,
mainEntityOfPage: { '@type': 'WebPage', '@id': `${SITE_URL}${opts.path}` },
image: image.startsWith('http') ? image : `${SITE_URL}${image}`,
datePublished: opts.datePublished,
dateModified: opts.dateModified ?? opts.datePublished,
inLanguage: opts.inLanguage ?? 'en',
author: opts.authorName
? { '@type': 'Person', name: opts.authorName }
: { '@id': `${SITE_URL}/#organization` },
publisher: { '@id': `${SITE_URL}/#organization` },
...(opts.wordCount ? { wordCount: opts.wordCount } : {}),
};
}
/** BreadcrumbList structured data. Pass the trail from root to current page. */
export function breadcrumbJsonLd(trail: { name: string; path: string }[]): object {
return {
'@context': 'https://schema.org',
'@type': 'BreadcrumbList',
itemListElement: trail.map((crumb, i) => ({
'@type': 'ListItem',
position: i + 1,
name: crumb.name,
item: `${SITE_URL}${crumb.path}`,
})),
};
}
/** SoftwareApplication structured data for a published marketplace template. */
export function templateJsonLd(opts: {
slug: string;
title: string;
description: string;
category: string;
tools: string[];
author: string | null;
}): object {
return {
'@context': 'https://schema.org',
'@type': 'SoftwareApplication',
'@id': `${SITE_URL}/templates/${opts.slug}#software`,
name: opts.title,
description: opts.description,
url: `${SITE_URL}/templates/${opts.slug}`,
applicationCategory: 'DeveloperApplication',
applicationSubCategory: 'MCP server',
operatingSystem: 'Web Browser',
inLanguage: 'en',
keywords: ['MCP server', 'Model Context Protocol', opts.category],
featureList: opts.tools,
isAccessibleForFree: true,
offers: { '@type': 'Offer', price: '0', priceCurrency: 'EUR' },
...(opts.author ? { author: { '@type': 'Person', name: opts.author } } : {}),
publisher: { '@id': `${SITE_URL}/#organization` },
};
}
/**
* Per-page metadata. `title` is a bare string so the root layout's
* "%s | BuildMyMCPServer" template appends the brand exactly once.
*/
export function pageMetadata(opts: { title: string; description: string; path: string }): Metadata {
const fullTitle = `${opts.title} | ${SITE_NAME}`;
return {
title: opts.title,
description: opts.description,
alternates: { canonical: opts.path },
openGraph: {
type: 'website',
siteName: SITE_NAME,
title: fullTitle,
description: opts.description,
url: `${SITE_URL}${opts.path}`,
},
twitter: {
card: 'summary_large_image',
title: fullTitle,
description: opts.description,
},
};
}