From 83e0b969065faef7099eb68f5b5bf17c8ec359a4 Mon Sep 17 00:00:00 2001 From: OpenClaw Date: Fri, 22 May 2026 05:59:35 +0000 Subject: [PATCH] chore(resilience): error boundaries, not-found, loading, health, sitemap/robots, richer metadata --- app/api/health/route.ts | 63 +++++++++++++++++++++++++++++++++++++++ app/dashboard/error.tsx | 46 ++++++++++++++++++++++++++++ app/dashboard/loading.tsx | 8 +++++ app/error.tsx | 50 +++++++++++++++++++++++++++++++ app/global-error.tsx | 47 +++++++++++++++++++++++++++++ app/layout.tsx | 40 +++++++++++++++++++++++-- app/loading.tsx | 10 +++++++ app/not-found.tsx | 29 ++++++++++++++++++ app/robots.ts | 23 ++++++++++++++ app/sitemap.ts | 17 +++++++++++ 10 files changed, 330 insertions(+), 3 deletions(-) create mode 100644 app/api/health/route.ts create mode 100644 app/dashboard/error.tsx create mode 100644 app/dashboard/loading.tsx create mode 100644 app/error.tsx create mode 100644 app/global-error.tsx create mode 100644 app/loading.tsx create mode 100644 app/not-found.tsx create mode 100644 app/robots.ts create mode 100644 app/sitemap.ts diff --git a/app/api/health/route.ts b/app/api/health/route.ts new file mode 100644 index 0000000..606a4fc --- /dev/null +++ b/app/api/health/route.ts @@ -0,0 +1,63 @@ +import { NextResponse } from 'next/server' +import { getDb } from '@/lib/db' + +export const dynamic = 'force-dynamic' +export const runtime = 'nodejs' + +interface CheckResult { + ok: boolean + detail?: string +} + +async function checkDatabase(): Promise { + if (!process.env.DATABASE_URL) { + return { ok: false, detail: 'DATABASE_URL not set' } + } + try { + const sql = getDb() + await sql`SELECT 1 as ping` + return { ok: true } + } catch (error) { + return { ok: false, detail: error instanceof Error ? error.message : 'unknown error' } + } +} + +function checkEnv(): Record { + return { + DATABASE_URL: !!process.env.DATABASE_URL, + GITHUB_CLIENT_ID: !!(process.env.GITHUB_CLIENT_ID || process.env.NEXT_PUBLIC_GITHUB_CLIENT_ID), + GITHUB_CLIENT_SECRET: !!process.env.GITHUB_CLIENT_SECRET, + NEXT_PUBLIC_APP_URL: !!process.env.NEXT_PUBLIC_APP_URL, + OPENAI_API_KEY: !!process.env.OPENAI_API_KEY, + ANTHROPIC_API_KEY: !!process.env.ANTHROPIC_API_KEY, + STRIPE_SECRET_KEY: !!process.env.STRIPE_SECRET_KEY, + STRIPE_PRO_PRICE_ID: !!process.env.STRIPE_PRO_PRICE_ID, + } +} + +export async function GET() { + const startedAt = Date.now() + const database = await checkDatabase() + const env = checkEnv() + + const status = database.ok ? 'ok' : 'degraded' + + return NextResponse.json( + { + status, + uptime_ms: Date.now() - startedAt, + commit: process.env.VERCEL_GIT_COMMIT_SHA ?? null, + env, + checks: { + database, + }, + timestamp: new Date().toISOString(), + }, + { + status: 200, + headers: { + 'Cache-Control': 'no-store', + }, + }, + ) +} diff --git a/app/dashboard/error.tsx b/app/dashboard/error.tsx new file mode 100644 index 0000000..61573fe --- /dev/null +++ b/app/dashboard/error.tsx @@ -0,0 +1,46 @@ +'use client' + +import { useEffect } from 'react' +import Link from 'next/link' + +export default function DashboardError({ + error, + reset, +}: { + error: Error & { digest?: string } + reset: () => void +}) { + useEffect(() => { + console.error('[DashboardError]', error) + }, [error]) + + return ( +
+

Dashboard

+

This panel could not load.

+

+ Try refreshing the panel. If it keeps happening, check your environment variables on Vercel. +

+ {error?.digest ? ( + + ref: {error.digest} + + ) : null} +
+ + + Dashboard home + +
+
+ ) +} diff --git a/app/dashboard/loading.tsx b/app/dashboard/loading.tsx new file mode 100644 index 0000000..5ac99f1 --- /dev/null +++ b/app/dashboard/loading.tsx @@ -0,0 +1,8 @@ +export default function DashboardLoading() { + return ( +
+
+

Loading dashboard…

+
+ ) +} diff --git a/app/error.tsx b/app/error.tsx new file mode 100644 index 0000000..a880257 --- /dev/null +++ b/app/error.tsx @@ -0,0 +1,50 @@ +'use client' + +import { useEffect } from 'react' +import Link from 'next/link' + +export default function RootError({ + error, + reset, +}: { + error: Error & { digest?: string } + reset: () => void +}) { + useEffect(() => { + console.error('[RootError]', error) + }, [error]) + + return ( + + +
+

RepoFuse

+

Something broke loading this page.

+

+ We logged it. Try again, or head back home. +

+ {error?.digest ? ( + + ref: {error.digest} + + ) : null} +
+ + + Go home + +
+
+ + + ) +} diff --git a/app/global-error.tsx b/app/global-error.tsx new file mode 100644 index 0000000..6c375f1 --- /dev/null +++ b/app/global-error.tsx @@ -0,0 +1,47 @@ +'use client' + +import { useEffect } from 'react' + +export default function GlobalError({ + error, + reset, +}: { + error: Error & { digest?: string } + reset: () => void +}) { + useEffect(() => { + console.error('[GlobalError]', error) + }, [error]) + + return ( + + +
+

RepoFuse

+

The page hit an unrecoverable error.

+

We logged it. Try again or go home.

+ {error?.digest ? ( + ref: {error.digest} + ) : null} +
+ +
+
+ + + ) +} diff --git a/app/layout.tsx b/app/layout.tsx index 84d8e68..41cfdf9 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -8,10 +8,44 @@ import './globals.css' const geist = Geist({ subsets: ['latin'], variable: '--font-geist' }) const geistMono = Geist_Mono({ subsets: ['latin'], variable: '--font-geist-mono' }) +function siteUrl(): string { + return ( + process.env.NEXT_PUBLIC_APP_URL || + (process.env.VERCEL_PROJECT_PRODUCTION_URL ? `https://${process.env.VERCEL_PROJECT_PRODUCTION_URL}` : 'https://repofuse.com') + ) +} + export const metadata: Metadata = { - title: 'RepoFuse - Discover Apps Hidden in Your Code', - description: 'AI-powered GitHub repository analyzer that discovers what apps you can build from your existing code', - generator: 'v0.app', + metadataBase: new URL(siteUrl()), + title: { + default: 'RepoFuse — Discover Apps Hidden in Your Code', + template: '%s · RepoFuse', + }, + description: 'AI-powered GitHub repository analyzer that discovers what apps you can build from your existing code.', + applicationName: 'RepoFuse', + generator: 'Next.js', + keywords: [ + 'GitHub analyzer', + 'AI code analysis', + 'reuse code', + 'project ideas', + 'app blueprints', + 'Claude MCP', + 'developer tools', + ], + openGraph: { + type: 'website', + url: siteUrl(), + title: 'RepoFuse — Discover Apps Hidden in Your Code', + description: 'Scan your GitHub repos, surface buildable app ideas, and ship faster.', + siteName: 'RepoFuse', + }, + twitter: { + card: 'summary_large_image', + title: 'RepoFuse', + description: 'Scan your GitHub repos, surface buildable app ideas, and ship faster.', + }, + robots: { index: true, follow: true }, icons: { icon: [ { diff --git a/app/loading.tsx b/app/loading.tsx new file mode 100644 index 0000000..749e1e6 --- /dev/null +++ b/app/loading.tsx @@ -0,0 +1,10 @@ +export default function RootLoading() { + return ( +
+
+
+

Loading…

+
+
+ ) +} diff --git a/app/not-found.tsx b/app/not-found.tsx new file mode 100644 index 0000000..d25bf84 --- /dev/null +++ b/app/not-found.tsx @@ -0,0 +1,29 @@ +import Link from 'next/link' + +export default function NotFound() { + return ( +
+
+

RepoFuse · 404

+

This route does not exist.

+

+ Either the page moved or never existed. Head back to the dashboard or homepage. +

+
+ + Home + + + Dashboard + +
+
+
+ ) +} diff --git a/app/robots.ts b/app/robots.ts new file mode 100644 index 0000000..6c221ec --- /dev/null +++ b/app/robots.ts @@ -0,0 +1,23 @@ +import type { MetadataRoute } from 'next' + +function baseUrl(): string { + return ( + process.env.NEXT_PUBLIC_APP_URL || + (process.env.VERCEL_PROJECT_PRODUCTION_URL ? `https://${process.env.VERCEL_PROJECT_PRODUCTION_URL}` : 'https://repofuse.com') + ) +} + +export default function robots(): MetadataRoute.Robots { + const url = baseUrl() + return { + rules: [ + { + userAgent: '*', + allow: ['/', '/pricing'], + disallow: ['/api/', '/dashboard/'], + }, + ], + sitemap: `${url}/sitemap.xml`, + host: url, + } +} diff --git a/app/sitemap.ts b/app/sitemap.ts new file mode 100644 index 0000000..e4b3e27 --- /dev/null +++ b/app/sitemap.ts @@ -0,0 +1,17 @@ +import type { MetadataRoute } from 'next' + +function baseUrl(): string { + return ( + process.env.NEXT_PUBLIC_APP_URL || + (process.env.VERCEL_PROJECT_PRODUCTION_URL ? `https://${process.env.VERCEL_PROJECT_PRODUCTION_URL}` : 'https://repofuse.com') + ) +} + +export default function sitemap(): MetadataRoute.Sitemap { + const url = baseUrl() + const now = new Date() + return [ + { url: `${url}/`, lastModified: now, changeFrequency: 'weekly', priority: 1 }, + { url: `${url}/pricing`, lastModified: now, changeFrequency: 'monthly', priority: 0.8 }, + ] +}