A Bun-native, HTML-first framework that renders real pages, treats UI frameworks as renderers, and makes SEO the default—not a feature.
Bun Runtime SSR Only SEO Guaranteed
KilatJS is a server-first web framework inspired by the simplicity of PHP and the power of modern component libraries. It rejects the complexity of SPAs, hydration, and client-side routing in favor of real HTML rendered on the server.
"Request → Load → Render HTML → Response. That's it."
The easiest way to start is using the KilatJS CLI:
# Create a new project in my-app folder bunx kilat create my-app cd my-app bun install bun run dev
Visit http://localhost:3000 🎉
| Command | Description |
|---|---|
kilat create [dir] |
Create a new project from template |
kilat dev |
Start development server with HMR + Live Reload |
kilat build |
Build for production |
kilat serve |
Run production server (bun dist/server.js) |
kilat preview |
Preview build output |
⚡ KilatJS Dev Server
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
➜ http://localhost:3000
➜ HMR + Live Reload enabled
➜ Edit files and see changes instantly
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
KilatJS uses a simplified, root-level structure:
my-app/
├── public/ # Static assets & index.html
├── routes/ # SSR routes (index.tsx, about.tsx)
├── components/ # React/Kilat components
├── kilat.config.ts # Configuration
├── input.css # Tailwind input
├── index.client.tsx # (Optional) React client entry
├── App.tsx # (Optional) Client-side app
└── dist/ # Production build
While KilatJS is HTML-first, you can easily add a full React client-side application for complex interactive features.
- Auto-detection: If
index.client.tsxexists in your root, KilatJS will automatically bundle it. - Setup: Create a
public/index.htmlwith a mount point:
<div id="root"></div> <script type="module" src="/client/index.client.js"></script>
- Route: Define
clientRoute: "/client"inkilat.config.tsto host your SPA.
Each route file can export:
// SEO meta tags export const meta = { title: "Page Title", description: "Page description", robots: "index,follow", ogTitle: "Open Graph Title", ogDescription: "OG description", ogImage: "https://example.com/image.jpg", }; // Server-side data loading export async function load(ctx) { const data = await fetchData(); return { items: data }; } // HTTP method handlers (POST, PUT, DELETE, etc.) export async function POST(ctx) { const formData = await ctx.request.formData(); // Process form... return Response.redirect("/success", 302); } // Page component (receives data from load()) export default function Page({ data, params, state }) { return ( <div> {data.items.map((item) => ( <p>{item.name}</p> ))} </div> ); }
src/components/Layout.tsx:
interface LayoutProps { children: React.ReactNode; title?: string; } export function Layout({ children, title = "My Blog" }: LayoutProps) { return ( <div className="min-h-screen bg-gray-50"> <header className="bg-white shadow"> <nav className="max-w-4xl mx-auto px-4 py-4"> <a href="/" className="text-xl font-bold"> My Blog </a> <div className="float-right space-x-4"> <a href="/" className="text-gray-600 hover:text-gray-900"> Home </a> <a href="/blog" className="text-gray-600 hover:text-gray-900"> Blog </a> <a href="/about" className="text-gray-600 hover:text-gray-900"> About </a> </div> </nav> </header> <main className="max-w-4xl mx-auto px-4 py-8">{children}</main> </div> ); }
src/routes/blog/index.tsx:
import { Layout } from "../../components/Layout"; export const meta = { title: "Blog - My Site", description: "Read our latest articles", }; export async function load() { // In real app, fetch from database const posts = [ { id: 1, slug: "hello-world", title: "Hello World", excerpt: "My first post...", }, { id: 2, slug: "getting-started", title: "Getting Started", excerpt: "Learn how to...", }, ]; return { posts }; } export default function BlogPage({ data }) { return ( <Layout> <h1 className="text-3xl font-bold mb-8">Blog</h1> <div className="space-y-6"> {data.posts.map((post) => ( <article key={post.id} className="bg-white p-6 rounded-lg shadow"> <h2 className="text-xl font-semibold"> <a href={`/blog/${post.slug}`} className="hover:text-blue-600"> {post.title} </a> </h2> <p className="text-gray-600 mt-2">{post.excerpt}</p> </article> ))} </div> </Layout> ); }
src/routes/blog/[slug].tsx:
import { Layout } from "../../components/Layout"; export const meta = { title: "Blog Post", description: "Read this article", }; export async function load({ params }) { // params.slug contains the URL parameter const post = await getPostBySlug(params.slug); if (!post) { throw new Response("Not Found", { status: 404 }); } return { post }; } async function getPostBySlug(slug: string) { // In real app, fetch from database const posts = { "hello-world": { title: "Hello World", content: "This is my first post!" }, "getting-started": { title: "Getting Started", content: "Let me show you how...", }, }; return posts[slug] || null; } export default function BlogPostPage({ data, params }) { return ( <Layout> <article> <h1 className="text-4xl font-bold mb-4">{data.post.title}</h1> <div className="prose max-w-none">{data.post.content}</div> </article> <a href="/blog" className="text-blue-600 mt-8 inline-block"> ← Back to Blog </a> </Layout> ); }
src/routes/api/posts.ts:
import { RouteContext } from "kilatjs"; const posts = [ { id: 1, title: "Hello World", slug: "hello-world" }, { id: 2, title: "Getting Started", slug: "getting-started" }, ]; // GET /api/posts export async function GET(ctx: RouteContext) { const search = ctx.query.get("search")?.toLowerCase(); let results = posts; if (search) { results = posts.filter((p) => p.title.toLowerCase().includes(search)); } return new Response(JSON.stringify({ data: results }), { headers: { "Content-Type": "application/json" }, }); } // POST /api/posts export async function POST(ctx: RouteContext) { const body = await ctx.request.json(); const newPost = { id: posts.length + 1, title: body.title, slug: body.title.toLowerCase().replace(/\s+/g, "-"), }; posts.push(newPost); return new Response(JSON.stringify({ data: newPost }), { status: 201, headers: { "Content-Type": "application/json" }, }); } /** * NEW: Standalone API Mode with Swagger Documentation * KilatJS provides a dedicated core for building pure APIs. */ // example/server.ts import KilatJS, { swagger, RouteContext } from "kilatjs/core"; const app = new KilatJS(); // Register Swagger documentation app.use( swagger(app, { path: "/docs", title: "KilatJS API", description: "Automatic API documentation", }) ); // Route with metadata for Swagger app.post( "/api/echo", { summary: "Echo request", body: { text: "string" }, // Optional: for documentation response: { echo: {}, receivedAt: "string" }, // Optional: for documentation }, async (ctx: RouteContext) => { const body = await ctx.request.json(); return { echo: body, receivedAt: new Date() }; } ); app.listen(4000);
src/routes/contact.tsx:
import { Layout } from "../components/Layout"; export const meta = { title: "Contact Us", description: "Get in touch with us", }; // Handle form submission export async function POST({ request }) { const formData = await request.formData(); const name = formData.get("name"); const email = formData.get("email"); const message = formData.get("message"); // Save to database, send email, etc. console.log("Contact form:", { name, email, message }); // PRG: Post-Redirect-Get pattern return Response.redirect("/contact/success", 302); } export default function ContactPage() { return ( <Layout> <h1 className="text-3xl font-bold mb-8">Contact Us</h1> <form method="POST" className="max-w-md space-y-4"> <div> <label className="block text-sm font-medium mb-1">Name</label> <input type="text" name="name" required className="w-full px-3 py-2 border rounded-lg" /> </div> <div> <label className="block text-sm font-medium mb-1">Email</label> <input type="email" name="email" required className="w-full px-3 py-2 border rounded-lg" /> </div> <div> <label className="block text-sm font-medium mb-1">Message</label> <textarea name="message" rows={4} required className="w-full px-3 py-2 border rounded-lg" /> </div> <button type="submit" className="bg-blue-600 text-white px-6 py-2 rounded-lg hover:bg-blue-700" > Send Message </button> </form> </Layout> ); }
src/routes/contact/success.tsx:
import { Layout } from "../../components/Layout"; export const meta = { title: "Message Sent", }; export default function ContactSuccessPage() { return ( <Layout> <div className="text-center py-12"> <h1 className="text-3xl font-bold text-green-600 mb-4"> ✓ Message Sent! </h1> <p className="text-gray-600 mb-8"> Thank you for contacting us. We'll get back to you soon. </p> <a href="/" className="text-blue-600 hover:underline"> ← Back to Home </a> </div> </Layout> ); }
// src/routes/dashboard.tsx export async function load({ request }) { const cookie = request.headers.get("cookie"); const session = await getSession(cookie); if (!session) { // Redirect to login if not authenticated throw Response.redirect("/login", 302); } return { user: session.user }; } export default function DashboardPage({ data }) { return ( <div> <h1>Welcome, {data.user.name}!</h1> <form method="POST" action="/logout"> <button type="submit">Logout</button> </form> </div> ); }
// kilat.config.ts import { defineConfig } from "kilatjs"; export default defineConfig({ appDir: "./src", // Source directory (auto-detects routes/ or pages/) outDir: "./dist", // Production build output port: 3000, // Server port hostname: "localhost", // Server hostname publicDir: "./public", // Static assets directory tailwind: { enabled: true, // Enable Tailwind CSS inputPath: "./input.css", cssPath: "./styles.css", }, });
KilatJS leverages Bun's runtime directly:
- ⚡ Bun.FileSystemRouter - Zero-config file-based routing
- 🚀 Bun.serve() - HTTP server with WebSocket support
- 📦 Bun.build() - Native TypeScript/JSX bundling
- 💾 Bun.file() - Streaming file operations
- 🔍 Bun.Glob - Pattern matching for routes
Performance:
- 3x faster cold starts vs Node.js
- Native TypeScript - no transpilation
- Zero-copy streaming for static files
- HTML is the protocol — Every response is complete, semantic HTML
- Server owns all truth — No client-side state management
- Every change = HTTP request — Forms, mutations, navigation
- UI frameworks render only — React for templating, not orchestration
- JavaScript is optional — Sites work without JS
KilatJS is server-first, but you can add client-side interactivity using these patterns:
Export a function that runs in the browser. TypeScript/LSP fully supports it!
// src/routes/counter.tsx // Client-side script - auto-injected, LSP checks the code! export function clientScript() { let count = 0; const countEl = document.getElementById("count")!; document.getElementById("decrement")!.onclick = () => { countEl.textContent = String(--count); }; document.getElementById("increment")!.onclick = () => { countEl.textContent = String(++count); }; } export default function CounterPage() { return ( <div> <button id="decrement">-</button> <span id="count">0</span> <button id="increment">+</button> </div> ); }
The framework automatically injects the script - no manual <script> tags needed!
Lightweight reactivity with declarative HTML attributes:
export default function AlpinePage() { return ( <div x-data="{ count: 0 }"> <button x-on:click="count--">-</button> <span x-text="count">0</span> <button x-on:click="count++">+</button> </div> ); }
Server-driven UI updates without JavaScript:
<!-- Can use .html files directly in routes/ --> <button hx-get="/api/greeting" hx-target="#result" hx-swap="innerHTML"> Load </button> <div id="result"></div>
KilatJS supports .html files as routes - perfect for HTMX pages!
Best for mutations - no JS required:
<form method="POST" action="/contact"> <input name="email" type="email" /> <button type="submit">Submit</button> </form>
// kilat.config.ts import { defineConfig } from "kilatjs"; export default defineConfig({ appDir: ".", // Root directory for routes/ and components/ outDir: "./dist", // Production build output port: 3000, // Server port publicDir: "./public", // Static assets directory (auto-detects index.html) clientRoute: "/client", // (Optional) Path for React client SPA tailwind: { enabled: true, // Enable Tailwind CSS inputPath: "./input.css", cssPath: "./public/styles.css", }, });
KilatJS uniquely allows you to mix HTML-First SSR with Full React CSR:
- Use SSR for public pages, SEO, blogs, and landing pages.
- Use CSR for complex dashboards, interactive editors, or apps requiring shared state.
Note: KilatJS keeps these worlds separate. SSR pages are pure HTML (no React runtime), while CSR pages are full React apps. This gives you the speed of a static site with the power of a modern app.
KilatJS provides a dedicated core for building pure JSON APIs with automatic documentation.
- Automatic Swagger Documentation: Use the
swaggermiddleware to generate interactive documentation. - Typed Routes: Use
RouteOptionsto provide metadata, request bodies, and response structures. - Core API: Use the lightweight
KilatJScore for JSON-first services.
import KilatJS, { swagger, RouteContext } from "kilatjs/core"; const app = new KilatJS(); app.use( swagger(app, { path: "/docs", title: "My API Documentation", }) ); app.get( "/api/users", { summary: "List users", response: { users: [] }, }, (ctx) => { return { users: ["Alice", "Bob"] }; } ); app.listen(4000);
MIT
Built with 💜 for the real web using Bun.