From abfc784aa40382da179c280e19848f9a579f4c9f Mon Sep 17 00:00:00 2001 From: Philip Olson Date: Sun, 3 May 2026 11:36:56 -0700 Subject: [PATCH 1/7] docs: add basic scalar integration --- next-env.d.ts | 6 + package-lock.json | 107 ++++++++++++++++++ package.json | 1 + .../docs/api-reference-preview/route.ts | 79 +++++++++++++ tsconfig.json | 38 +++++++ 5 files changed, 231 insertions(+) create mode 100644 next-env.d.ts create mode 100644 src/app/(docs)/docs/api-reference-preview/route.ts create mode 100644 tsconfig.json diff --git a/next-env.d.ts b/next-env.d.ts new file mode 100644 index 0000000000..c4b7818fbb --- /dev/null +++ b/next-env.d.ts @@ -0,0 +1,6 @@ +/// +/// +import "./.next/dev/types/routes.d.ts"; + +// NOTE: This file should not be edited +// see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/package-lock.json b/package-lock.json index e5b8dadad5..5a09194747 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,6 +24,7 @@ "@react-hook/throttle": "^2.2.0", "@react-hook/window-scroll": "^1.3.0", "@rive-app/react-canvas": "^4.20.0", + "@scalar/nextjs-api-reference": "^0.10.10", "@segment/analytics-next": "^1.67.0", "@splinetool/react-spline": "^2.2.6", "@supabase/sql-to-rest": "^0.1.8", @@ -8608,6 +8609,100 @@ "dev": true, "license": "MIT" }, + "node_modules/@scalar/client-side-rendering": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@scalar/client-side-rendering/-/client-side-rendering-0.1.3.tgz", + "integrity": "sha512-rUI5EQA8y0xivzqc/AwExZYW8HRfQSFxPxvvuLjHTCATpKz0xcQBGVJQIPOmp78NJyO8mnQmltE1AxIdtvdPGg==", + "license": "MIT", + "dependencies": { + "@scalar/types": "0.9.2" + }, + "engines": { + "node": ">=22" + } + }, + "node_modules/@scalar/helpers": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/@scalar/helpers/-/helpers-0.5.2.tgz", + "integrity": "sha512-Pi1GAl8jO6ungmGj2sjDfCfqiBNrKW6HXDZmminV94ybGU/KtRLOqHwd0n9FIhY3j0RYGpGC0VCuniCICfQPHg==", + "license": "MIT", + "engines": { + "node": ">=22" + } + }, + "node_modules/@scalar/nextjs-api-reference": { + "version": "0.10.10", + "resolved": "https://registry.npmjs.org/@scalar/nextjs-api-reference/-/nextjs-api-reference-0.10.10.tgz", + "integrity": "sha512-CTztkYcwi0qyWuGx1zgpmTuewOet0yQC8Gr4uAS8llw4llx/eeu2dLFeGH5pMkajxvtk2F9/kjMbt/yeDGzQ9Q==", + "license": "MIT", + "dependencies": { + "@scalar/client-side-rendering": "0.1.3" + }, + "engines": { + "node": ">=22" + }, + "peerDependencies": { + "next": "^15.0.0 || ^16.0.0", + "react": "^19.0.0" + } + }, + "node_modules/@scalar/types": { + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/@scalar/types/-/types-0.9.2.tgz", + "integrity": "sha512-aLn7QTHafpjrN/whup7U5/CHaoPPaYMNtz4L/2yfN5GDSy3AbDM7kV1q8s3nMQIhvC1uscxfriV+KhEND+xHvA==", + "license": "MIT", + "dependencies": { + "@scalar/helpers": "0.5.2", + "nanoid": "^5.1.6", + "type-fest": "^5.3.1", + "zod": "^4.3.5" + }, + "engines": { + "node": ">=22" + } + }, + "node_modules/@scalar/types/node_modules/nanoid": { + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.9.tgz", + "integrity": "sha512-ZUvP7KeBLe3OZ1ypw6dI/TzYJuvHP77IM4Ry73waSQTLn8/g8rpdjfyVAh7t1/+FjBtG4lCP42MEbDxOsRpBMw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.js" + }, + "engines": { + "node": "^18 || >=20" + } + }, + "node_modules/@scalar/types/node_modules/type-fest": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-5.6.0.tgz", + "integrity": "sha512-8ZiHFm91orbSAe2PSAiSVBVko18pbhbiB3U9GglSzF/zCGkR+rxpHx6sEMCUm4kxY4LjDIUGgCfUMtwfZfjfUA==", + "license": "(MIT OR CC0-1.0)", + "dependencies": { + "tagged-tag": "^1.0.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@scalar/types/node_modules/zod": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", + "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, "node_modules/@segment/analytics-core": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/@segment/analytics-core/-/analytics-core-1.6.0.tgz", @@ -27908,6 +28003,18 @@ "integrity": "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==", "license": "MIT" }, + "node_modules/tagged-tag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/tagged-tag/-/tagged-tag-1.0.0.tgz", + "integrity": "sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==", + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/tailwind-merge": { "version": "3.5.0", "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.5.0.tgz", diff --git a/package.json b/package.json index 087373c458..fbb79893ab 100644 --- a/package.json +++ b/package.json @@ -54,6 +54,7 @@ "@react-hook/throttle": "^2.2.0", "@react-hook/window-scroll": "^1.3.0", "@rive-app/react-canvas": "^4.20.0", + "@scalar/nextjs-api-reference": "^0.10.10", "@segment/analytics-next": "^1.67.0", "@splinetool/react-spline": "^2.2.6", "@supabase/sql-to-rest": "^0.1.8", diff --git a/src/app/(docs)/docs/api-reference-preview/route.ts b/src/app/(docs)/docs/api-reference-preview/route.ts new file mode 100644 index 0000000000..655f1e8586 --- /dev/null +++ b/src/app/(docs)/docs/api-reference-preview/route.ts @@ -0,0 +1,79 @@ +import { renderApiReference } from '@scalar/client-side-rendering'; + +const SPEC_URL = 'https://neon.com/api_spec/release/v2.json'; + +const CUSTOM_CSS = ` + .dark-mode { + --scalar-background-1: #0d0e12; + --scalar-background-2: #131415; + --scalar-background-3: #18191b; + --scalar-background-accent: #00E59912; + --scalar-color-1: #e4e5e7; + --scalar-color-2: #afb1b6; + --scalar-color-3: #797d86; + --scalar-color-accent: #00E599; + --scalar-border-color: #242628; + --scalar-font: 'IBM Plex Sans', sans-serif; + --scalar-font-code: 'IBM Plex Mono', 'Fira Code', monospace; + --scalar-radius: 6px; + --scalar-radius-lg: 8px; + } + + .light-mode { + --scalar-background-1: #ffffff; + --scalar-background-2: #f2f2f3; + --scalar-background-3: #efeff0; + --scalar-background-accent: #00E59912; + --scalar-color-1: #0c0d0d; + --scalar-color-2: #494b50; + --scalar-color-3: #797d86; + --scalar-color-accent: #00E599; + --scalar-border-color: #e4e5e7; + --scalar-font: 'IBM Plex Sans', sans-serif; + --scalar-font-code: 'IBM Plex Mono', 'Fira Code', monospace; + --scalar-radius: 6px; + --scalar-radius-lg: 8px; + } +`; + +export async function GET() { + let spec: Record; + + try { + const res = await fetch(SPEC_URL, { next: { revalidate: 300 } }); + spec = await res.json(); + } catch { + return new Response('Failed to load API spec', { status: 502 }); + } + + // Strip fields from the spec info that shouldn't appear in the UI + if (spec.info && typeof spec.info === 'object') { + const info = spec.info as Record; + delete info.contact; + if (typeof info.description === 'string') { + info.description = info.description.replaceAll('https://neon.tech/docs/', 'https://neon.com/docs/'); + } + } + + const html = renderApiReference( + { + config: { + content: JSON.stringify(spec), + pageTitle: 'Neon API Reference', + theme: 'default', + agent: { disabled: true }, + mcp: { disabled: true }, + showToolbar: 'never', + hideModels: true, + hideClientButton: true, + defaultHttpClient: { targetKey: 'js', clientKey: 'fetch' }, + }, + pageTitle: 'Neon API Reference', + }, + CUSTOM_CSS, + ); + + return new Response(html, { + headers: { 'Content-Type': 'text/html' }, + }); +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000000..d53d0e234c --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,38 @@ +{ + "compilerOptions": { + "baseUrl": "src", + "target": "ES2017", + "lib": [ + "dom", + "dom.iterable", + "esnext" + ], + "allowJs": true, + "skipLibCheck": true, + "strict": false, + "noEmit": true, + "incremental": true, + "module": "esnext", + "esModuleInterop": true, + "moduleResolution": "node", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "react-jsx", + "plugins": [ + { + "name": "next" + } + ] + }, + "include": [ + "next-env.d.ts", + ".next/types/**/*.ts", + ".next/dev/types/**/*.ts", + "**/*.mts", + "**/*.ts", + "**/*.tsx" + ], + "exclude": [ + "node_modules" + ] +} From 3f83b4a98ac59ab712b818548112d5bc00d46f9b Mon Sep 17 00:00:00 2001 From: Philip Olson Date: Sun, 3 May 2026 12:10:12 -0700 Subject: [PATCH 2/7] docs: integrate into neon.com doc site --- .../api-reference-preview/ScalarMount.tsx | 129 ++++++++++++++++++ .../docs/api-reference-preview/page.tsx | 28 ++++ src/app/(api-docs)/layout.tsx | 21 +++ .../docs/api-reference-preview/route.ts | 79 ----------- 4 files changed, 178 insertions(+), 79 deletions(-) create mode 100644 src/app/(api-docs)/docs/api-reference-preview/ScalarMount.tsx create mode 100644 src/app/(api-docs)/docs/api-reference-preview/page.tsx create mode 100644 src/app/(api-docs)/layout.tsx delete mode 100644 src/app/(docs)/docs/api-reference-preview/route.ts diff --git a/src/app/(api-docs)/docs/api-reference-preview/ScalarMount.tsx b/src/app/(api-docs)/docs/api-reference-preview/ScalarMount.tsx new file mode 100644 index 0000000000..3c205c5c86 --- /dev/null +++ b/src/app/(api-docs)/docs/api-reference-preview/ScalarMount.tsx @@ -0,0 +1,129 @@ +'use client'; + +import { useTheme } from 'next-themes'; +import { useEffect, useRef } from 'react'; + +// Pin to major version so patch/minor updates are picked up but breaking changes are not. +// Update this when intentionally upgrading Scalar. +const CDN_URL = 'https://cdn.jsdelivr.net/npm/@scalar/api-reference@1'; + +// Module-level promise: CDN loads once per page session regardless of React re-renders or +// theme changes. Persists across HMR reloads in dev (acceptable). +let scalarCdnReady: Promise | null = null; + +function loadScalarCdn(): Promise { + if (scalarCdnReady) return scalarCdnReady; + + scalarCdnReady = new Promise((resolve, reject) => { + // Already loaded (e.g. back-navigation in SPA) + // eslint-disable-next-line @typescript-eslint/no-explicit-any + if (typeof (window as any).Scalar?.createApiReference === 'function') { + resolve(); + return; + } + const script = document.createElement('script'); + script.src = CDN_URL; + script.onload = () => resolve(); + script.onerror = () => { + scalarCdnReady = null; // allow retry on next mount + reject(new Error('Failed to load Scalar CDN')); + }; + document.head.appendChild(script); + }); + + return scalarCdnReady; +} + +function buildConfig(spec: string, darkMode: boolean) { + return { + content: spec, + theme: 'default', + darkMode, + agent: { disabled: true }, + mcp: { disabled: true }, + showDeveloperTools: 'never', + hideModels: true, + hideClientButton: true, + defaultOpenAllTags: true, + defaultHttpClient: { targetKey: 'js', clientKey: 'fetch' }, + }; +} + +const NEON_CSS = ` + /* --scalar-custom-header-height is Scalar's public variable for external header height. + It feeds --refs-header-height which controls sidebar sticky top, sidebar height, + and IntersectionObserver rootMargin. Must be set on :root. */ + :root { + --scalar-custom-header-height: 112px; + } + /* Ensure anchor jumps and sidebar scrollIntoView land below the Neon header */ + html { + scroll-padding-top: 112px; + } + #scalar-mount .dark-mode { + --scalar-background-1: #0d0e12; + --scalar-background-2: #131415; + --scalar-background-3: #18191b; + --scalar-background-accent: #00E59912; + --scalar-color-1: #e4e5e7; + --scalar-color-2: #afb1b6; + --scalar-color-3: #797d86; + --scalar-color-accent: #00E599; + --scalar-border-color: #242628; + --scalar-font: 'IBM Plex Sans', sans-serif; + --scalar-font-code: 'IBM Plex Mono', 'Fira Code', monospace; + } + #scalar-mount .light-mode { + --scalar-background-1: #ffffff; + --scalar-background-2: #f2f2f3; + --scalar-background-3: #efeff0; + --scalar-background-accent: #00E59912; + --scalar-color-1: #0c0d0d; + --scalar-color-2: #494b50; + --scalar-color-3: #797d86; + --scalar-color-accent: #00E599; + --scalar-border-color: #e4e5e7; + --scalar-font: 'IBM Plex Sans', sans-serif; + --scalar-font-code: 'IBM Plex Mono', 'Fira Code', monospace; + } +`; + +export default function ScalarMount({ spec }: { spec: string }) { + const { resolvedTheme } = useTheme(); + const mountRef = useRef(null); + + useEffect(() => { + const mount = mountRef.current; + if (!mount) return; + + // Treat undefined (SSR/pre-hydration) as dark to match neon's default + const darkMode = resolvedTheme !== 'light'; + + loadScalarCdn() + .then(() => { + if (!mount) return; + // Clear previous Scalar instance before re-initializing + while (mount.firstChild) mount.removeChild(mount.firstChild); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (window as any).Scalar.createApiReference('#scalar-mount', buildConfig(spec, darkMode)); + }) + .catch((err) => { + console.error('Scalar failed to initialize:', err); + }); + }, [spec, resolvedTheme]); + + return ( + <> + {/* eslint-disable-next-line react/no-danger */} +