From b9abb7ea6d3e914df3f22aa6d53ef4c1d97a12ac Mon Sep 17 00:00:00 2001 From: David Pereira Date: Thu, 4 Jun 2026 16:55:13 +0200 Subject: [PATCH] feat(docs): add sitemap, robots.txt, homepage metadata and canonical URLs SEO Phase 1 critical improvements for @deessejs/errors documentation site: - Add sitemap.ts with image sitemap (all doc pages + OG images) - Add robots.ts (allow all, disallow /api/search and /llms.mdx) - Add homepage metadata: description, OpenGraph, Twitter Card - Add canonical URLs to all doc pages via generateMetadata - Fix OG image route slug handling (defensive slice, removes 'image.png' safely) - Add homepage OG image route at /og/home - Add siteUrl to shared.ts as single source of truth - Add sitemap discovery link to root layout Co-Authored-By: Claude Opus 4.6 --- apps/web/src/app/(home)/page.tsx | 43 ++++++++++++++++++++ apps/web/src/app/docs/[[...slug]]/page.tsx | 5 ++- apps/web/src/app/layout.tsx | 3 ++ apps/web/src/app/og/docs/[...slug]/route.tsx | 12 +++++- apps/web/src/app/og/home/route.tsx | 19 +++++++++ apps/web/src/app/robots.ts | 14 +++++++ apps/web/src/app/sitemap.ts | 27 ++++++++++++ apps/web/src/lib/shared.ts | 1 + 8 files changed, 121 insertions(+), 3 deletions(-) create mode 100644 apps/web/src/app/og/home/route.tsx 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/(home)/page.tsx b/apps/web/src/app/(home)/page.tsx index c5f003d..318a443 100644 --- a/apps/web/src/app/(home)/page.tsx +++ b/apps/web/src/app/(home)/page.tsx @@ -1,8 +1,51 @@ import Link from 'next/link'; +import type { Metadata } from 'next'; import { CodeBlock } from '@/components/code-block'; import { CtaCard } from '@/components/cta-card'; import { Footer } from '@/components/footer'; +export const metadata: Metadata = { + title: '@deessejs/errors — Error Handling, Reimagined', + description: + '@deessejs/errors is a TypeScript library bringing Python-inspired error handling to JavaScript. Exception chaining, hierarchical inheritance, message templates, and rich error semantics through a function-based API.', + keywords: [ + 'typescript error handling', + 'exception chaining typescript', + 'python-style errors javascript', + 'error factory typescript', + 'structured errors typescript', + 'hierarchical error inheritance', + ], + openGraph: { + type: 'website', + locale: 'en_US', + url: 'https://errors.deessejs.com', + siteName: '@deessejs/errors', + title: '@deessejs/errors — Error Handling, Reimagined', + description: + 'A TypeScript library bringing Python-inspired error handling to JavaScript. Exception chaining, hierarchical inheritance, and rich error semantics.', + images: [ + { + url: 'https://errors.deessejs.com/og/home.png', + width: 1200, + height: 630, + alt: '@deessejs/errors — TypeScript Error Handling Library', + }, + ], + }, + twitter: { + card: 'summary_large_image', + title: '@deessejs/errors — Error Handling, Reimagined', + description: + 'A TypeScript library bringing Python-inspired error handling to JavaScript.', + images: ['https://errors.deessejs.com/og/home.png'], + creator: '@nesalia_inc', + }, + alternates: { + canonical: 'https://errors.deessejs.com', + }, +}; + // Floating squares data for blueprint aesthetic const floatingSquares = [ { x: 300, y: 120, opacity: 1.0, delay: 0.7 }, diff --git a/apps/web/src/app/docs/[[...slug]]/page.tsx b/apps/web/src/app/docs/[[...slug]]/page.tsx index 53ce219..32d40ab 100644 --- a/apps/web/src/app/docs/[[...slug]]/page.tsx +++ b/apps/web/src/app/docs/[[...slug]]/page.tsx @@ -11,7 +11,7 @@ import { notFound } from 'next/navigation'; import { getMDXComponents } from '@/components/mdx'; import type { Metadata } from 'next'; import { createRelativeLink } from 'fumadocs-ui/mdx'; -import { gitConfig } from '@/lib/shared'; +import { gitConfig, siteUrl } from '@/lib/shared'; export default async function Page(props: PageProps<'/docs/[[...slug]]'>) { const params = await props.params; @@ -56,6 +56,9 @@ export async function generateMetadata(props: PageProps<'/docs/[[...slug]]'>): P return { title: page.data.title, description: page.data.description, + alternates: { + canonical: `${siteUrl}${page.url}`, + }, openGraph: { images: getPageImage(page).url, }, diff --git a/apps/web/src/app/layout.tsx b/apps/web/src/app/layout.tsx index 42a0b4f..3330cd2 100644 --- a/apps/web/src/app/layout.tsx +++ b/apps/web/src/app/layout.tsx @@ -11,6 +11,9 @@ const inter = Inter({ export default function Layout({ children }: LayoutProps<'/'>) { return ( + + + {children} diff --git a/apps/web/src/app/og/docs/[...slug]/route.tsx b/apps/web/src/app/og/docs/[...slug]/route.tsx index 877166d..518a98c 100644 --- a/apps/web/src/app/og/docs/[...slug]/route.tsx +++ b/apps/web/src/app/og/docs/[...slug]/route.tsx @@ -8,7 +8,16 @@ export const revalidate = false; export async function GET(_req: Request, { params }: RouteContext<'/og/docs/[...slug]'>) { const { slug } = await params; - const page = source.getPage(slug.slice(0, -1)); + + // getPageImage() appends 'image.png' to the slug array. + // Strip it to get back the real page slugs. + // Defensive: only strip if the last segment is exactly 'image.png'. + const cleanSlug = + slug.length > 1 && slug[slug.length - 1] === 'image.png' + ? slug.slice(0, -1) + : slug; + + const page = source.getPage(cleanSlug); if (!page) notFound(); return new ImageResponse( @@ -22,7 +31,6 @@ export async function GET(_req: Request, { params }: RouteContext<'/og/docs/[... export function generateStaticParams() { return source.getPages().map((page) => ({ - lang: page.locale, slug: getPageImage(page).segments, })); } diff --git a/apps/web/src/app/og/home/route.tsx b/apps/web/src/app/og/home/route.tsx new file mode 100644 index 0000000..ff870cd --- /dev/null +++ b/apps/web/src/app/og/home/route.tsx @@ -0,0 +1,19 @@ +import { ImageResponse } from 'next/og'; +import { generate as DefaultImage } from 'fumadocs-ui/og'; +import { appName } from '@/lib/shared'; + +export const revalidate = false; + +export async function GET() { + return new ImageResponse( + , + { + width: 1200, + height: 630, + }, + ); +} diff --git a/apps/web/src/app/robots.ts b/apps/web/src/app/robots.ts new file mode 100644 index 0000000..ca31f53 --- /dev/null +++ b/apps/web/src/app/robots.ts @@ -0,0 +1,14 @@ +import type { MetadataRoute } from 'next'; + +export default function robots(): MetadataRoute.Robots { + return { + rules: [ + { + userAgent: '*', + allow: '/', + disallow: ['/api/search', '/llms.mdx'], + }, + ], + sitemap: 'https://errors.deessejs.com/sitemap.xml', + }; +} diff --git a/apps/web/src/app/sitemap.ts b/apps/web/src/app/sitemap.ts new file mode 100644 index 0000000..f0ea468 --- /dev/null +++ b/apps/web/src/app/sitemap.ts @@ -0,0 +1,27 @@ +import type { MetadataRoute } from 'next'; +import { source } from '@/lib/source'; +import { siteUrl } from '@/lib/shared'; +import { getPageImage } from '@/lib/source'; + +export default function sitemap(): MetadataRoute.Sitemap { + const pages = source.getPages(); + + const docPages = pages.map((page) => ({ + url: `${siteUrl}${page.url}`, + lastModified: new Date(), + changeFrequency: 'weekly' as const, + priority: page.url === '/docs' ? 1.0 : 0.8, + images: [`${siteUrl}${getPageImage(page).url}`], + })); + + const staticPages: MetadataRoute.Sitemap = [ + { + url: siteUrl, + lastModified: new Date(), + changeFrequency: 'monthly', + priority: 0.9, + }, + ]; + + return [...staticPages, ...docPages]; +} diff --git a/apps/web/src/lib/shared.ts b/apps/web/src/lib/shared.ts index 077db91..c562b06 100644 --- a/apps/web/src/lib/shared.ts +++ b/apps/web/src/lib/shared.ts @@ -2,6 +2,7 @@ export const appName = 'DeesseJS Errors'; export const docsRoute = '/docs'; export const docsImageRoute = '/og/docs'; export const docsContentRoute = '/llms.mdx/docs'; +export const siteUrl = 'https://errors.deessejs.com'; export const gitConfig = { user: 'nesalia-inc',