feat(web): SEO — server-rendered template pages + /guides articles

- templates/[slug] converted from client to server component: per-template
  generateMetadata (title/description/canonical/OG) + SoftwareApplication
  JSON-LD; code-audit toggle split into a client island; missing/non-public
  templates now return a real 404.
- sitemap.ts pulls public template slugs live from the API (best-effort) +
  the new /guides routes.
- new /guides section: 3 server-rendered SEO articles (host MCP with OAuth,
  hosted-platforms comparison, MintMCP alternative) with TechArticle JSON-LD;
  Guides link added to the marketing nav.
- lib/seo.ts: articleJsonLd + templateJsonLd builders.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@
This commit is contained in:
Marco Sadjadi
2026-05-31 12:08:05 +02:00
parent 21a5cf5762
commit 1349dc1dc0
11 changed files with 734 additions and 141 deletions

View File

@@ -184,6 +184,58 @@ export function faqJsonLd(items: FaqItem[] = FAQ): object {
};
}
/** TechArticle structured data for /guides/* SEO articles. */
export function articleJsonLd(opts: {
title: string;
description: string;
path: string;
datePublished: string;
dateModified?: string;
}): object {
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}` },
datePublished: opts.datePublished,
dateModified: opts.dateModified ?? opts.datePublished,
inLanguage: 'en',
author: { '@id': `${SITE_URL}/#organization` },
publisher: { '@id': `${SITE_URL}/#organization` },
};
}
/** 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.