From cc61b9e521130b21a447ac10880170ccf7a432d2 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 20 Jan 2026 09:52:25 +0000 Subject: [PATCH 1/3] feat(web): fix critical SEO issues and add AI-friendly infrastructure - Fix canonical URL bug: use './' instead of '/' so each page gets correct canonical - Add sitemap.ts with 11 URLs (core, docs, legal pages) - Add robots.ts with AI bot rules (GPTBot, ClaudeBot, PerplexityBot, etc.) - Enhance metadata with metadataBase, googleBot settings, category - Upgrade JSON-LD to @graph structure with Organization and WebSite entities The canonical fix prevents Google from treating all docs as homepage duplicates. --- apps/web/src/app/layout.tsx | 94 ++++++++++++++++++++++++++++++------- apps/web/src/app/robots.ts | 47 +++++++++++++++++++ apps/web/src/app/sitemap.ts | 44 +++++++++++++++++ 3 files changed, 169 insertions(+), 16 deletions(-) create mode 100644 apps/web/src/app/robots.ts create mode 100644 apps/web/src/app/sitemap.ts diff --git a/apps/web/src/app/layout.tsx b/apps/web/src/app/layout.tsx index 67afbcb..a3da3ff 100644 --- a/apps/web/src/app/layout.tsx +++ b/apps/web/src/app/layout.tsx @@ -22,6 +22,8 @@ const geistMono = localFont({ // SEO Metadata export const metadata: Metadata = { + metadataBase: new URL('https://replimap.com'), + title: { default: "RepliMap - AWS Infrastructure Intelligence", template: "%s | RepliMap", @@ -38,17 +40,25 @@ export const metadata: Metadata = { "DevOps", "SRE", "infrastructure audit", + "AWS to Terraform", + "reverse engineer AWS", + "Terraform import", ], authors: [{ name: "RepliMap" }], creator: "RepliMap", - // Icons from v0 (enhanced) + publisher: "RepliMap", + + // CRITICAL FIX: './' instead of '/' + // './' = relative to current path, each page gets correct canonical + // '/' = absolute path, all pages point to homepage (fatal SEO error!) + alternates: { + canonical: './', + }, + icons: { - icon: [ - { url: "/favicon.ico" }, - { url: "/icon.svg", type: "image/svg+xml" }, - ], - apple: "/apple-icon.png", + icon: [{ url: "/favicon.ico" }], }, + openGraph: { title: "RepliMap - AWS Infrastructure Intelligence", description: "Reverse-engineer your AWS infrastructure into Terraform", @@ -65,31 +75,83 @@ export const metadata: Metadata = { locale: "en_US", type: "website", }, + twitter: { card: "summary_large_image", title: "RepliMap - AWS Infrastructure Intelligence", description: "Reverse-engineer your AWS infrastructure into Terraform", images: ["/og-image.png"], }, + robots: { index: true, follow: true, + googleBot: { + index: true, + follow: true, + 'max-video-preview': -1, + 'max-image-preview': 'large', + 'max-snippet': -1, + }, }, + + category: 'Technology', + applicationName: 'RepliMap', }; // JSON-LD Structured Data for SEO const jsonLd = { "@context": "https://schema.org", - "@type": "SoftwareApplication", - name: "RepliMap", - applicationCategory: "DeveloperApplication", - operatingSystem: "Linux, macOS, Windows", - description: - "AWS Infrastructure Intelligence Engine - Reverse-engineer infrastructure into Terraform, detect drift, generate IAM policies", - offers: [ - { "@type": "Offer", price: "0", priceCurrency: "USD", name: "Free" }, - { "@type": "Offer", price: "49", priceCurrency: "USD", name: "Solo" }, - { "@type": "Offer", price: "99", priceCurrency: "USD", name: "Pro" }, + "@graph": [ + { + "@type": "SoftwareApplication", + "@id": "https://replimap.com/#software", + name: "RepliMap", + applicationCategory: "DeveloperApplication", + applicationSubCategory: "Infrastructure as Code Tool", + operatingSystem: "Linux, macOS, Windows", + description: + "AWS Infrastructure Intelligence Engine - Reverse-engineer infrastructure into Terraform, detect drift, generate IAM policies", + url: "https://replimap.com", + downloadUrl: "https://replimap.com/docs/installation", + softwareVersion: "1.0.0", + releaseNotes: "https://replimap.com/docs/changelog", + screenshot: "https://replimap.com/og-image.png", + featureList: [ + "Reverse-engineer AWS to Terraform", + "Infrastructure drift detection", + "Least-privilege IAM policy generation", + "Multi-region scanning", + "Offline/air-gapped support", + ], + offers: [ + { "@type": "Offer", price: "0", priceCurrency: "USD", name: "Community" }, + { "@type": "Offer", price: "49", priceCurrency: "USD", name: "Solo" }, + { "@type": "Offer", price: "99", priceCurrency: "USD", name: "Pro" }, + ], + author: { "@id": "https://replimap.com/#organization" }, + }, + { + "@type": "Organization", + "@id": "https://replimap.com/#organization", + name: "RepliMap", + url: "https://replimap.com", + logo: { "@type": "ImageObject", url: "https://replimap.com/og-image.png" }, + sameAs: ["https://github.com/RepliMap/replimap-mono"], + contactPoint: { + "@type": "ContactPoint", + email: "hello@replimap.com", + contactType: "customer support", + }, + }, + { + "@type": "WebSite", + "@id": "https://replimap.com/#website", + url: "https://replimap.com", + name: "RepliMap", + description: "AWS Infrastructure Intelligence", + publisher: { "@id": "https://replimap.com/#organization" }, + }, ], }; diff --git a/apps/web/src/app/robots.ts b/apps/web/src/app/robots.ts new file mode 100644 index 0000000..5671f9a --- /dev/null +++ b/apps/web/src/app/robots.ts @@ -0,0 +1,47 @@ +import { MetadataRoute } from 'next' + +export default function robots(): MetadataRoute.Robots { + const baseUrl = 'https://replimap.com' + + return { + rules: [ + { + userAgent: '*', + allow: '/', + disallow: ['/dashboard/', '/api/', '/sign-in/', '/sign-up/', '/_next/'], + }, + // Explicitly welcome AI crawlers + { + userAgent: 'GPTBot', + allow: '/', + disallow: ['/dashboard/', '/api/', '/sign-in/', '/sign-up/'], + }, + { + userAgent: 'PerplexityBot', + allow: '/', + disallow: ['/dashboard/', '/api/', '/sign-in/', '/sign-up/'], + }, + { + userAgent: 'ClaudeBot', + allow: '/', + disallow: ['/dashboard/', '/api/', '/sign-in/', '/sign-up/'], + }, + { + userAgent: 'Google-Extended', + allow: '/', + disallow: ['/dashboard/', '/api/', '/sign-in/', '/sign-up/'], + }, + { + userAgent: 'Amazonbot', + allow: '/', + disallow: ['/dashboard/', '/api/', '/sign-in/', '/sign-up/'], + }, + // Block low-value crawlers + { userAgent: 'CCBot', disallow: '/' }, + { userAgent: 'AhrefsBot', disallow: '/' }, + { userAgent: 'SemrushBot', disallow: '/' }, + ], + sitemap: `${baseUrl}/sitemap.xml`, + host: baseUrl, + } +} diff --git a/apps/web/src/app/sitemap.ts b/apps/web/src/app/sitemap.ts new file mode 100644 index 0000000..e551830 --- /dev/null +++ b/apps/web/src/app/sitemap.ts @@ -0,0 +1,44 @@ +import { MetadataRoute } from 'next' + +export default function sitemap(): MetadataRoute.Sitemap { + const baseUrl = 'https://replimap.com' + const now = new Date() + + // Core marketing pages + const coreRoutes = ['', '/docs'] + + // Documentation pages (matches content/docs/*.mdx) + const docRoutes = [ + '/docs/quick-start', + '/docs/installation', + '/docs/cli-reference', + '/docs/iam-policy', + '/docs/security', + '/docs/changelog', + '/docs/contributing', + ] + + // Legal pages (privacy/page.tsx and terms/page.tsx exist) + const legalRoutes = ['/privacy', '/terms'] + + return [ + ...coreRoutes.map((route) => ({ + url: `${baseUrl}${route}`, + lastModified: now, + changeFrequency: 'weekly' as const, + priority: route === '' ? 1.0 : 0.9, + })), + ...docRoutes.map((route) => ({ + url: `${baseUrl}${route}`, + lastModified: now, + changeFrequency: 'monthly' as const, + priority: route.includes('quick-start') ? 0.9 : 0.7, + })), + ...legalRoutes.map((route) => ({ + url: `${baseUrl}${route}`, + lastModified: new Date('2026-01-01'), + changeFrequency: 'yearly' as const, + priority: 0.3, + })), + ] +} From 2afbe41ad9c9ba38df14b505fc3c52ee78f49278 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 20 Jan 2026 10:28:48 +0000 Subject: [PATCH 2/3] feat(web): implement SEO improvements with SSOT pricing and dynamic OG images - Add lib/schema.ts: generate JSON-LD from pricing.ts (SSOT) - Fixes wrong prices: $0/$29/$99 (not $0/$49/$99 for obsolete Solo plan) - Update layout.tsx: use generateSiteSchema() for dynamic JSON-LD - Add opengraph-image.tsx: dynamic OG image for homepage - Add docs/opengraph-image.tsx: dynamic OG image per doc page with title/description - Update docs/page.tsx: add openGraph metadata with proper URLs - Replace sitemap.ts: dynamically read from content/docs/ directory Prices now correctly reflect: Community ($0), Pro ($29), Team ($99) --- .../app/docs/[[...slug]]/opengraph-image.tsx | 79 +++++++++++++++++++ apps/web/src/app/docs/[[...slug]]/page.tsx | 8 ++ apps/web/src/app/layout.tsx | 59 +------------- apps/web/src/app/opengraph-image.tsx | 62 +++++++++++++++ apps/web/src/app/sitemap.ts | 72 +++++++++-------- apps/web/src/lib/schema.ts | 71 +++++++++++++++++ 6 files changed, 264 insertions(+), 87 deletions(-) create mode 100644 apps/web/src/app/docs/[[...slug]]/opengraph-image.tsx create mode 100644 apps/web/src/app/opengraph-image.tsx create mode 100644 apps/web/src/lib/schema.ts diff --git a/apps/web/src/app/docs/[[...slug]]/opengraph-image.tsx b/apps/web/src/app/docs/[[...slug]]/opengraph-image.tsx new file mode 100644 index 0000000..4f2384b --- /dev/null +++ b/apps/web/src/app/docs/[[...slug]]/opengraph-image.tsx @@ -0,0 +1,79 @@ +import { ImageResponse } from 'next/og' +import { source } from '@/lib/source' + +export const runtime = 'edge' +export const alt = 'RepliMap Documentation' +export const size = { width: 1200, height: 630 } +export const contentType = 'image/png' + +interface PageData { + title?: string + description?: string +} + +export default async function Image({ params }: { params: Promise<{ slug?: string[] }> }) { + const resolvedParams = await params + const page = source.getPage(resolvedParams.slug) + const data = page?.data as PageData | undefined + const title = data?.title || 'Documentation' + const description = data?.description || 'AWS Infrastructure Intelligence' + + return new ImageResponse( + ( +
+
+
+ + RepliMap Docs + +
+
+ {title} +
+
+ {description} +
+
+
+ ), + { ...size } + ) +} diff --git a/apps/web/src/app/docs/[[...slug]]/page.tsx b/apps/web/src/app/docs/[[...slug]]/page.tsx index 56b82c4..160797a 100644 --- a/apps/web/src/app/docs/[[...slug]]/page.tsx +++ b/apps/web/src/app/docs/[[...slug]]/page.tsx @@ -51,9 +51,17 @@ export async function generateMetadata(props: { if (!page) notFound() const data = page.data as unknown as ExtendedPageData + const slug = params.slug?.join('/') || '' return { title: `${data.title} | RepliMap Docs`, description: data.description, + openGraph: { + title: data.title, + description: data.description, + url: `https://replimap.com/docs/${slug}`, + siteName: 'RepliMap', + type: 'article', + }, } } diff --git a/apps/web/src/app/layout.tsx b/apps/web/src/app/layout.tsx index a3da3ff..79c532f 100644 --- a/apps/web/src/app/layout.tsx +++ b/apps/web/src/app/layout.tsx @@ -2,6 +2,7 @@ import type { Metadata } from "next"; import localFont from "next/font/local"; import { ClerkProviderWrapper } from "@/components/clerk-provider"; import { ThemeProvider } from "@/components/theme-provider"; +import { generateSiteSchema } from "@/lib/schema"; import "./globals.css"; const geistSans = localFont({ @@ -99,67 +100,13 @@ export const metadata: Metadata = { applicationName: 'RepliMap', }; -// JSON-LD Structured Data for SEO -const jsonLd = { - "@context": "https://schema.org", - "@graph": [ - { - "@type": "SoftwareApplication", - "@id": "https://replimap.com/#software", - name: "RepliMap", - applicationCategory: "DeveloperApplication", - applicationSubCategory: "Infrastructure as Code Tool", - operatingSystem: "Linux, macOS, Windows", - description: - "AWS Infrastructure Intelligence Engine - Reverse-engineer infrastructure into Terraform, detect drift, generate IAM policies", - url: "https://replimap.com", - downloadUrl: "https://replimap.com/docs/installation", - softwareVersion: "1.0.0", - releaseNotes: "https://replimap.com/docs/changelog", - screenshot: "https://replimap.com/og-image.png", - featureList: [ - "Reverse-engineer AWS to Terraform", - "Infrastructure drift detection", - "Least-privilege IAM policy generation", - "Multi-region scanning", - "Offline/air-gapped support", - ], - offers: [ - { "@type": "Offer", price: "0", priceCurrency: "USD", name: "Community" }, - { "@type": "Offer", price: "49", priceCurrency: "USD", name: "Solo" }, - { "@type": "Offer", price: "99", priceCurrency: "USD", name: "Pro" }, - ], - author: { "@id": "https://replimap.com/#organization" }, - }, - { - "@type": "Organization", - "@id": "https://replimap.com/#organization", - name: "RepliMap", - url: "https://replimap.com", - logo: { "@type": "ImageObject", url: "https://replimap.com/og-image.png" }, - sameAs: ["https://github.com/RepliMap/replimap-mono"], - contactPoint: { - "@type": "ContactPoint", - email: "hello@replimap.com", - contactType: "customer support", - }, - }, - { - "@type": "WebSite", - "@id": "https://replimap.com/#website", - url: "https://replimap.com", - name: "RepliMap", - description: "AWS Infrastructure Intelligence", - publisher: { "@id": "https://replimap.com/#organization" }, - }, - ], -}; - export default function RootLayout({ children, }: Readonly<{ children: React.ReactNode; }>) { + const jsonLd = generateSiteSchema(); + return ( diff --git a/apps/web/src/app/opengraph-image.tsx b/apps/web/src/app/opengraph-image.tsx new file mode 100644 index 0000000..0a69cde --- /dev/null +++ b/apps/web/src/app/opengraph-image.tsx @@ -0,0 +1,62 @@ +import { ImageResponse } from 'next/og' + +export const runtime = 'edge' +export const alt = 'RepliMap - AWS Infrastructure Intelligence' +export const size = { width: 1200, height: 630 } +export const contentType = 'image/png' + +export default async function Image() { + return new ImageResponse( + ( +
+
+ R +
+
+ RepliMap +
+
+ AWS Infrastructure Intelligence +
+
+ Reverse-engineer AWS into Terraform • Detect Drift • Generate IAM Policies +
+
+
+ ), + { ...size } + ) +} diff --git a/apps/web/src/app/sitemap.ts b/apps/web/src/app/sitemap.ts index e551830..b22541a 100644 --- a/apps/web/src/app/sitemap.ts +++ b/apps/web/src/app/sitemap.ts @@ -1,44 +1,54 @@ import { MetadataRoute } from 'next' +import fs from 'fs' +import path from 'path' + +function getDocSlugs(): string[] { + const docsDir = path.join(process.cwd(), 'content/docs') + try { + return fs + .readdirSync(docsDir) + .filter((file) => file.endsWith('.mdx')) + .map((file) => file.replace('.mdx', '')) + .filter((slug) => slug !== 'index') + } catch { + return [] + } +} export default function sitemap(): MetadataRoute.Sitemap { const baseUrl = 'https://replimap.com' const now = new Date() - // Core marketing pages - const coreRoutes = ['', '/docs'] - - // Documentation pages (matches content/docs/*.mdx) - const docRoutes = [ - '/docs/quick-start', - '/docs/installation', - '/docs/cli-reference', - '/docs/iam-policy', - '/docs/security', - '/docs/changelog', - '/docs/contributing', + // Core pages + const coreRoutes: MetadataRoute.Sitemap = [ + { url: baseUrl, lastModified: now, changeFrequency: 'weekly', priority: 1.0 }, + { url: `${baseUrl}/docs`, lastModified: now, changeFrequency: 'weekly', priority: 0.9 }, ] - // Legal pages (privacy/page.tsx and terms/page.tsx exist) - const legalRoutes = ['/privacy', '/terms'] + // Dynamic documentation pages + const docSlugs = getDocSlugs() + const docRoutes: MetadataRoute.Sitemap = docSlugs.map((slug) => ({ + url: `${baseUrl}/docs/${slug}`, + lastModified: now, + changeFrequency: 'monthly' as const, + priority: slug === 'quick-start' ? 0.9 : 0.7, + })) - return [ - ...coreRoutes.map((route) => ({ - url: `${baseUrl}${route}`, - lastModified: now, - changeFrequency: 'weekly' as const, - priority: route === '' ? 1.0 : 0.9, - })), - ...docRoutes.map((route) => ({ - url: `${baseUrl}${route}`, - lastModified: now, - changeFrequency: 'monthly' as const, - priority: route.includes('quick-start') ? 0.9 : 0.7, - })), - ...legalRoutes.map((route) => ({ - url: `${baseUrl}${route}`, + // Legal pages + const legalRoutes: MetadataRoute.Sitemap = [ + { + url: `${baseUrl}/privacy`, lastModified: new Date('2026-01-01'), - changeFrequency: 'yearly' as const, + changeFrequency: 'yearly', priority: 0.3, - })), + }, + { + url: `${baseUrl}/terms`, + lastModified: new Date('2026-01-01'), + changeFrequency: 'yearly', + priority: 0.3, + }, ] + + return [...coreRoutes, ...docRoutes, ...legalRoutes] } diff --git a/apps/web/src/lib/schema.ts b/apps/web/src/lib/schema.ts new file mode 100644 index 0000000..28fc359 --- /dev/null +++ b/apps/web/src/lib/schema.ts @@ -0,0 +1,71 @@ +import { PLANS, type PlanName } from './pricing' + +/** + * Generate JSON-LD structured data for SEO + * Single Source of Truth: prices from PLANS config + */ +export function generateSiteSchema() { + const displayPlans: PlanName[] = ['community', 'pro', 'team'] + + const offers = displayPlans.map((planKey) => { + const plan = PLANS[planKey] + return { + '@type': 'Offer', + name: plan.name, + price: String(plan.price.monthly), + priceCurrency: 'USD', + description: plan.tagline, + } + }) + + return { + '@context': 'https://schema.org', + '@graph': [ + { + '@type': 'SoftwareApplication', + '@id': 'https://replimap.com/#software', + name: 'RepliMap', + applicationCategory: 'DeveloperApplication', + applicationSubCategory: 'Infrastructure as Code Tool', + operatingSystem: 'Linux, macOS, Windows', + description: + 'AWS Infrastructure Intelligence Engine - Reverse-engineer infrastructure into Terraform, detect drift, generate IAM policies', + url: 'https://replimap.com', + downloadUrl: 'https://replimap.com/docs/installation', + softwareVersion: '1.0.0', + releaseNotes: 'https://replimap.com/docs/changelog', + screenshot: 'https://replimap.com/og-image.png', + featureList: [ + 'Reverse-engineer AWS to Terraform', + 'Infrastructure drift detection', + 'Least-privilege IAM policy generation', + 'Multi-region scanning', + 'Offline/air-gapped support', + ], + offers, + author: { '@id': 'https://replimap.com/#organization' }, + }, + { + '@type': 'Organization', + '@id': 'https://replimap.com/#organization', + name: 'RepliMap', + url: 'https://replimap.com', + logo: { '@type': 'ImageObject', url: 'https://replimap.com/og-image.png' }, + sameAs: ['https://github.com/RepliMap/replimap-mono'], + contactPoint: { + '@type': 'ContactPoint', + email: 'hello@replimap.com', + contactType: 'customer support', + }, + }, + { + '@type': 'WebSite', + '@id': 'https://replimap.com/#website', + url: 'https://replimap.com', + name: 'RepliMap', + description: 'AWS Infrastructure Intelligence', + publisher: { '@id': 'https://replimap.com/#organization' }, + }, + ], + } +} From 033dab901ddfd8a6b6be1461149e5054e47f4fcc Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 20 Jan 2026 10:31:13 +0000 Subject: [PATCH 3/3] fix(web): remove docs opengraph-image incompatible with catch-all route Next.js doesn't allow static file conventions (opengraph-image.tsx) inside catch-all route segments ([[...slug]]). Docs pages will use the default OG image from layout.tsx metadata. --- .../app/docs/[[...slug]]/opengraph-image.tsx | 79 ------------------- 1 file changed, 79 deletions(-) delete mode 100644 apps/web/src/app/docs/[[...slug]]/opengraph-image.tsx diff --git a/apps/web/src/app/docs/[[...slug]]/opengraph-image.tsx b/apps/web/src/app/docs/[[...slug]]/opengraph-image.tsx deleted file mode 100644 index 4f2384b..0000000 --- a/apps/web/src/app/docs/[[...slug]]/opengraph-image.tsx +++ /dev/null @@ -1,79 +0,0 @@ -import { ImageResponse } from 'next/og' -import { source } from '@/lib/source' - -export const runtime = 'edge' -export const alt = 'RepliMap Documentation' -export const size = { width: 1200, height: 630 } -export const contentType = 'image/png' - -interface PageData { - title?: string - description?: string -} - -export default async function Image({ params }: { params: Promise<{ slug?: string[] }> }) { - const resolvedParams = await params - const page = source.getPage(resolvedParams.slug) - const data = page?.data as PageData | undefined - const title = data?.title || 'Documentation' - const description = data?.description || 'AWS Infrastructure Intelligence' - - return new ImageResponse( - ( -
-
-
- - RepliMap Docs - -
-
- {title} -
-
- {description} -
-
-
- ), - { ...size } - ) -}