Rebuild hatch.surf with Next.js
Some checks failed
Deploy hatch.surf / deploy (push) Failing after 0s

- Recreated old website with Next.js 16 + Tailwind CSS
- Hero section with terminal animation
- Features section (Capture, Inspect, Mock)
- Why section (Compliance, Cost, Speed)
- CTA section
- Header and Footer components
- Dark theme with Inter + JetBrains Mono fonts
- Static export for Docker deployment

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
Chris Anderson 2026-06-24 10:36:40 +02:00
parent 28ef0685b6
commit 91c6ed94f1
11 changed files with 363 additions and 89 deletions

View File

@ -1,26 +1,69 @@
@import "tailwindcss"; @tailwind base;
@tailwind components;
@tailwind utilities;
:root { :root {
--background: #ffffff; --surface-0: #09090b;
--foreground: #171717; --surface-1: #111114;
--surface-2: #1a1a1f;
--surface-3: #232328;
--brand: #3b82f6;
--brand-dim: #2563eb;
--text-primary: #fafafa;
--text-secondary: #a1a1aa;
--text-tertiary: #71717a;
--text-code: #e879f9;
--code-bg: #18181b;
--code-border: #27272a;
} }
@theme inline { * {
--color-background: var(--background); margin: 0;
--color-foreground: var(--foreground); padding: 0;
--font-sans: var(--font-geist-sans); box-sizing: border-box;
--font-mono: var(--font-geist-mono);
} }
@media (prefers-color-scheme: dark) { html {
:root { font-size: 16px;
--background: #0a0a0a; line-height: 1.6;
--foreground: #ededed; -webkit-font-smoothing: antialiased;
} -moz-osx-font-smoothing: grayscale;
} }
body { body {
background: var(--background); font-family: var(--font-inter), -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
color: var(--foreground); color: var(--text-primary);
font-family: Arial, Helvetica, sans-serif; background-color: var(--surface-0);
overflow-x: hidden;
}
a {
color: var(--brand);
text-decoration: none;
transition: color 150ms cubic-bezier(0.16, 1, 0.3, 1);
}
a:hover {
color: var(--text-primary);
}
code {
font-family: var(--font-mono), 'SFMono-Regular', Consolas, monospace;
font-size: 0.875em;
background-color: var(--code-bg);
border: 1px solid var(--code-border);
padding: 0.15em 0.4em;
border-radius: 6px;
color: var(--text-code);
}
/* Particle canvas */
#particle-canvas {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 0;
pointer-events: none;
} }

View File

@ -1,20 +1,45 @@
import type { Metadata } from "next"; import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google"; import { Inter, JetBrains_Mono } from "next/font/google";
import "./globals.css"; import "./globals.css";
import Header from "@/components/Header";
import Footer from "@/components/Footer";
const geistSans = Geist({ const inter = Inter({
variable: "--font-geist-sans",
subsets: ["latin"], subsets: ["latin"],
variable: "--font-inter",
}); });
const geistMono = Geist_Mono({ const jetbrainsMono = JetBrains_Mono({
variable: "--font-geist-mono",
subsets: ["latin"], subsets: ["latin"],
variable: "--font-mono",
}); });
export const metadata: Metadata = { export const metadata: Metadata = {
title: "Create Next App", title: "Hatch — Self-hostable HTTP request inspector + mocker",
description: "Generated by create next app", description: "Hatch is a self-hostable HTTP request inspector and mocker. One Go binary, SQLite, no SaaS, no per-request fee. Open source, MIT.",
openGraph: {
title: "Hatch — Self-hostable HTTP request inspector + mocker",
description: "Hatch is a self-hostable HTTP request inspector and mocker. One Go binary, SQLite, no SaaS, no per-request fee. Open source, MIT.",
url: "https://hatch.surf",
siteName: "Hatch",
images: [
{
url: "/brand/og/default.png",
width: 1200,
height: 630,
},
],
type: "website",
},
twitter: {
card: "summary_large_image",
title: "Hatch — Self-hostable HTTP request inspector + mocker",
description: "Hatch is a self-hostable HTTP request inspector and mocker. One Go binary, SQLite, no SaaS, no per-request fee. Open source, MIT.",
images: ["/brand/og/default.png"],
},
icons: {
icon: "/brand/favicon/favicon-48.png",
},
}; };
export default function RootLayout({ export default function RootLayout({
@ -23,11 +48,12 @@ export default function RootLayout({
children: React.ReactNode; children: React.ReactNode;
}>) { }>) {
return ( return (
<html <html lang="en" className={`${inter.variable} ${jetbrainsMono.variable}`}>
lang="en" <body className="min-h-screen bg-zinc-950 text-white font-sans antialiased">
className={`${geistSans.variable} ${geistMono.variable} h-full antialiased`} <Header />
> {children}
<body className="min-h-full flex flex-col">{children}</body> <Footer />
</body>
</html> </html>
); );
} }

View File

@ -1,65 +1,15 @@
import Image from "next/image"; import Hero from '@/components/Hero'
import Features from '@/components/Features'
import Why from '@/components/Why'
import CTA from '@/components/CTA'
export default function Home() { export default function Home() {
return ( return (
<div className="flex flex-col flex-1 items-center justify-center bg-zinc-50 font-sans dark:bg-black"> <main className="relative z-10">
<main className="flex flex-1 w-full max-w-3xl flex-col items-center justify-between py-32 px-16 bg-white dark:bg-black sm:items-start"> <Hero />
<Image <Features />
className="dark:invert" <Why />
src="/next.svg" <CTA />
alt="Next.js logo"
width={100}
height={20}
priority
/>
<div className="flex flex-col items-center gap-6 text-center sm:items-start sm:text-left">
<h1 className="max-w-xs text-3xl font-semibold leading-10 tracking-tight text-black dark:text-zinc-50">
To get started, edit the page.tsx file.
</h1>
<p className="max-w-md text-lg leading-8 text-zinc-600 dark:text-zinc-400">
Looking for a starting point or more instructions? Head over to{" "}
<a
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
className="font-medium text-zinc-950 dark:text-zinc-50"
>
Templates
</a>{" "}
or the{" "}
<a
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
className="font-medium text-zinc-950 dark:text-zinc-50"
>
Learning
</a>{" "}
center.
</p>
</div>
<div className="flex flex-col gap-4 text-base font-medium sm:flex-row">
<a
className="flex h-12 w-full items-center justify-center gap-2 rounded-full bg-foreground px-5 text-background transition-colors hover:bg-[#383838] dark:hover:bg-[#ccc] md:w-[158px]"
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
className="dark:invert"
src="/vercel.svg"
alt="Vercel logomark"
width={16}
height={16}
/>
Deploy Now
</a>
<a
className="flex h-12 w-full items-center justify-center rounded-full border border-solid border-black/[.08] px-5 transition-colors hover:border-transparent hover:bg-black/[.04] dark:border-white/[.145] dark:hover:bg-[#1a1a1a] md:w-[158px]"
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
Documentation
</a>
</div>
</main> </main>
</div> )
);
} }

20
components/CTA.tsx Normal file
View File

@ -0,0 +1,20 @@
export default function CTA() {
return (
<section className="py-20 px-6">
<div className="max-w-4xl mx-auto text-center">
<h2 className="text-3xl md:text-4xl font-bold mb-4">
Start inspecting in 30&nbsp;seconds
</h2>
<p className="text-xl text-zinc-400 mb-8">
One binary. Your data stays yours.
</p>
<a
href="https://github.com/elfoundation/hatch"
className="inline-flex items-center justify-center gap-2 px-8 py-4 bg-blue-500 hover:bg-blue-600 text-white font-medium rounded-lg transition-colors text-lg"
>
Get Hatch
</a>
</div>
</section>
)
}

61
components/Features.tsx Normal file
View File

@ -0,0 +1,61 @@
const features = [
{
icon: (
<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
<circle cx="12" cy="12" r="10"></circle>
<polyline points="12 6 12 12 16 14"></polyline>
</svg>
),
title: 'Capture',
description: 'Method, path, headers, query, body. Persists across restarts because the storage is SQLite on disk, not a hosted queue.',
},
{
icon: (
<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path>
<circle cx="12" cy="12" r="3"></circle>
</svg>
),
title: 'Inspect',
description: 'A live SSE feed of incoming requests. Click any captured request to see the headers, the body, the timing.',
},
{
icon: (
<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
<polyline points="16 18 22 12 16 6"></polyline>
<polyline points="8 6 2 12 8 18"></polyline>
</svg>
),
title: 'Mock',
description: 'Return a 200, a 500, or a custom JSON payload. For testing your own retry, backoff, and error-handling logic.',
},
]
export default function Features() {
return (
<section className="py-20 px-6">
<div className="max-w-4xl mx-auto">
<h2 className="text-3xl md:text-4xl font-bold text-center mb-16">
Three things, and nothing else
</h2>
<div className="grid md:grid-cols-3 gap-8">
{features.map((feature, index) => (
<div
key={feature.title}
className="p-6 rounded-xl bg-zinc-900/50 border border-zinc-800 hover:border-zinc-700 transition-colors"
>
<div className="w-12 h-12 flex items-center justify-center rounded-lg bg-blue-500/10 text-blue-400 mb-4">
{feature.icon}
</div>
<h3 className="text-xl font-semibold mb-2">{feature.title}</h3>
<p className="text-zinc-400 leading-relaxed">
{feature.description}
</p>
</div>
))}
</div>
</div>
</section>
)
}

29
components/Footer.tsx Normal file
View File

@ -0,0 +1,29 @@
export default function Footer() {
return (
<footer className="py-8 px-6 border-t border-zinc-800">
<div className="max-w-4xl mx-auto">
<div className="flex flex-col md:flex-row justify-between items-center gap-4">
<span className="text-white font-bold flex items-center gap-2">
<span className="text-blue-400"></span> Hatch
</span>
<p className="text-zinc-500 text-sm text-center md:text-right">
© 2026 El Foundation.{' '}
<a
href="https://github.com/elfoundation/hatch"
className="text-zinc-400 hover:text-white transition-colors"
>
Hatch
</a>{' '}
is released under the{' '}
<a
href="https://github.com/elfoundation/hatch/blob/main/LICENSE"
className="text-zinc-400 hover:text-white transition-colors"
>
MIT License
</a>.
</p>
</div>
</div>
</footer>
)
}

25
components/Header.tsx Normal file
View File

@ -0,0 +1,25 @@
export default function Header() {
return (
<header className="fixed top-0 left-0 right-0 z-50 py-4 bg-zinc-950/80 backdrop-blur-lg border-b border-zinc-800">
<div className="max-w-4xl mx-auto px-6 flex justify-between items-center">
<a href="/" className="text-xl font-bold text-white flex items-center gap-2 hover:text-white transition-colors">
<span className="text-blue-400"></span> Hatch
</a>
<nav className="flex items-center gap-6">
<a
href="https://github.com/elfoundation/hatch"
className="text-zinc-400 hover:text-white transition-colors"
>
GitHub
</a>
<a
href="/blog/"
className="text-zinc-400 hover:text-white transition-colors"
>
Blog
</a>
</nav>
</div>
</header>
)
}

62
components/Hero.tsx Normal file
View File

@ -0,0 +1,62 @@
export default function Hero() {
return (
<section className="pt-32 pb-20 px-6">
<div className="max-w-4xl mx-auto text-center">
<div className="inline-block mb-6 px-4 py-2 rounded-full bg-blue-500/10 border border-blue-500/20 text-blue-400 text-sm font-medium">
Open source · MIT licensed
</div>
<h1 className="text-5xl md:text-6xl font-bold tracking-tight mb-6 leading-tight">
Self-hostable HTTP request<br />
inspector&nbsp;+&nbsp;mocker
</h1>
<p className="text-xl text-zinc-400 mb-8 max-w-2xl mx-auto">
One Go binary. SQLite under the hood.
<code className="mx-1 px-2 py-0.5 bg-zinc-800 border border-zinc-700 rounded text-fuchsia-400 text-base">
docker compose up
</code>
, and you have an inspection endpoint and a live feed in under 30 seconds.
</p>
<div className="flex flex-col sm:flex-row gap-4 justify-center mb-12">
<a
href="https://github.com/elfoundation/hatch"
className="inline-flex items-center justify-center gap-2 px-6 py-3 bg-blue-500 hover:bg-blue-600 text-white font-medium rounded-lg transition-colors"
>
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M9 19c-5 1.5-5-2.5-7-3m14 6v-3.87a3.37 3.37 0 0 0-.94-2.61c3.14-.35 6.44-1.54 6.44-7A5.44 5.44 0 0 0 20 4.77 5.07 5.07 0 0 0 19.91 1S18.73.65 16 2.48a13.38 13.38 0 0 0-7 0C6.27.65 5.09 1 5.09 1A5.07 5.07 0 0 0 5 4.77a5.44 5.44 0 0 0-1.5 3.78c0 5.42 3.3 6.61 6.44 7A3.37 3.37 0 0 0 9 18.13V22"></path>
</svg>
View on GitHub
</a>
<a
href="https://github.com/elfoundation/hatch#quick-start"
className="inline-flex items-center justify-center gap-2 px-6 py-3 bg-zinc-800 hover:bg-zinc-700 text-white font-medium rounded-lg border border-zinc-700 transition-colors"
>
Quick Start
</a>
</div>
{/* Terminal */}
<div className="max-w-2xl mx-auto rounded-xl overflow-hidden border border-zinc-800 bg-zinc-900/50 backdrop-blur-sm">
<div className="flex items-center gap-2 px-4 py-3 bg-zinc-800/50 border-b border-zinc-800">
<div className="w-3 h-3 rounded-full bg-zinc-600"></div>
<div className="w-3 h-3 rounded-full bg-zinc-600"></div>
<div className="w-3 h-3 rounded-full bg-zinc-600"></div>
<span className="ml-2 text-sm text-zinc-500">terminal</span>
</div>
<pre className="p-4 text-left text-sm font-mono overflow-x-auto">
<code>
<span className="text-green-400">$</span>{' '}
<span className="text-zinc-300">docker compose up -d</span>{'\n'}
<span className="text-zinc-500"> hatch started on :8080</span>{'\n'}
<span className="text-green-400">$</span>{' '}
<span className="text-zinc-300">curl -X POST https://your-bin.hatch.surf/test -d {'{\'hello\':\'world\'}'}</span>{'\n'}
<span className="text-zinc-500"> captured view at https://your-bin.hatch.surf/inspect</span>
</code>
</pre>
</div>
</div>
</section>
)
}

55
components/Why.tsx Normal file
View File

@ -0,0 +1,55 @@
const reasons = [
{
number: '01',
title: 'Compliance and privacy',
description: 'Some teams cannot legally send webhook payloads to a hosted SaaS. Hatch keeps the data on your own network.',
},
{
number: '02',
title: 'Cost',
description: 'A hosted inspector charges per request, per seat, or per retention day. Hatch is one Go binary on a $5 VPS. There is no per-request fee because there is no one to charge it.',
},
{
number: '03',
title: 'Speed of setup',
description: (
<>
<code className="px-2 py-0.5 bg-zinc-800 border border-zinc-700 rounded text-fuchsia-400 text-sm">
docker compose up
</code>{' '}
is faster than signing up for a SaaS, verifying your email, configuring your first bin, and pasting the URL into your webhook config.
</>
),
},
]
export default function Why() {
return (
<section className="py-20 px-6 bg-zinc-900/30">
<div className="max-w-4xl mx-auto">
<h2 className="text-3xl md:text-4xl font-bold text-center mb-16">
Why a single binary, not a SaaS
</h2>
<div className="space-y-8">
{reasons.map((reason) => (
<div
key={reason.number}
className="flex gap-6 items-start"
>
<div className="text-4xl font-bold text-zinc-700 font-mono shrink-0">
{reason.number}
</div>
<div>
<h3 className="text-xl font-semibold mb-2">{reason.title}</h3>
<p className="text-zinc-400 leading-relaxed">
{reason.description}
</p>
</div>
</div>
))}
</div>
</div>
</section>
)
}

0
deploy.sh Normal file → Executable file
View File

View File

@ -2,6 +2,9 @@ import type { NextConfig } from "next";
const nextConfig: NextConfig = { const nextConfig: NextConfig = {
output: "export", output: "export",
images: {
unoptimized: true,
},
}; };
export default nextConfig; export default nextConfig;