diff --git a/website/src/app/benchmarks/BenchmarksContent.tsx b/website/src/app/benchmarks/BenchmarksContent.tsx
index d2b6f924..f847cfe6 100644
--- a/website/src/app/benchmarks/BenchmarksContent.tsx
+++ b/website/src/app/benchmarks/BenchmarksContent.tsx
@@ -1,8 +1,11 @@
'use client';
+import { useState } from 'react';
import { FadeIn } from '@/components/ui/FadeIn';
import { GlassCard } from '@/components/ui/GlassCard';
import { Button } from '@/components/ui/Button';
+import { ParseChart } from '@/components/benchmarks/ParseChart';
+import { CompetitorChart } from '@/components/benchmarks/CompetitorChart';
const metrics = [
{ label: 'Sustained Ops/sec', value: '1.38M+' },
@@ -26,6 +29,38 @@ const methodology = [
'Results averaged over 5 consecutive runs to reduce variance',
];
+function RawDataToggle({ id, children }: { id: string; children: React.ReactNode }) {
+ const [open, setOpen] = useState(false);
+
+ return (
+
+
+ {open && (
+
+ {children}
+
+ )}
+
+ );
+}
+
function renderMethodologyItem(item: string) {
const FLAG = '-benchmem';
const idx = item.indexOf(FLAG);
@@ -75,110 +110,121 @@ export function BenchmarksContent() {
- {/* Benchmark Table */}
+ {/* Parse Benchmarks Chart + Table */}
Parse Benchmarks
-
-
-
- GoSQLX Parse Benchmarks
-
-
- | Benchmark |
- Query Type |
- Apple M4 |
- Baseline (CI) |
-
-
-
- {benchmarks.map((b) => (
-
- | {b.name} |
- {b.query} |
- {b.m4} |
- {b.baseline} |
-
- ))}
-
-
-
+
+
- ← Swipe to see all columns →
+
+
+
+
+
+ GoSQLX Parse Benchmarks
+
+
+ | Benchmark |
+ Query Type |
+ Apple M4 |
+ Baseline (CI) |
+
+
+
+ {benchmarks.map((b) => (
+
+ | {b.name} |
+ {b.query} |
+ {b.m4} |
+ {b.baseline} |
+
+ ))}
+
+
+
+
+ ← Swipe to see all columns →
+
- {/* Competitor Comparison */}
+ {/* Competitor Comparison Chart + Table */}
Competitor Comparison
-
-
-
- Competitor Library Comparison
-
-
- | Library |
- Language |
- Ops/sec |
- Memory/op |
- Zero-copy |
-
-
-
-
- |
- GoSQLX{' '}
-
- This Library
-
- |
- Go |
- 1.38M+ |
- Low |
-
- ✓
- |
-
-
- | xwb1989/sqlparser |
- Go |
- ~380K |
- Higher |
-
- ✗
- |
-
-
- | pg_query_go |
- Go |
- ~220K |
- Higher (CGo) |
-
- ✗
- |
-
-
- | blastrain/sqlparser |
- Go |
- ~290K |
- Medium |
-
- ✗
- |
-
-
-
-
+
+
- ← Swipe to see all columns →
- * Competitor figures estimated from published benchmarks on equivalent hardware. Results may vary by query complexity.
+
+
+
+
+
+ Competitor Library Comparison
+
+
+ | Library |
+ Language |
+ Ops/sec |
+ Memory/op |
+ Zero-copy |
+
+
+
+
+ |
+ GoSQLX{' '}
+
+ This Library
+
+ |
+ Go |
+ 1.38M+ |
+ Low |
+
+ {'\u2713'}
+ |
+
+
+ | xwb1989/sqlparser |
+ Go |
+ ~380K |
+ Higher |
+
+ {'\u2717'}
+ |
+
+
+ | pg_query_go |
+ Go |
+ ~220K |
+ Higher (CGo) |
+
+ {'\u2717'}
+ |
+
+
+ | blastrain/sqlparser |
+ Go |
+ ~290K |
+ Medium |
+
+ {'\u2717'}
+ |
+
+
+
+
+
+ ← Swipe to see all columns →
+
diff --git a/website/src/app/compare/CompareContent.tsx b/website/src/app/compare/CompareContent.tsx
new file mode 100644
index 00000000..081e7a73
--- /dev/null
+++ b/website/src/app/compare/CompareContent.tsx
@@ -0,0 +1,262 @@
+'use client';
+
+import Link from 'next/link';
+import { FadeIn } from '@/components/ui/FadeIn';
+import { GlassCard } from '@/components/ui/GlassCard';
+import { Button } from '@/components/ui/Button';
+
+/* ------------------------------------------------------------------ */
+/* Data */
+/* ------------------------------------------------------------------ */
+
+const LIBRARIES = ['GoSQLX', 'pg_query_go', 'vitess-sqlparser', 'sqlparser (xwb1989)'] as const;
+
+type FeatureRow = {
+ feature: string;
+ values: [string | boolean, string | boolean, string | boolean, string | boolean];
+};
+
+const FEATURES: FeatureRow[] = [
+ { feature: 'Language', values: ['Go', 'Go (C via CGo)', 'Go', 'Go'] },
+ { feature: 'Ops/sec', values: ['1.38M+', '~340K', '~560K', '~220K'] },
+ { feature: 'Memory/op', values: ['~800B', '~12KB', '~4KB', '~2KB'] },
+ { feature: 'Zero-copy', values: [true, false, false, false] },
+ { feature: 'Dialects', values: ['8', '1 (PostgreSQL)', '1 (MySQL)', '1 (MySQL)'] },
+ { feature: 'CGo required', values: [false, true, false, false] },
+ { feature: 'WASM support', values: [true, false, false, false] },
+ { feature: 'SQL injection detection', values: [true, false, false, false] },
+ { feature: 'MCP server', values: [true, false, false, false] },
+ { feature: 'SQL formatting', values: [true, false, false, false] },
+ { feature: 'SQL linting (30 rules)', values: [true, false, false, false] },
+ { feature: 'LSP server', values: [true, false, false, false] },
+ { feature: 'Thread-safe', values: [true, true, true, true] },
+ { feature: 'Active maintenance', values: [true, true, true, false] },
+];
+
+const PERF_BARS = [
+ { name: 'GoSQLX', ops: 1_380_000, label: '1.38M ops/sec', accent: true },
+ { name: 'vitess-sqlparser', ops: 560_000, label: '560K ops/sec', accent: false },
+ { name: 'pg_query_go', ops: 340_000, label: '340K ops/sec', accent: false },
+ { name: 'sqlparser (xwb1989)', ops: 220_000, label: '220K ops/sec', accent: false },
+];
+
+const MAX_OPS = PERF_BARS[0].ops;
+
+/* ------------------------------------------------------------------ */
+/* Icons */
+/* ------------------------------------------------------------------ */
+
+function CheckIcon() {
+ return (
+
+ );
+}
+
+function XIcon() {
+ return (
+
+ );
+}
+
+/* ------------------------------------------------------------------ */
+/* Helpers */
+/* ------------------------------------------------------------------ */
+
+function CellValue({ value }: { value: string | boolean }) {
+ if (typeof value === 'boolean') {
+ return value ? : ;
+ }
+ return {value};
+}
+
+/* ------------------------------------------------------------------ */
+/* Component */
+/* ------------------------------------------------------------------ */
+
+export function CompareContent() {
+ return (
+
+ {/* Hero */}
+
+
+
+
+ GoSQLX vs The Competition
+
+
+ See how GoSQLX compares to other Go SQL parsing libraries
+
+
+
+
+
+ {/* Feature Comparison Table */}
+
+
+
+
+
+
+
+ | Feature |
+ {LIBRARIES.map((lib) => (
+
+ {lib}
+ |
+ ))}
+
+
+
+ {FEATURES.map((row, i) => (
+
+ |
+ {row.feature}
+ |
+ {row.values.map((val, j) => (
+
+
+
+
+ |
+ ))}
+
+ ))}
+
+
+
+
+
+
+
+ {/* Performance Comparison */}
+
+
+
+ Performance Comparison
+
+ Operations per second parsing a standard SELECT query (higher is better)
+
+
+
+ {PERF_BARS.map((bar) => {
+ const pct = Math.round((bar.ops / MAX_OPS) * 100);
+ return (
+
+
+
+ {bar.name}
+
+
+ {bar.label}
+
+
+
+
+ );
+ })}
+
+
+ Benchmarked on Apple Silicon with Go 1.26+, object pooling enabled, race detector off.
+ Results averaged over 5 runs.
+
+
+
+
+
+
+ {/* CTA */}
+
+
+
+
+
+ Ready to switch to the fastest Go SQL parser?
+
+
+ Get started in under a minute with a single{' '}
+
+ go get
+ {' '}
+ command.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/website/src/app/compare/page.tsx b/website/src/app/compare/page.tsx
new file mode 100644
index 00000000..5d41f584
--- /dev/null
+++ b/website/src/app/compare/page.tsx
@@ -0,0 +1,48 @@
+import type { Metadata } from 'next';
+import { CompareContent } from './CompareContent';
+
+export const metadata: Metadata = {
+ title: { absolute: 'GoSQLX vs pg_query vs vitess-sqlparser — Go SQL Parser Comparison' },
+ description:
+ 'Compare GoSQLX against pg_query_go, vitess-sqlparser, and xwb1989/sqlparser. Feature matrix, performance benchmarks, dialect support, and more for Go SQL parsing libraries.',
+ alternates: {
+ canonical: '/compare/',
+ },
+ openGraph: {
+ title: 'GoSQLX vs pg_query vs vitess-sqlparser — Go SQL Parser Comparison',
+ description:
+ 'Compare GoSQLX against pg_query_go, vitess-sqlparser, and xwb1989/sqlparser. Feature matrix, performance benchmarks, and dialect support.',
+ url: '/compare/',
+ },
+};
+
+// Structured data is static and hardcoded (no user input), safe for JSON serialization.
+const structuredData = JSON.stringify({
+ '@context': 'https://schema.org',
+ '@type': 'SoftwareApplication',
+ name: 'GoSQLX',
+ applicationCategory: 'DeveloperApplication',
+ operatingSystem: 'Any',
+ url: 'https://gosqlx.dev/compare/',
+ programmingLanguage: 'Go',
+ description:
+ 'High-performance, zero-copy SQL parsing SDK for Go with 8-dialect support, 1.38M+ ops/sec.',
+ offers: {
+ '@type': 'Offer',
+ price: '0',
+ priceCurrency: 'USD',
+ },
+});
+
+export default function ComparePage() {
+ return (
+ <>
+
+
+ >
+ );
+}
diff --git a/website/src/app/docs/[...slug]/page.tsx b/website/src/app/docs/[...slug]/page.tsx
index 66b17f1f..9f3b68de 100644
--- a/website/src/app/docs/[...slug]/page.tsx
+++ b/website/src/app/docs/[...slug]/page.tsx
@@ -8,8 +8,14 @@ import { getDocBySlug, getDocSlugs, getAdjacentDocs, extractHeadings } from '@/l
import { DOCS_SIDEBAR } from '@/lib/constants';
import { Sidebar } from '@/components/docs/Sidebar';
import { Toc } from '@/components/docs/Toc';
+import { DocNavigation } from '@/components/docs/DocNavigation';
import { mdxComponents } from '@/components/docs/mdx-components';
+function getReadingTime(text: string): number {
+ const words = text.trim().split(/\s+/).length;
+ return Math.max(1, Math.ceil(words / 200));
+}
+
interface PageProps {
params: Promise<{ slug: string[] }>;
}
@@ -47,6 +53,7 @@ export default async function DocPage({ params }: PageProps) {
const headings = extractHeadings(doc.content);
const { prev, next } = getAdjacentDocs(slugStr);
+ const readingTime = getReadingTime(doc.content);
const { content } = await compileMDX({
source: doc.content,
@@ -123,36 +130,21 @@ export default async function DocPage({ params }: PageProps) {
+ {/* Reading time */}
+
+
+ {readingTime} min read
+
+
{/* Content */}
{content}
{/* Prev / Next */}
-
- {prev ? (
-
-
- {prev.title}
-
- ) :
}
- {next ? (
-
- {next.title}
-
-
- ) :
}
-
+
{/* Table of Contents */}
diff --git a/website/src/app/docs/page.tsx b/website/src/app/docs/page.tsx
index f09572fb..c124627e 100644
--- a/website/src/app/docs/page.tsx
+++ b/website/src/app/docs/page.tsx
@@ -1,6 +1,7 @@
import type { Metadata } from 'next';
import Link from 'next/link';
import { DOCS_SIDEBAR } from '@/lib/constants';
+import { DocsSearchTrigger } from '@/components/docs/DocsSearchTrigger';
export const metadata: Metadata = {
title: 'Documentation',
@@ -39,6 +40,8 @@ export default function DocsPage() {
+
+
{DOCS_SIDEBAR.map((group) => (
[aria-hidden="true"] { opacity: 0.15; }
+
+/* --- Code blocks / pre elements --- */
+html.light pre[class*="font-mono"] {
+ background-color: #1E1E2E;
+ color: #D4D4D8;
+}
+html.light code { color: inherit; }
+
+/* Code in MiniPlayground, hero, etc. — keep dark code blocks */
+html.light .font-mono[class*="text-zinc-300"],
+html.light .font-mono[class*="text-zinc-400"] {
+ color: #D4D4D8;
+}
+
+/* Keep code blocks dark in light mode (standard convention) */
+html.light pre, html.light textarea[class*="font-mono"] {
+ background-color: #1E1E2E !important;
+ color: #D4D4D8 !important;
+}
+/* Undo text color overrides inside code blocks */
+html.light pre .text-white,
+html.light pre .text-zinc-300,
+html.light pre .text-zinc-400 {
+ color: inherit !important;
+}
+html.light pre .text-accent-green { color: #22C55E !important; }
+html.light pre .text-accent-orange { color: #F97316 !important; }
+
+/* Accent colors on code stay vivid */
+html.light pre .text-accent-green,
+html.light pre .text-accent-orange,
+html.light textarea .text-accent-green,
+html.light textarea .text-accent-orange {
+ color: inherit;
+}
+
+/* --- Stats / accent-colored numbers --- */
+html.light .text-accent-orange { color: #EA580C !important; }
+html.light .text-accent-green { color: #16A34A !important; }
+html.light .text-accent-indigo { color: #4F46E5 !important; }
+html.light .text-accent-purple { color: #7C3AED !important; }
+
+/* --- Section borders --- */
+html.light section[class*="border-t"] { border-color: rgba(0, 0, 0, 0.06) !important; }
+html.light [class*="border-b"][class*="border-white"] { border-color: rgba(0, 0, 0, 0.06) !important; }
+
+/* --- Feature card icon tinted backgrounds --- */
+html.light [class*="bg-accent-purple\\/10"] { background-color: rgba(124, 58, 237, 0.08) !important; }
+html.light [class*="bg-accent-green\\/10"] { background-color: rgba(22, 163, 74, 0.08) !important; }
+html.light [class*="bg-accent-orange\\/10"] { background-color: rgba(234, 88, 12, 0.08) !important; }
+html.light [class*="bg-accent-indigo\\/10"] { background-color: rgba(79, 70, 229, 0.08) !important; }
+html.light [class*="bg-red-500\\/10"] { background-color: rgba(220, 38, 38, 0.08) !important; }
+html.light [class*="bg-cyan-500\\/10"] { background-color: rgba(6, 182, 212, 0.08) !important; }
+
+/* --- Footer --- */
+html.light footer { border-color: rgba(0, 0, 0, 0.06); }
+html.light footer .text-white { color: #1A1A2E; }
+html.light footer .text-zinc-300 { color: #4B5563; }
+html.light footer .text-zinc-300:hover { color: #1F2937; }
+html.light footer .text-sm.text-zinc-300 { color: #6B7280; }
+
+/* --- Scrollbar --- */
+html.light ::-webkit-scrollbar-thumb { background: rgba(0, 0, 0, 0.12); }
+html.light ::-webkit-scrollbar-thumb:hover { background: rgba(0, 0, 0, 0.2); }
+
+/* --- Prose / docs --- */
+html.light .prose-dark {
+ --tw-prose-body: rgba(0, 0, 0, 0.75);
+ --tw-prose-headings: #1A1A2E;
+ --tw-prose-links: #4F46E5;
+ --tw-prose-code: #1A1A2E;
+ --tw-prose-pre-bg: #1E1E2E;
+}
+
+/* --- ThemeToggle button --- */
+html.light button[aria-label*="Switch to"] {
+ color: #4B5563;
+}
+html.light button[aria-label*="Switch to"]:hover {
+ color: #1A1A2E;
+ background-color: rgba(0, 0, 0, 0.04);
+}
+
+/* --- Comparison page / bar charts --- */
+html.light .bg-zinc-700 { background-color: #D1D5DB !important; }
+
+/* --- Dialects badge colors remain vibrant --- */
+html.light .text-green-400 { color: #16A34A !important; }
+html.light .text-blue-400 { color: #2563EB !important; }
+html.light .text-yellow-400 { color: #B45309 !important; }
+html.light .text-red-400 { color: #DC2626 !important; }
+html.light .text-cyan-400 { color: #0891B2 !important; }
+
+/* --- Search modal --- */
+html.light [class*="bg-zinc-950\\/"] {
+ background-color: rgba(0, 0, 0, 0.4) !important;
+}
+html.light [class*="bg-zinc-900"] {
+ background-color: #FFFFFF !important;
+}
+
+/* --- Shadow adjustments --- */
+html.light .shadow-2xl { --tw-shadow-color: rgba(0, 0, 0, 0.08); }
+html.light [class*="shadow-indigo-500\\/"] { --tw-shadow-color: rgba(99, 102, 241, 0.06); }
+
+/* --- Gradient accent line in footer --- */
+html.light .opacity-40[class*="bg-gradient"] { opacity: 0.2; }
/* Respect prefers-reduced-motion for all animations */
@media (prefers-reduced-motion: reduce) {
diff --git a/website/src/app/layout.tsx b/website/src/app/layout.tsx
index aea88246..c2597a7e 100644
--- a/website/src/app/layout.tsx
+++ b/website/src/app/layout.tsx
@@ -1,5 +1,5 @@
import type { Metadata } from 'next';
-import { instrumentSans, jetbrainsMono } from '@/lib/fonts';
+import { spaceGrotesk, inter, jetbrainsMono } from '@/lib/fonts';
import { Navbar } from '@/components/layout/Navbar';
import { Footer } from '@/components/layout/Footer';
import { ServiceWorkerRegister } from '@/components/ServiceWorkerRegister';
@@ -52,7 +52,14 @@ export default function RootLayout({
children: React.ReactNode;
}>) {
return (
-
+
+
+
+
-
+
+
+
-
>
);
diff --git a/website/src/components/benchmarks/BarChart.tsx b/website/src/components/benchmarks/BarChart.tsx
new file mode 100644
index 00000000..7cb963b6
--- /dev/null
+++ b/website/src/components/benchmarks/BarChart.tsx
@@ -0,0 +1,91 @@
+'use client';
+
+import { useEffect, useRef, useState } from 'react';
+
+export interface BarChartItem {
+ label: string;
+ value: number;
+ color?: string;
+ highlight?: boolean;
+}
+
+interface BarChartProps {
+ data: BarChartItem[];
+ unit?: string;
+}
+
+function formatValue(value: number): string {
+ if (value >= 1_000_000) {
+ const m = value / 1_000_000;
+ return m % 1 === 0 ? `${m}M` : `${m.toFixed(2)}M`;
+ }
+ if (value >= 1_000) {
+ const k = value / 1_000;
+ return k % 1 === 0 ? `${k}K` : `${k.toFixed(0)}K`;
+ }
+ return String(value);
+}
+
+export function BarChart({ data, unit = 'ops/sec' }: BarChartProps) {
+ const containerRef = useRef(null);
+ const [visible, setVisible] = useState(false);
+
+ useEffect(() => {
+ const el = containerRef.current;
+ if (!el) return;
+
+ const observer = new IntersectionObserver(
+ ([entry]) => {
+ if (entry.isIntersecting) {
+ setVisible(true);
+ observer.disconnect();
+ }
+ },
+ { threshold: 0.15 },
+ );
+
+ observer.observe(el);
+ return () => observer.disconnect();
+ }, []);
+
+ const maxValue = Math.max(...data.map((d) => d.value));
+
+ return (
+
+ {data.map((item) => {
+ const pct = maxValue > 0 ? (item.value / maxValue) * 100 : 0;
+
+ return (
+
+ {/* Label row */}
+
+
+ {item.label}
+ {item.highlight && (
+
+ GoSQLX
+
+ )}
+
+
+ {formatValue(item.value)} {unit}
+
+
+
+ {/* Bar */}
+
+
+ );
+ })}
+
+ );
+}
diff --git a/website/src/components/benchmarks/CompetitorChart.tsx b/website/src/components/benchmarks/CompetitorChart.tsx
new file mode 100644
index 00000000..24f4eb50
--- /dev/null
+++ b/website/src/components/benchmarks/CompetitorChart.tsx
@@ -0,0 +1,22 @@
+'use client';
+
+import { BarChart, type BarChartItem } from './BarChart';
+
+const competitorData: BarChartItem[] = [
+ { label: 'GoSQLX', value: 1_380_000, highlight: true },
+ { label: 'vitess-sqlparser', value: 560_000 },
+ { label: 'pg_query_go', value: 340_000 },
+ { label: 'sqlparser', value: 220_000 },
+];
+
+export function CompetitorChart() {
+ return (
+
+
Competitor Comparison
+
+
+ * Competitor figures estimated from published benchmarks on equivalent hardware.
+
+
+ );
+}
diff --git a/website/src/components/benchmarks/ParseChart.tsx b/website/src/components/benchmarks/ParseChart.tsx
new file mode 100644
index 00000000..612e92f7
--- /dev/null
+++ b/website/src/components/benchmarks/ParseChart.tsx
@@ -0,0 +1,20 @@
+'use client';
+
+import { BarChart, type BarChartItem } from './BarChart';
+
+const parseData: BarChartItem[] = [
+ { label: 'Simple SELECT', value: 1_400_000, highlight: true },
+ { label: 'Multi-row INSERT', value: 992_000, highlight: true },
+ { label: 'Window Function', value: 848_000, highlight: true },
+ { label: 'CTE (WITH RECURSIVE)', value: 833_000, highlight: true },
+ { label: 'Complex Multi-JOIN', value: 574_000, highlight: true },
+];
+
+export function ParseChart() {
+ return (
+
+
Parse Performance (Apple M4)
+
+
+ );
+}
diff --git a/website/src/components/docs/CopyButton.tsx b/website/src/components/docs/CopyButton.tsx
new file mode 100644
index 00000000..878570dc
--- /dev/null
+++ b/website/src/components/docs/CopyButton.tsx
@@ -0,0 +1,41 @@
+'use client';
+
+import { useState } from 'react';
+
+export function CopyButton({ text }: { text: string }) {
+ const [copied, setCopied] = useState(false);
+ const [failed, setFailed] = useState(false);
+
+ const copy = () => {
+ navigator.clipboard.writeText(text).then(() => {
+ setCopied(true);
+ setTimeout(() => setCopied(false), 2000);
+ }).catch(() => {
+ setFailed(true);
+ setTimeout(() => setFailed(false), 2000);
+ });
+ };
+
+ return (
+
+ );
+}
diff --git a/website/src/components/docs/DocNavigation.tsx b/website/src/components/docs/DocNavigation.tsx
new file mode 100644
index 00000000..fead0959
--- /dev/null
+++ b/website/src/components/docs/DocNavigation.tsx
@@ -0,0 +1,66 @@
+import Link from 'next/link';
+
+interface NavItem {
+ slug: string;
+ title: string;
+}
+
+interface DocNavigationProps {
+ prev: NavItem | null;
+ next: NavItem | null;
+}
+
+export function DocNavigation({ prev, next }: DocNavigationProps) {
+ return (
+
+ {prev ? (
+
+
+ Previous
+
+
+
+ {prev.title}
+
+
+ ) : (
+
+ )}
+ {next ? (
+
+
+ Next
+
+
+ {next.title}
+
+
+
+ ) : (
+
+ )}
+
+ );
+}
diff --git a/website/src/components/docs/DocsSearchTrigger.tsx b/website/src/components/docs/DocsSearchTrigger.tsx
new file mode 100644
index 00000000..98417b09
--- /dev/null
+++ b/website/src/components/docs/DocsSearchTrigger.tsx
@@ -0,0 +1,41 @@
+'use client';
+
+export function DocsSearchTrigger() {
+ function openSearch() {
+ // Dispatch Cmd+K so the Navbar's global listener opens the modal
+ window.dispatchEvent(
+ new KeyboardEvent('keydown', {
+ key: 'k',
+ metaKey: true,
+ bubbles: true,
+ }),
+ );
+ }
+
+ return (
+
+ );
+}
diff --git a/website/src/components/docs/Sidebar.tsx b/website/src/components/docs/Sidebar.tsx
index c1dee099..920bbb62 100644
--- a/website/src/components/docs/Sidebar.tsx
+++ b/website/src/components/docs/Sidebar.tsx
@@ -1,6 +1,6 @@
'use client';
-import { useState } from 'react';
+import { useState, useEffect } from 'react';
import Link from 'next/link';
import { usePathname } from 'next/navigation';
import { DOCS_SIDEBAR } from '@/lib/constants';
@@ -10,6 +10,18 @@ export function Sidebar() {
const [mobileOpen, setMobileOpen] = useState(false);
const [collapsed, setCollapsed] = useState>({});
+ // Lock body scroll when mobile drawer is open
+ useEffect(() => {
+ if (mobileOpen) {
+ document.body.style.overflow = 'hidden';
+ } else {
+ document.body.style.overflow = '';
+ }
+ return () => {
+ document.body.style.overflow = '';
+ };
+ }, [mobileOpen]);
+
const toggleCategory = (category: string) => {
setCollapsed((prev) => ({ ...prev, [category]: !prev[category] }));
};
diff --git a/website/src/components/docs/mdx-components.tsx b/website/src/components/docs/mdx-components.tsx
index 3e649015..46a1d7d2 100644
--- a/website/src/components/docs/mdx-components.tsx
+++ b/website/src/components/docs/mdx-components.tsx
@@ -1,32 +1,7 @@
'use client';
-import React, { useState, type ReactNode, type ComponentPropsWithoutRef } from 'react';
-
-function CopyButton({ text }: { text: string }) {
- const [copied, setCopied] = useState(false);
- const [copyFailed, setCopyFailed] = useState(false);
-
- const copy = () => {
- navigator.clipboard.writeText(text).then(() => {
- setCopied(true);
- setTimeout(() => setCopied(false), 2000);
- }).catch(() => {
- setCopyFailed(true);
- setTimeout(() => setCopyFailed(false), 2000);
- });
- };
-
- return (
-
- );
-}
+import React, { type ReactNode, type ComponentPropsWithoutRef } from 'react';
+import { CopyButton } from './CopyButton';
function HeadingAnchor({ id, level, children }: { id?: string; level: number; children?: ReactNode }) {
return React.createElement(
diff --git a/website/src/components/home/AnimatedBars.tsx b/website/src/components/home/AnimatedBars.tsx
new file mode 100644
index 00000000..fe19045d
--- /dev/null
+++ b/website/src/components/home/AnimatedBars.tsx
@@ -0,0 +1,69 @@
+'use client';
+
+import { useRef, useEffect, useState } from 'react';
+
+interface Benchmark {
+ name: string;
+ ops: number;
+ label: string;
+ highlight: boolean;
+}
+
+export function AnimatedBars({ benchmarks, maxOps }: { benchmarks: Benchmark[]; maxOps: number }) {
+ const ref = useRef(null);
+ const [visible, setVisible] = useState(false);
+
+ useEffect(() => {
+ const el = ref.current;
+ if (!el) return;
+
+ if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
+ setVisible(true);
+ return;
+ }
+
+ const observer = new IntersectionObserver(
+ ([entry]) => {
+ if (entry.isIntersecting) {
+ setVisible(true);
+ observer.unobserve(el);
+ }
+ },
+ { threshold: 0.2 },
+ );
+
+ observer.observe(el);
+ return () => observer.disconnect();
+ }, []);
+
+ return (
+
+ {benchmarks.map((b, i) => {
+ const widthPercent = (b.ops / maxOps) * 100;
+ return (
+
+
+ {b.name}
+
+
+
+ {b.label}
+
+
+ );
+ })}
+
+ );
+}
diff --git a/website/src/components/home/CodeExamples.tsx b/website/src/components/home/CodeExamples.tsx
index 699cf3b7..6099317f 100644
--- a/website/src/components/home/CodeExamples.tsx
+++ b/website/src/components/home/CodeExamples.tsx
@@ -3,7 +3,7 @@
import { useState } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { GlassCard } from '@/components/ui/GlassCard';
-import { FadeIn } from '@/components/ui/FadeIn';
+import { FadeInCSS } from '@/components/ui/FadeInCSS';
type Segment = { text: string; cls: string };
type CodeLine = Segment[];
@@ -103,12 +103,12 @@ export function CodeExamples() {
return (
-
+
Simple, Powerful API
-
-
+
+
{tabs.map((tab, i) => (
-
+
);
diff --git a/website/src/components/home/CtaBanner.tsx b/website/src/components/home/CtaBanner.tsx
index 5501523a..33484e8a 100644
--- a/website/src/components/home/CtaBanner.tsx
+++ b/website/src/components/home/CtaBanner.tsx
@@ -1,6 +1,4 @@
-'use client';
-
-import { FadeIn } from '@/components/ui/FadeIn';
+import { FadeInCSS } from '@/components/ui/FadeInCSS';
import { Button } from '@/components/ui/Button';
export function CtaBanner() {
@@ -14,7 +12,7 @@ export function CtaBanner() {
-
+
Ready to parse SQL at the speed of Go?
@@ -26,7 +24,7 @@ export function CtaBanner() {
Try Playground
-
+
);
diff --git a/website/src/components/home/DialectShowcase.tsx b/website/src/components/home/DialectShowcase.tsx
new file mode 100644
index 00000000..26646f85
--- /dev/null
+++ b/website/src/components/home/DialectShowcase.tsx
@@ -0,0 +1,81 @@
+import { GlassCard } from '@/components/ui/GlassCard';
+import { FadeInCSS } from '@/components/ui/FadeInCSS';
+
+interface Dialect {
+ name: string;
+ abbr: string;
+ color: string;
+ bgColor: string;
+ compliance: number;
+ detail?: string;
+ status: 'Excellent' | 'Very Good' | 'Good' | 'In Progress';
+}
+
+const DIALECTS: Dialect[] = [
+ { name: 'PostgreSQL', abbr: 'PG', color: 'text-blue-400', bgColor: 'bg-blue-500/20', compliance: 95, status: 'Excellent' },
+ { name: 'MySQL', abbr: 'My', color: 'text-orange-400', bgColor: 'bg-orange-500/20', compliance: 95, status: 'Excellent' },
+ { name: 'MariaDB', abbr: 'Ma', color: 'text-sky-400', bgColor: 'bg-sky-500/20', compliance: 95, status: 'Excellent' },
+ { name: 'SQL Server', abbr: 'MS', color: 'text-red-400', bgColor: 'bg-red-500/20', compliance: 85, status: 'Very Good' },
+ { name: 'Oracle', abbr: 'Or', color: 'text-rose-400', bgColor: 'bg-rose-500/20', compliance: 80, status: 'Good' },
+ { name: 'SQLite', abbr: 'SL', color: 'text-emerald-400', bgColor: 'bg-emerald-500/20', compliance: 85, status: 'Very Good' },
+ { name: 'Snowflake', abbr: 'SF', color: 'text-cyan-400', bgColor: 'bg-cyan-500/20', compliance: 100, detail: '87/87 QA', status: 'Excellent' },
+ { name: 'ClickHouse', abbr: 'CH', color: 'text-yellow-400', bgColor: 'bg-yellow-500/20', compliance: 83, detail: '69/83 QA', status: 'Good' },
+];
+
+const statusColors: Record = {
+ 'Excellent': 'bg-green-500/15 text-green-400 border-green-500/20',
+ 'Very Good': 'bg-blue-500/15 text-blue-400 border-blue-500/20',
+ 'Good': 'bg-yellow-500/15 text-yellow-400 border-yellow-500/20',
+ 'In Progress': 'bg-zinc-500/15 text-zinc-400 border-zinc-500/20',
+};
+
+function complianceColor(pct: number): string {
+ if (pct >= 95) return 'text-green-400';
+ if (pct >= 85) return 'text-blue-400';
+ if (pct >= 80) return 'text-yellow-400';
+ return 'text-zinc-400';
+}
+
+export function DialectShowcase() {
+ return (
+
+
+
+
+ 8 SQL Dialects, One Parser
+
+
+ From PostgreSQL to ClickHouse — parse them all
+
+
+
+ {DIALECTS.map((dialect, i) => (
+
+
+
+ {dialect.abbr}
+
+
+ {dialect.name}
+
+
+ {dialect.compliance}%
+
+ {dialect.detail && (
+ {dialect.detail}
+ )}
+
+ {dialect.status}
+
+
+
+ ))}
+
+
+
+ );
+}
diff --git a/website/src/components/home/FeatureGrid.tsx b/website/src/components/home/FeatureGrid.tsx
index a7e093ac..8f058451 100644
--- a/website/src/components/home/FeatureGrid.tsx
+++ b/website/src/components/home/FeatureGrid.tsx
@@ -1,7 +1,5 @@
-'use client';
-
import { GlassCard } from '@/components/ui/GlassCard';
-import { FadeIn } from '@/components/ui/FadeIn';
+import { FadeInCSS } from '@/components/ui/FadeInCSS';
import { FEATURES } from '@/lib/constants';
const icons: Record = {
@@ -50,22 +48,22 @@ export function FeatureGrid() {
return (
-
+
Built for Production
-
+
{FEATURES.map((feature, i) => (
-
+
{icons[feature.icon]}
{feature.title}
- {feature.description}
+ {feature.description}
-
+
))}
diff --git a/website/src/components/home/GitHubStarButton.tsx b/website/src/components/home/GitHubStarButton.tsx
new file mode 100644
index 00000000..26e630a4
--- /dev/null
+++ b/website/src/components/home/GitHubStarButton.tsx
@@ -0,0 +1,38 @@
+'use client';
+
+import { useState, useEffect } from 'react';
+
+export function GitHubStarButton() {
+ const [stars, setStars] = useState(null);
+
+ useEffect(() => {
+ const controller = new AbortController();
+ fetch('https://api.github.com/repos/ajitpratap0/GoSQLX', { signal: controller.signal })
+ .then((r) => r.json())
+ .then((d) => { if (typeof d.stargazers_count === 'number') setStars(d.stargazers_count); })
+ .catch(() => {});
+ return () => controller.abort();
+ }, []);
+
+ return (
+
+
+
+ Star
+ {stars !== null && (
+
+ {stars >= 1000 ? `${(stars / 1000).toFixed(1)}k` : stars}
+
+ )}
+
+ );
+}
diff --git a/website/src/components/home/GitHubStarCount.tsx b/website/src/components/home/GitHubStarCount.tsx
new file mode 100644
index 00000000..b0323561
--- /dev/null
+++ b/website/src/components/home/GitHubStarCount.tsx
@@ -0,0 +1,26 @@
+'use client';
+
+import { useState, useEffect } from 'react';
+
+export function GitHubStarCount() {
+ const [stars, setStars] = useState('1.2k');
+
+ useEffect(() => {
+ const controller = new AbortController();
+ fetch('https://api.github.com/repos/ajitpratap0/GoSQLX', { signal: controller.signal })
+ .then((r) => r.json())
+ .then((d) => {
+ if (typeof d.stargazers_count === 'number') {
+ setStars(
+ d.stargazers_count >= 1000
+ ? `${(d.stargazers_count / 1000).toFixed(1)}k`
+ : String(d.stargazers_count),
+ );
+ }
+ })
+ .catch(() => {});
+ return () => controller.abort();
+ }, []);
+
+ return {stars};
+}
diff --git a/website/src/components/home/Hero.tsx b/website/src/components/home/Hero.tsx
index 0acf0b36..425976e6 100644
--- a/website/src/components/home/Hero.tsx
+++ b/website/src/components/home/Hero.tsx
@@ -1,85 +1,10 @@
-'use client';
-
-import { FadeIn } from '@/components/ui/FadeIn';
+import { FadeInCSS } from '@/components/ui/FadeInCSS';
import { GlassCard } from '@/components/ui/GlassCard';
import { GradientText } from '@/components/ui/GradientText';
import { VersionBadge } from '@/components/ui/VersionBadge';
import { Button } from '@/components/ui/Button';
-import Link from 'next/link';
-import { useState, useEffect } from 'react';
-
-function GitHubStarButton() {
- const [stars, setStars] = useState(null);
-
- useEffect(() => {
- const controller = new AbortController();
- fetch('https://api.github.com/repos/ajitpratap0/GoSQLX', { signal: controller.signal })
- .then((r) => r.json())
- .then((d) => { if (typeof d.stargazers_count === 'number') setStars(d.stargazers_count); })
- .catch(() => {});
- return () => controller.abort();
- }, []);
-
- return (
-
-
-
- Star
- {stars !== null && (
-
- {stars >= 1000 ? `${(stars / 1000).toFixed(1)}k` : stars}
-
- )}
-
- );
-}
-
-const SAMPLE_SQL = `SELECT
- u.name,
- u.email,
- COUNT(o.id) AS order_count,
- SUM(o.total) AS lifetime_value
-FROM users u
-LEFT JOIN orders o ON o.user_id = u.id
-WHERE u.created_at > '2024-01-01'
-GROUP BY u.name, u.email
-HAVING COUNT(o.id) > 5
-ORDER BY lifetime_value DESC
-LIMIT 20;`;
-
-const SAMPLE_AST = `{
- "type": "Query",
- "body": {
- "type": "Select",
- "projection": [
- { "type": "CompoundIdentifier", "value": "u.name" },
- { "type": "CompoundIdentifier", "value": "u.email" },
- { "type": "Function", "name": "COUNT",
- "args": ["o.id"], "alias": "order_count" },
- { "type": "Function", "name": "SUM",
- "args": ["o.total"], "alias": "lifetime_value" }
- ],
- "from": {
- "type": "Join", "join_type": "LEFT",
- "left": { "type": "Table", "name": "users", "alias": "u" },
- "right": { "type": "Table", "name": "orders", "alias": "o" }
- },
- "selection": { "type": "BinaryOp", "op": ">" },
- "group_by": ["u.name", "u.email"],
- "having": { "type": "BinaryOp", "op": ">" },
- "order_by": [{ "expr": "lifetime_value", "asc": false }],
- "limit": 20
- }
-}`;
+import { GitHubStarButton } from '@/components/home/GitHubStarButton';
+import { MiniPlayground } from '@/components/home/MiniPlayground';
export function Hero() {
return (
@@ -116,31 +41,31 @@ export function Hero() {
{/* Content */}
{/* Version badge */}
-
+
-
+
{/* Headline */}
-
+
Parse SQL at the speed of Go
-
+
{/* Subtitle */}
-
+
Production-ready SQL parsing with zero-copy tokenization, object pooling, and multi-dialect support
-
+
{/* Buttons */}
-
+
-
+
- {/* Mini playground preview */}
-
-
-
-
- {/* SQL Input side */}
-
-
- {/* AST Output side */}
-
-
- AST Output
- parsed in <1ms
-
-
- {SAMPLE_AST}
-
-
-
-
- {/* Overlay CTA */}
-
-
- Try Interactive Playground
-
-
-
-
-
+ {/* Live mini playground */}
+
+
+
+
+
diff --git a/website/src/components/home/McpSection.tsx b/website/src/components/home/McpSection.tsx
index 89e0d3b0..9aa4cef3 100644
--- a/website/src/components/home/McpSection.tsx
+++ b/website/src/components/home/McpSection.tsx
@@ -1,7 +1,5 @@
-'use client';
-
import Link from 'next/link';
-import { FadeIn } from '@/components/ui/FadeIn';
+import { FadeInCSS } from '@/components/ui/FadeInCSS';
import { TerminalMockup } from '@/components/ui/TerminalMockup';
const tools = [
@@ -18,21 +16,21 @@ export function McpSection() {
return (
-
+
AI-Ready SQL Tools
Connect 7 SQL tools to Claude, Cursor, or any MCP client — no installation, no API key.
-
-
+
+
-
-
+
+
{tools.map((tool) => (
))}
-
-
+
+
-
+
);
diff --git a/website/src/components/home/MiniPlayground.tsx b/website/src/components/home/MiniPlayground.tsx
new file mode 100644
index 00000000..7dc4458c
--- /dev/null
+++ b/website/src/components/home/MiniPlayground.tsx
@@ -0,0 +1,240 @@
+'use client';
+import { useState, useEffect, useCallback, useRef } from 'react';
+import Link from 'next/link';
+import { useWasm, type GoSQLXApi } from '@/components/playground/WasmLoader';
+
+const SAMPLE_SQL = `SELECT
+ u.name,
+ u.email,
+ COUNT(o.id) AS order_count,
+ SUM(o.total) AS lifetime_value
+FROM users u
+LEFT JOIN orders o ON o.user_id = u.id
+WHERE u.created_at > '2024-01-01'
+GROUP BY u.name, u.email
+HAVING COUNT(o.id) > 5
+ORDER BY lifetime_value DESC
+LIMIT 20;`;
+
+const DIALECTS = [
+ { value: 'generic', label: 'Generic' },
+ { value: 'postgresql', label: 'PostgreSQL' },
+ { value: 'mysql', label: 'MySQL' },
+ { value: 'sqlserver', label: 'SQL Server' },
+ { value: 'snowflake', label: 'Snowflake' },
+ { value: 'clickhouse', label: 'ClickHouse' },
+];
+
+type TabId = 'ast' | 'format' | 'lint';
+
+function formatDuration(us: number): string {
+ if (us < 1000) return `${us.toFixed(1)}\u00B5s`;
+ return `${(us / 1000).toFixed(2)}ms`;
+}
+
+function runParse(api: GoSQLXApi, sql: string, dialect: string, tab: TabId) {
+ const start = performance.now();
+ let result: unknown;
+ let error: string | null = null;
+ try {
+ if (tab === 'ast') {
+ result = api.parse(sql, dialect);
+ } else if (tab === 'format') {
+ const f = api.format(sql, dialect);
+ result = typeof f === 'string' ? f : (f as { result?: string }).result ?? JSON.stringify(f, null, 2);
+ } else {
+ result = api.lint(sql, dialect);
+ }
+ } catch (e: unknown) {
+ error = e instanceof Error ? e.message : String(e);
+ }
+ const elapsed = (performance.now() - start) * 1000; // to microseconds
+ return { result, error, elapsed };
+}
+
+function formatOutput(result: unknown, tab: TabId): string {
+ if (tab === 'format') return typeof result === 'string' ? result : '';
+ if (tab === 'lint') {
+ const violations = Array.isArray(result)
+ ? result
+ : (result as { violations?: unknown[] })?.violations ?? [];
+ if (Array.isArray(violations) && violations.length === 0) return 'No lint violations found.';
+ return JSON.stringify(violations, null, 2);
+ }
+ return JSON.stringify(result, null, 2);
+}
+
+export function MiniPlayground() {
+ const { loading, ready, api, progress } = useWasm();
+ const [sql, setSql] = useState(SAMPLE_SQL);
+ const [dialect, setDialect] = useState('generic');
+ const [activeTab, setActiveTab] = useState('ast');
+ const [output, setOutput] = useState('');
+ const [parseError, setParseError] = useState(null);
+ const [elapsed, setElapsed] = useState(null);
+ const debounceRef = useRef>(null);
+
+ const parse = useCallback(
+ (query: string, dial: string, tab: TabId) => {
+ if (!api || !query.trim()) {
+ setOutput('');
+ setParseError(null);
+ setElapsed(null);
+ return;
+ }
+ const { result, error, elapsed: us } = runParse(api, query, dial, tab);
+ setElapsed(us);
+ if (error) {
+ setParseError(error);
+ setOutput('');
+ } else {
+ setParseError(null);
+ setOutput(formatOutput(result, tab));
+ }
+ },
+ [api],
+ );
+
+ // Parse on ready
+ useEffect(() => {
+ if (ready && api) parse(sql, dialect, activeTab);
+ }, [ready, api]); // eslint-disable-line react-hooks/exhaustive-deps
+
+ // Re-parse on tab/dialect change (immediate)
+ useEffect(() => {
+ if (ready && api) parse(sql, dialect, activeTab);
+ }, [dialect, activeTab]); // eslint-disable-line react-hooks/exhaustive-deps
+
+ // Debounced parse on SQL change
+ const handleSqlChange = useCallback(
+ (e: React.ChangeEvent) => {
+ const value = e.target.value;
+ setSql(value);
+ if (debounceRef.current) clearTimeout(debounceRef.current);
+ debounceRef.current = setTimeout(() => {
+ parse(value, dialect, activeTab);
+ }, 50);
+ },
+ [parse, dialect, activeTab],
+ );
+
+ const tabLabel = activeTab === 'ast' ? 'AST Output' : activeTab === 'format' ? 'Formatted' : 'Lint';
+
+ return (
+
+ {/* Left: SQL input */}
+
+
+
+
+
+
+
+
query.sql
+
+
+
+
+
+
+
+
+ {/* Right: output */}
+
+
+
+ {(['ast', 'format', 'lint'] as TabId[]).map((tab) => (
+
+ ))}
+
+
+ {loading
+ ? progress < 1
+ ? `Loading... ${Math.round(progress * 100)}%`
+ : 'Initializing...'
+ : elapsed !== null
+ ? `Parsed in ${formatDuration(elapsed)}`
+ : ''}
+
+
+
+ {parseError ? (
+
+ {parseError}
+
+ ) : (
+
+ {output || (loading ? SAMPLE_AST_PLACEHOLDER : '')}
+
+ )}
+
+
+
+ Open Full Playground →
+
+
+
+
+ );
+}
+
+// Static placeholder shown while WASM loads
+const SAMPLE_AST_PLACEHOLDER = `{
+ "type": "Query",
+ "body": {
+ "type": "Select",
+ "projection": [
+ { "type": "CompoundIdentifier", "value": "u.name" },
+ { "type": "CompoundIdentifier", "value": "u.email" },
+ { "type": "Function", "name": "COUNT", "alias": "order_count" },
+ { "type": "Function", "name": "SUM", "alias": "lifetime_value" }
+ ],
+ "from": { "type": "Join", "join_type": "LEFT" },
+ "group_by": ["u.name", "u.email"],
+ "order_by": [{ "expr": "lifetime_value", "asc": false }],
+ "limit": 20
+ }
+}`;
diff --git a/website/src/components/home/PerformanceSection.tsx b/website/src/components/home/PerformanceSection.tsx
new file mode 100644
index 00000000..8a16397e
--- /dev/null
+++ b/website/src/components/home/PerformanceSection.tsx
@@ -0,0 +1,61 @@
+import { GlassCard } from '@/components/ui/GlassCard';
+import { FadeInCSS } from '@/components/ui/FadeInCSS';
+import { AnimatedCounter } from '@/components/ui/AnimatedCounter';
+import { AnimatedBars } from './AnimatedBars';
+
+const stats = [
+ { value: 1380000, suffix: '+', label: 'ops/sec', color: 'text-accent-orange' },
+ { value: 1, suffix: '\u00B5s', label: 'latency', color: 'text-accent-indigo', prefix: '<' },
+ { value: 85, suffix: '%', label: 'SQL-99', color: 'text-accent-green' },
+ { value: 8, suffix: '', label: 'Dialects', color: 'text-accent-purple' },
+];
+
+const benchmarks = [
+ { name: 'GoSQLX', ops: 1380000, label: '1.38M ops/sec', highlight: true },
+ { name: 'vitess', ops: 560000, label: '560K ops/sec', highlight: false },
+ { name: 'pg_query', ops: 340000, label: '340K ops/sec', highlight: false },
+ { name: 'sqlparser', ops: 220000, label: '220K ops/sec', highlight: false },
+];
+
+const maxOps = Math.max(...benchmarks.map((b) => b.ops));
+
+export function PerformanceSection() {
+ return (
+
+
+
+
+ Performance That Speaks for Itself
+
+
+
+ {/* Stat cards */}
+
+ {stats.map((stat, i) => (
+
+
+
+ {stat.prefix && (
+
{stat.prefix}
+ )}
+
+
+ {stat.label}
+
+
+ ))}
+
+
+ {/* Bar chart */}
+
+
+
+
+ Based on BenchmarkParse, Apple M4, Go 1.26
+
+
+
+
+
+ );
+}
diff --git a/website/src/components/home/SocialProof.tsx b/website/src/components/home/SocialProof.tsx
index feca712a..2d28d90e 100644
--- a/website/src/components/home/SocialProof.tsx
+++ b/website/src/components/home/SocialProof.tsx
@@ -1,7 +1,5 @@
-'use client';
-
import Image from 'next/image';
-import { FadeIn } from '@/components/ui/FadeIn';
+import { FadeInCSS } from '@/components/ui/FadeInCSS';
const badges = [
{
@@ -34,7 +32,7 @@ export function SocialProof() {
return (
-
+
{badges.map((badge) => (
))}
-
+
);
diff --git a/website/src/components/home/StatsBar.tsx b/website/src/components/home/StatsBar.tsx
index 65744207..9f34afbc 100644
--- a/website/src/components/home/StatsBar.tsx
+++ b/website/src/components/home/StatsBar.tsx
@@ -1,7 +1,5 @@
-'use client';
-
import { GlassCard } from '@/components/ui/GlassCard';
-import { FadeIn } from '@/components/ui/FadeIn';
+import { FadeInCSS } from '@/components/ui/FadeInCSS';
import { AnimatedCounter } from '@/components/ui/AnimatedCounter';
const stats = [
@@ -17,7 +15,7 @@ export function StatsBar() {
{stats.map((stat, i) => (
-
+
{stat.prefix && (
@@ -25,9 +23,9 @@ export function StatsBar() {
)}
- {stat.label}
+ {stat.label}
-
+
))}
diff --git a/website/src/components/home/TrustSection.tsx b/website/src/components/home/TrustSection.tsx
new file mode 100644
index 00000000..b86a2c33
--- /dev/null
+++ b/website/src/components/home/TrustSection.tsx
@@ -0,0 +1,142 @@
+import { FadeInCSS } from '@/components/ui/FadeInCSS';
+import { GitHubStarCount } from './GitHubStarCount';
+
+/* ── Inline SVG icons (Heroicons-style, 20x20) ────────────────────────── */
+
+function StarIcon() {
+ return (
+
+ );
+}
+
+function CheckIcon() {
+ return (
+
+ );
+}
+
+function BoltIcon() {
+ return (
+
+ );
+}
+
+function LockIcon() {
+ return (
+
+ );
+}
+
+function PackageIcon() {
+ return (
+
+ );
+}
+
+/* ── Metric data ───────────────────────────────────────────────────────── */
+
+const metrics = [
+ {
+ id: 'stars',
+ icon: ,
+ value: null, // rendered via client component
+ label: 'GitHub Stars',
+ },
+ {
+ id: 'tests',
+ icon: ,
+ value: '800+',
+ label: 'Tests Passing',
+ },
+ {
+ id: 'perf',
+ icon: ,
+ value: '1.38M',
+ label: 'ops/sec',
+ },
+ {
+ id: 'race',
+ icon: ,
+ value: 'Zero',
+ label: 'Race Conditions',
+ },
+ {
+ id: 'go',
+ icon: ,
+ value: 'Go 1.26+',
+ label: 'Minimum Version',
+ },
+] as const;
+
+/* ── Integration data ──────────────────────────────────────────────────── */
+
+const integrations = [
+ { name: 'Claude', detail: 'MCP Server' },
+ { name: 'VS Code', detail: 'Extension' },
+ { name: 'Cursor', detail: 'MCP Server' },
+] as const;
+
+/* ── Component ─────────────────────────────────────────────────────────── */
+
+export function TrustSection() {
+ return (
+
+
+ {/* Heading */}
+
+
+ Trusted by Developers
+
+
+
+ {/* Metric cards */}
+
+ {metrics.map((m, i) => (
+
+
+ {m.icon}
+
+ {m.id === 'stars' ? : m.value}
+
+ {m.label}
+
+
+ ))}
+
+
+ {/* Integrations */}
+
+
+ Integrates with
+
+
+
+
+ {integrations.map((item, i) => (
+
+
+
+ {item.name}
+
+
+ {item.detail}
+
+
+
+ ))}
+
+
+
+ );
+}
diff --git a/website/src/components/home/VscodeSection.tsx b/website/src/components/home/VscodeSection.tsx
index ea4226a5..c8b912b7 100644
--- a/website/src/components/home/VscodeSection.tsx
+++ b/website/src/components/home/VscodeSection.tsx
@@ -1,6 +1,4 @@
-'use client';
-
-import { FadeIn } from '@/components/ui/FadeIn';
+import { FadeInCSS } from '@/components/ui/FadeInCSS';
import { GlassCard } from '@/components/ui/GlassCard';
import { Button } from '@/components/ui/Button';
@@ -11,7 +9,7 @@ export function VscodeSection() {
{/* Left: Copy */}
-
+
IDE Integration
@@ -28,12 +26,12 @@ export function VscodeSection() {
Install Extension
-
+
{/* Right: VS Code Mockup */}
-
+
{/* Title bar */}
@@ -102,7 +100,7 @@ export function VscodeSection() {
0 issues
-
+
diff --git a/website/src/components/layout/Footer.tsx b/website/src/components/layout/Footer.tsx
index d654d6ee..ef6cfb89 100644
--- a/website/src/components/layout/Footer.tsx
+++ b/website/src/components/layout/Footer.tsx
@@ -14,6 +14,7 @@ const FOOTER_LINKS = {
Resources: [
{ label: 'Getting Started', href: '/docs/getting-started' },
{ label: 'API Reference', href: '/docs/api-reference' },
+ { label: 'Compare Parsers', href: '/compare' },
{ label: 'Blog', href: '/blog' },
{ label: 'Changelog', href: 'https://github.com/ajitpratap0/GoSQLX/blob/main/CHANGELOG.md', external: true },
{ label: 'Privacy Policy', href: '/privacy' },
diff --git a/website/src/components/layout/Navbar.tsx b/website/src/components/layout/Navbar.tsx
index df73480c..5a7005d5 100644
--- a/website/src/components/layout/Navbar.tsx
+++ b/website/src/components/layout/Navbar.tsx
@@ -6,6 +6,8 @@ import { usePathname } from 'next/navigation';
import { motion, AnimatePresence, useScroll, useTransform } from 'framer-motion';
import { NAV_LINKS } from '@/lib/constants';
import { Button } from '@/components/ui/Button';
+import { SearchModal, useSearchShortcut } from '@/components/ui/SearchModal';
+import { ThemeToggle } from '@/components/ui/ThemeToggle';
function GitHubIcon({ className = '' }: { className?: string }) {
return (
@@ -40,10 +42,20 @@ function HamburgerIcon({ open }: { open: boolean }) {
export function Navbar() {
const [mobileOpen, setMobileOpen] = useState(false);
const pathname = usePathname();
+ const { open: searchOpen, setOpen: setSearchOpen } = useSearchShortcut();
const { scrollY } = useScroll();
+ const [isLight, setIsLight] = useState(false);
+ useEffect(() => {
+ const check = () => setIsLight(document.documentElement.classList.contains('light'));
+ check();
+ const observer = new MutationObserver(check);
+ observer.observe(document.documentElement, { attributes: true, attributeFilter: ['class'] });
+ return () => observer.disconnect();
+ }, []);
+
const bgOpacity = useTransform(scrollY, [0, 100], [0.6, 0.9]);
const blurAmount = useTransform(scrollY, [0, 100], [8, 16]);
- const bgColor = useTransform(bgOpacity, (v) => `rgba(9, 9, 11, ${v})`);
+ const bgColor = useTransform(bgOpacity, (v) => isLight ? `rgba(250, 251, 252, ${v})` : `rgba(9, 9, 11, ${v})`);
const blur = useTransform(blurAmount, (v) => `blur(${v}px)`);
// Close mobile menu on resize
@@ -53,7 +65,18 @@ export function Navbar() {
return () => window.removeEventListener('resize', handler);
}, []);
+ // Lock body scroll when mobile menu is open
+ useEffect(() => {
+ if (mobileOpen) {
+ document.body.style.overflow = 'hidden';
+ } else {
+ document.body.style.overflow = '';
+ }
+ return () => { document.body.style.overflow = ''; };
+ }, [mobileOpen]);
+
return (
+ <>
{/* Logo */}
-
+
GoSQLX
@@ -87,6 +110,18 @@ export function Navbar() {
{/* Desktop right side */}
+
+
@@ -123,7 +159,7 @@ export function Navbar() {
animate={{ height: 'auto', opacity: 1, pointerEvents: 'auto' }}
exit={{ height: 0, opacity: 0, pointerEvents: 'none' }}
transition={{ duration: 0.25, ease: 'easeInOut' }}
- className="lg:hidden overflow-hidden border-t border-white/[0.06] bg-primary/95 backdrop-blur-xl"
+ className="lg:hidden overflow-hidden border-t border-white/[0.06] bg-primary backdrop-blur-xl"
>
{NAV_LINKS.map((link) => {
@@ -150,6 +186,7 @@ export function Navbar() {
>
+
@@ -159,5 +196,7 @@ export function Navbar() {
)}
+
setSearchOpen(false)} />
+ >
);
}
diff --git a/website/src/components/ui/FadeInCSS.tsx b/website/src/components/ui/FadeInCSS.tsx
new file mode 100644
index 00000000..ad9076ef
--- /dev/null
+++ b/website/src/components/ui/FadeInCSS.tsx
@@ -0,0 +1,36 @@
+'use client';
+
+import { useRef, useEffect, useState, ReactNode } from 'react';
+
+export function FadeInCSS({ children, delay = 0, className = '' }: { children: ReactNode; delay?: number; className?: string }) {
+ const ref = useRef(null);
+ const [visible, setVisible] = useState(false);
+
+ useEffect(() => {
+ const el = ref.current;
+ if (!el) return;
+
+ const observer = new IntersectionObserver(
+ ([entry]) => {
+ if (entry.isIntersecting) {
+ setVisible(true);
+ observer.unobserve(el);
+ }
+ },
+ { threshold: 0.1 },
+ );
+
+ observer.observe(el);
+ return () => observer.disconnect();
+ }, []);
+
+ return (
+
+ {children}
+
+ );
+}
diff --git a/website/src/components/ui/GlassCard.tsx b/website/src/components/ui/GlassCard.tsx
index d3e738f7..1d3dd1e1 100644
--- a/website/src/components/ui/GlassCard.tsx
+++ b/website/src/components/ui/GlassCard.tsx
@@ -5,9 +5,10 @@ import { ReactNode } from 'react';
export function GlassCard({ children, className = '', hover = true }: { children: ReactNode; className?: string; hover?: boolean }) {
return (
+
{children}
);
diff --git a/website/src/components/ui/SearchModal.tsx b/website/src/components/ui/SearchModal.tsx
new file mode 100644
index 00000000..508cc3ff
--- /dev/null
+++ b/website/src/components/ui/SearchModal.tsx
@@ -0,0 +1,236 @@
+'use client';
+
+import { useState, useEffect, useRef, useCallback, useMemo } from 'react';
+import { useRouter } from 'next/navigation';
+import Fuse from 'fuse.js';
+import { buildSearchIndex, type SearchEntry } from '@/lib/search-index';
+
+interface SearchModalProps {
+ open: boolean;
+ onClose: () => void;
+}
+
+export function SearchModal({ open, onClose }: SearchModalProps) {
+ const [query, setQuery] = useState('');
+ const [selectedIndex, setSelectedIndex] = useState(0);
+ const inputRef = useRef(null);
+ const overlayRef = useRef(null);
+ const listRef = useRef(null);
+ const router = useRouter();
+
+ const index = useMemo(() => buildSearchIndex(), []);
+
+ const fuse = useMemo(
+ () =>
+ new Fuse(index, {
+ keys: [
+ { name: 'title', weight: 0.5 },
+ { name: 'category', weight: 0.3 },
+ { name: 'description', weight: 0.2 },
+ ],
+ threshold: 0.4,
+ includeMatches: true,
+ }),
+ [index],
+ );
+
+ const results = useMemo(() => {
+ if (!query.trim()) return index.slice(0, 10);
+ return fuse.search(query, { limit: 10 }).map((r) => r.item);
+ }, [query, fuse, index]);
+
+ // Reset state when modal opens
+ useEffect(() => {
+ if (open) {
+ setQuery('');
+ setSelectedIndex(0);
+ // Small delay so the DOM is painted before we focus
+ requestAnimationFrame(() => inputRef.current?.focus());
+ }
+ }, [open]);
+
+ // Scroll selected item into view
+ useEffect(() => {
+ if (!listRef.current) return;
+ const selected = listRef.current.children[selectedIndex] as HTMLElement | undefined;
+ selected?.scrollIntoView({ block: 'nearest' });
+ }, [selectedIndex]);
+
+ const navigate = useCallback(
+ (entry: SearchEntry) => {
+ onClose();
+ router.push(`/docs/${entry.slug}`);
+ },
+ [onClose, router],
+ );
+
+ const handleKeyDown = useCallback(
+ (e: React.KeyboardEvent) => {
+ switch (e.key) {
+ case 'ArrowDown':
+ e.preventDefault();
+ setSelectedIndex((i) => (i + 1) % results.length);
+ break;
+ case 'ArrowUp':
+ e.preventDefault();
+ setSelectedIndex((i) => (i - 1 + results.length) % results.length);
+ break;
+ case 'Enter':
+ e.preventDefault();
+ if (results[selectedIndex]) navigate(results[selectedIndex]);
+ break;
+ case 'Escape':
+ e.preventDefault();
+ onClose();
+ break;
+ }
+ },
+ [results, selectedIndex, navigate, onClose],
+ );
+
+ // Reset selection when results change
+ useEffect(() => {
+ setSelectedIndex(0);
+ }, [results]);
+
+ if (!open) return null;
+
+ return (
+ {
+ if (e.target === overlayRef.current) onClose();
+ }}
+ role="dialog"
+ aria-modal="true"
+ aria-label="Search documentation"
+ >
+ {/* Backdrop */}
+
+
+ {/* Modal */}
+
+ {/* Search input */}
+
+
+
setQuery(e.target.value)}
+ onKeyDown={handleKeyDown}
+ placeholder="Search documentation..."
+ className="flex-1 bg-transparent py-3.5 text-sm text-white placeholder:text-zinc-500 outline-none"
+ aria-label="Search documentation"
+ autoComplete="off"
+ spellCheck={false}
+ />
+
+ ESC
+
+
+
+ {/* Results */}
+
+ {results.length === 0 && (
+ -
+ No results found for “{query}”
+
+ )}
+ {results.map((entry, i) => (
+ - navigate(entry)}
+ onMouseEnter={() => setSelectedIndex(i)}
+ >
+ {/* Icon */}
+
+
+
{entry.title}
+
{entry.category}
+
+ {i === selectedIndex && (
+
+ Enter
+
+ )}
+
+ ))}
+
+
+ {/* Footer */}
+
+
+ ↑↓
+ navigate
+
+
+ Enter
+ open
+
+
+ Esc
+ close
+
+
+
+
+ );
+}
+
+/**
+ * Hook that manages Cmd+K / Ctrl+K global shortcut for opening search.
+ */
+export function useSearchShortcut() {
+ const [open, setOpen] = useState(false);
+
+ useEffect(() => {
+ function handleKeyDown(e: KeyboardEvent) {
+ if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
+ e.preventDefault();
+ setOpen((prev) => !prev);
+ }
+ }
+ window.addEventListener('keydown', handleKeyDown);
+ return () => window.removeEventListener('keydown', handleKeyDown);
+ }, []);
+
+ return { open, setOpen };
+}
diff --git a/website/src/components/ui/ThemeToggle.tsx b/website/src/components/ui/ThemeToggle.tsx
new file mode 100644
index 00000000..835bfc90
--- /dev/null
+++ b/website/src/components/ui/ThemeToggle.tsx
@@ -0,0 +1,81 @@
+'use client';
+
+import { useState, useEffect } from 'react';
+
+function SunIcon({ className = '' }: { className?: string }) {
+ return (
+
+ );
+}
+
+function MoonIcon({ className = '' }: { className?: string }) {
+ return (
+
+ );
+}
+
+export function ThemeToggle() {
+ const [theme, setTheme] = useState<'dark' | 'light'>('dark');
+ const [mounted, setMounted] = useState(false);
+
+ useEffect(() => {
+ setMounted(true);
+ const saved = localStorage.getItem('theme');
+ if (saved === 'light' || saved === 'dark') {
+ setTheme(saved);
+ } else if (window.matchMedia('(prefers-color-scheme: light)').matches) {
+ setTheme('light');
+ } else {
+ setTheme('dark');
+ }
+ }, []);
+
+ const toggle = () => {
+ const next = theme === 'dark' ? 'light' : 'dark';
+ setTheme(next);
+ localStorage.setItem('theme', next);
+ const html = document.documentElement;
+ if (next === 'light') {
+ html.classList.add('light');
+ html.classList.remove('dark');
+ } else {
+ html.classList.add('dark');
+ html.classList.remove('light');
+ }
+ };
+
+ if (!mounted) {
+ return (
+
+ );
+ }
+
+ return (
+
+ );
+}
diff --git a/website/src/lib/constants.ts b/website/src/lib/constants.ts
index 005a03a3..ae64e32f 100644
--- a/website/src/lib/constants.ts
+++ b/website/src/lib/constants.ts
@@ -4,6 +4,7 @@ export const NAV_LINKS = [
{ href: '/blog', label: 'Changelog' },
{ href: '/vscode', label: 'VS Code' },
{ href: '/benchmarks', label: 'Benchmarks' },
+ { href: '/compare', label: 'Compare' },
];
export const FEATURES = [
diff --git a/website/src/lib/fonts.ts b/website/src/lib/fonts.ts
index 7f8097f6..256a432e 100644
--- a/website/src/lib/fonts.ts
+++ b/website/src/lib/fonts.ts
@@ -1,8 +1,15 @@
-import { Instrument_Sans, JetBrains_Mono } from 'next/font/google';
+import { Space_Grotesk, Inter, JetBrains_Mono } from 'next/font/google';
-export const instrumentSans = Instrument_Sans({
+export const spaceGrotesk = Space_Grotesk({
subsets: ['latin'],
- variable: '--font-instrument',
+ variable: '--font-heading',
+ display: 'swap',
+ weight: ['400', '500', '600', '700'],
+});
+
+export const inter = Inter({
+ subsets: ['latin'],
+ variable: '--font-body',
display: 'swap',
});
diff --git a/website/src/lib/search-index.ts b/website/src/lib/search-index.ts
new file mode 100644
index 00000000..b4cd2506
--- /dev/null
+++ b/website/src/lib/search-index.ts
@@ -0,0 +1,32 @@
+import { DOCS_SIDEBAR } from './constants';
+
+export interface SearchEntry {
+ slug: string;
+ title: string;
+ category: string;
+ description: string;
+ headings: string[];
+}
+
+/**
+ * Build a flat array of all docs metadata for Fuse.js indexing.
+ * This runs client-side on first search using the sidebar config
+ * (no fs access needed).
+ */
+export function buildSearchIndex(): SearchEntry[] {
+ const entries: SearchEntry[] = [];
+
+ for (const group of DOCS_SIDEBAR) {
+ for (const item of group.items) {
+ entries.push({
+ slug: item.slug,
+ title: item.label,
+ category: group.category,
+ description: `${group.category} - ${item.label}`,
+ headings: [],
+ });
+ }
+ }
+
+ return entries;
+}