melina

star 0

Bun-native web framework with file routing. Use this for building server-rendered pages with client mount scripts for interactivity.

7flash By 7flash schedule Updated 2/23/2026

name: melina description: Bun-native web framework with file routing. Use this for building server-rendered pages with client mount scripts for interactivity.

Melina.js — Framework Skill

Melina.js is a Bun-native web framework. Pages are server-rendered JSX, client interactivity is added via mount scripts.client.tsx files compiled to lightweight VNodes with a ~2KB reconciler runtime.

Current version: 2.3.1


1. When to Use Melina

Use Melina when the user wants:

  • Server-rendered HTML pages with file-based routing (Next.js App Router style)
  • Client-side interactivity WITHOUT a full SPA framework (no React on the client)
  • CSS styling (Tailwind CSS v4 or vanilla CSS)
  • API endpoints alongside pages
  • View Transitions for app-like navigation
  • Lightweight client bundles (zero React, zero framework overhead)
  • Static Site Generation (SSG) for pre-rendered pages

Melina replaces the full Next.js / Vite stack — server pages, API routes, SSG, and client interactivity — with zero React on the client.


2. Project Setup

New Project (from scratch)

mkdir my-app && cd my-app
bun init -y
bun add melina

Create server.ts:

import { start } from 'melina';
import path from 'path';

const appDir = path.join(import.meta.dir, 'app');

await start({
    port: parseInt(process.env.BUN_PORT || "3000"),
    appDir,
    defaultTitle: 'My App',
});

Create tsconfig.json:

{
    "compilerOptions": {
        "jsx": "react-jsx",
        "jsxImportSource": "melina/client",
        "target": "ESNext",
        "module": "ESNext",
        "moduleResolution": "bundler",
        "strict": true,
        "noEmit": true,
        "skipLibCheck": true
    },
    "include": ["app/**/*", "server.ts"]
}

Critical: "jsxImportSource": "melina/client" is required. This tells TypeScript and Bun to use Melina's VDOM runtime for JSX instead of React.

Within the Melina.js monorepo

When building examples inside the melina.js repo, import from local source:

// server.ts (inside examples/)
import { start } from '../../src/web';

And use paths in tsconfig.json:

{
    "compilerOptions": {
        "paths": {
            "melina/client/*": ["../../src/client/*"],
            "melina/*": ["../../src/*"]
        }
    }
}

3. File Structure & Routing

my-app/
├── app/
│   ├── layout.tsx              # Root layout (wraps all pages)
│   ├── layout.client.tsx       # Persistent client mount script
│   ├── globals.css             # Global styles (auto-discovered)
│   ├── page.tsx                # Home page (/)
│   ├── page.client.tsx         # Home page mount script
│   ├── page.css                # Scoped CSS for home page
│   ├── middleware.ts           # Root middleware
│   ├── error.tsx               # Error boundary
│   ├── about/
│   │   └── page.tsx            # /about
│   ├── post/[id]/
│   │   ├── page.tsx            # /post/:id (dynamic route)
│   │   └── page.client.tsx     # Client interactivity for post
│   └── api/
│       └── messages/
│           └── route.ts        # API: GET/POST /api/messages
├── server.ts
├── tsconfig.json
└── package.json

Routing Rules

File Pattern URL Type
app/page.tsx / Page
app/about/page.tsx /about Page
app/post/[id]/page.tsx /post/:id Dynamic page
app/api/messages/route.ts /api/messages API route
app/layout.tsx Layout (wraps children)
app/middleware.ts Runs before page render
app/error.tsx Catches render errors

Auto-discovered files

  • globals.css, global.css, or app.css → processed with PostCSS + Tailwind v4
  • page.css or style.css → scoped CSS for that route segment
  • layout.tsx → layout wrapping child pages (nested layouts compose automatically)
  • layout.client.tsx → persistent client mount (survives navigations)
  • page.client.tsx → per-page transient client mount
  • middleware.ts → runs root→leaf before page render
  • error.tsx → catches render errors with full layout chrome

4. Server Pages (page.tsx)

Pages run on the server. They can access databases, read files, anything server-side. They return JSX rendered to HTML via renderToString.

// app/page.tsx
export default function Page() {
    return (
        <div>
            <h1>Welcome</h1>
            <p>Server-rendered at {new Date().toISOString()}</p>
            <div id="app-root" />
        </div>
    );
}

Important patterns:

  • Place id="" attributes on containers that client scripts will target
  • Server pages render HTML — client scripts add interactivity

5. Layouts (layout.tsx)

Root layout wraps all pages. Must include {children}.

// app/layout.tsx
export default function RootLayout({ children }: { children: any }) {
    return (
        <html lang="en">
            <head>
                <meta charSet="utf-8" />
                <meta name="viewport" content="width=device-width, initial-scale=1" />
                <title>My App</title>
            </head>
            <body>
                <nav><a href="/">Home</a> | <a href="/about">About</a></nav>
                <main id="melina-page-content">{children}</main>
            </body>
        </html>
    );
}

Key points:

  • {children} is where the current page renders
  • id="melina-page-content" enables targeted page swaps during navigation
  • Nested layouts compose automatically — place layout.tsx in any subdirectory

6. <Head> Component

Declarative per-page <head> management:

// app/features/about/page.tsx
import { Head } from 'melina/web';

export default function AboutPage() {
    return (
        <>
            <Head>
                <title>About Us — My App</title>
                <meta name="description" content="Learn about our team" />
                <link rel="canonical" href="https://example.com/about" />
            </Head>
            <main><h1>About Us</h1></main>
        </>
    );
}

Head elements are collected during SSR and injected into <head>.


7. Client Architecture (page.client.tsx)

Melina uses a pure VDOM architecture for client interactivity. No hooks, no signals. Call render(vnode, container) to update the UI.

Mount Script Pattern

// app/counter/page.client.tsx
import { render } from 'melina/client';

function Counter({ count, onIncrement }: { count: number; onIncrement: () => void }) {
    return (
        <div>
            <span>Count: {count}</span>
            <button onClick={onIncrement}>+1</button>
        </div>
    );
}

export default function mount() {
    const root = document.getElementById('counter-root');
    if (!root) return;

    let count = 0;
    const update = () => {
        render(<Counter count={count} onIncrement={() => { count++; update(); }} />, root);
    };
    update();

    // Cleanup — called when navigating away
    return () => render(null, root);
}

Mount Script Lifecycle

File Lifecycle Use Case
page.client.tsx Mounts on page load, unmounts on navigate away Page-specific interactions
layout.client.tsx Mounts once, survives across navigations Global widgets, persistent state

Both must export default function mount():

  • Called when the script enters the DOM
  • Return value is a cleanup function, called on unmount
  • Uses render(vnode, container) to mount/update content

Pluggable Reconciler

Three diffing strategies:

  1. Keyed diff — O(n log n) via LIS for list reorders
  2. Sequential diff — O(n) linear patch for static layouts
  3. Replace — Full replace (baseline)
import { setReconciler } from 'melina/client';

setReconciler('auto');        // Default — inspects children for keys
setReconciler('keyed');       // Best for dynamic lists
setReconciler('sequential');  // Best for static layouts
Use Case Strategy
Dynamic lists with reorder keyed
Static forms, fixed layouts sequential
Large lists (1000+) keyed
Not sure auto (default)

8. API Routes (route.ts)

Export named HTTP method handlers:

// app/api/messages/route.ts
export async function GET(req: Request) {
    return Response.json([{ id: 1, text: 'Hello' }]);
}

export async function POST(req: Request) {
    const body = await req.json();
    return Response.json({ ok: true });
}

Streaming (SSE)

Return an AsyncGenerator for Server-Sent Events:

// app/api/stream/route.ts
export async function* GET(req: Request) {
    for (let i = 0; i < 10; i++) {
        yield `data: ${JSON.stringify({ count: i })}\n\n`;
        await new Promise(r => setTimeout(r, 1000));
    }
}

9. Middleware (middleware.ts)

Middleware functions run before page rendering, root→leaf:

// app/middleware.ts
export default async function middleware(req: Request) {
    const token = req.headers.get('authorization');
    if (!token) {
        return new Response('Unauthorized', { status: 401 });
    }
    // Return nothing to continue to the page
}

10. Error Boundaries (error.tsx)

Catches render errors and displays them with full layout chrome:

// app/error.tsx
export default function ErrorPage({ error }: { error: { message: string; stack?: string } }) {
    return (
        <div style={{ padding: '40px', color: '#ef4444' }}>
            <h1>Something went wrong</h1>
            <pre>{error.message}</pre>
        </div>
    );
}

11. SSG (Static Site Generation)

Pre-render pages at startup, serve from memory:

// app/pricing/page.tsx
export const ssg = true;

export default function PricingPage() {
    return <main><h1>Pricing</h1></main>;
}

With TTL (time-to-live):

export const ssg = { revalidate: 60 }; // re-render after 60 seconds

SSG API:

import { getPrerendered, setPrerendered, clearSSGCache } from 'melina/server';

// Check if a page is cached
const html = getPrerendered('/pricing');

// Clear cache (e.g., after content update)
clearSSGCache('/pricing');

12. Styling

Tailwind CSS v4

Create app/globals.css:

@import "tailwindcss";

@theme {
    --color-background: #0a0a0f;
    --color-surface: #111118;
    --color-foreground: #e4e4e7;
    --color-accent: #6366f1;
}

Melina auto-discovers globals.css, global.css, or app.css.

Scoped CSS

Place page.css or style.css alongside a page — auto-injected for that route only.

Vanilla CSS

You don't need Tailwind — any CSS works. Just use standard stylesheets.


13. Package Exports

Import Path Module Use Case
melina src/web.ts Server: start, serve, createAppRouter
melina/web src/web.ts Same as above
melina/server src/server/index.ts Server: renderToString, Head, SSG, build helpers
melina/server/ssr src/server/ssr.ts Server: renderToString only
melina/client src/client/index.ts Client: render, createElement, setReconciler, navigate, Link
melina/client/render src/client/render.ts Client: same as above (direct)
melina/client/types src/client/types.ts Types: VNode, Props, Component
melina/client/reconcilers src/client/reconcilers/index.ts Reconciler strategies
melina/client/jsx-runtime src/client/jsx-runtime.ts JSX transform (auto, don't import manually)
melina/client/jsx-dom src/client/jsx-dom.ts Real DOM JSX factory

Architecture: SSR vs Client separation

  • Client bundles (src/client/) are bundled for the browser — no server code
  • SSR (renderToString) lives at src/server/ssr.ts — never in client bundles
  • Server imports from melina/server or melina/web
  • Client mount scripts import from melina/client

14. Server API Reference

start(options) — Quick start

import { start } from 'melina';

await start({
    appDir: './app',                    // default: './app'
    port: 3000,                         // default: 3000 or BUN_PORT env
    defaultTitle: 'My App',
});

serve(handler, options) + createAppRouter(options) — Advanced

import { serve, createAppRouter } from 'melina';

const router = createAppRouter({
    appDir: './app',
    defaultTitle: 'My App',
    globalCss: './app/globals.css',
});

serve(router, { port: 3000 });

Build Helpers

import { buildScript, buildStyle, buildAsset } from 'melina/server';

const scriptUrl = await buildScript('./src/app.ts');     // JS/TS → hashed URL
const cssUrl = await buildStyle('./src/style.css');       // CSS → processed URL
const imgUrl = await buildAsset(Bun.file('./icon.png')); // Static → hashed URL

15. Common Patterns

Event Delegation for Lists

For dynamic lists, use data-* attributes with delegated event handlers:

// In page.client.tsx
document.addEventListener('click', (e) => {
    const btn = (e.target as HTMLElement).closest('[data-item-id]');
    if (btn) {
        const id = (btn as HTMLElement).dataset.itemId;
        selectItem(id);
    }
});

// In component
function Item({ item }: { item: { id: string; name: string } }) {
    return <button data-item-id={item.id}>{item.name}</button>;
}

Persistent Floating Widget

// layout.client.tsx — survives across navigations
import { render } from 'melina/client';

export default function mount() {
    const root = document.getElementById('widget-root');
    if (!root) return;

    let isOpen = false;
    const update = () => render(
        <div>
            {isOpen && <div className="widget-panel">Widget content</div>}
            <button onClick={() => { isOpen = !isOpen; update(); }}>
                {isOpen ? '✕' : '?'}
            </button>
        </div>,
        root
    );
    update();
    return () => render(null, root);
}

Client-Side Navigation

import { navigate } from 'melina/client';

// Programmatic navigation with View Transitions
navigate('/dashboard');

16. Gotchas & Troubleshooting

async Server Components

Melina's renderToString does NOT support async server components.

  • Wrong: export default async function Page() { ... }
  • Right: export default function Page() { ... }

If you need async data, fetch it before the component renders and pass it as props.

❌ Missing mount() export

Client scripts MUST export default function mount(). Without this, no interactivity.

❌ CSS 404

Melina auto-maps app/globals.css/globals.css. Don't use custom paths in <link> tags.

❌ Ghost Processes (EADDRINUSE)

On Windows, stale Bun processes may hold ports:

netstat -ano | findstr ":3000"
taskkill /F /PID <PID>

❌ Stale Dependencies

After switching package versions or linking locally:

Remove-Item -Recurse -Force node_modules, bun.lock; bun install
Install via CLI
npx skills add https://github.com/7flash/my-agent-skills --skill melina
Repository Details
star Stars 0
call_split Forks 0
navigation Branch main
article Path SKILL.md
More from Creator