dark

package module
v0.2.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Apr 4, 2026 License: MIT Imports: 30 Imported by: 0

README

Dark

A Go SSR web framework powered by Preact or React, htmx, and Islands architecture.

Dark renders TSX components on the server using ramune (a JS/TS runtime for Go), fetches data with Go Loader/Action functions, and delivers interactive pages through htmx's HTML-over-the-wire approach with minimal client-side JavaScript.

Requirements

Dark uses ramune for SSR, which supports two JS engine backends:

JSC (default) QuickJS (-tags quickjs)
Engine Apple JavaScriptCore via purego modernc.org/quickjs (pure Go)
JIT Yes No
Platforms macOS, Linux macOS, Linux, Windows
System deps macOS: none. Linux: apt install libjavascriptcoregtk-4.1-dev None
Best for Production performance Portability, zero-dependency deploys

Both are pure Go builds -- no C compiler or Cgo required.

Default (JavaScriptCore)
# macOS — no extra dependencies
go build .

# Linux
sudo apt install libjavascriptcoregtk-4.1-dev
go build .
QuickJS backend
go build -tags quickjs .

No shared libraries needed. Works on all platforms including Windows. Trade-off: no JIT, so JS execution is slower (SSR render time increases). For most apps where the bottleneck is I/O (database, network), this is negligible.

Built on net/http

Dark follows standard net/http conventions. There are no external router dependencies.

  • Internal routing uses http.NewServeMux with Go 1.22+ enhanced patterns (GET /users/{id})
  • app.Handler() returns (http.Handler, error) — plug it into any Go HTTP stack
  • Middleware is the standard func(http.Handler) http.Handler signature
  • Dark does not own the server — you start it yourself with http.ListenAndServe or http.Server
// Simple — MustHandler() panics on error (convenient for main)
http.ListenAndServe(":3000", app.MustHandler())

// With error handling
handler, err := app.Handler()
if err != nil {
    log.Fatal(err)
}
http.ListenAndServe(":3000", handler)

// With http.Server for full control
srv := &http.Server{
    Addr:         ":8080",
    Handler:      app.MustHandler(),
    ReadTimeout:  5 * time.Second,
    WriteTimeout: 10 * time.Second,
}
srv.ListenAndServe()

Any existing net/http middleware works with app.Use() out of the box.

Features

  • Server-side rendering — TSX templates rendered via Preact or React renderToString in a sandboxed JS runtime
  • Loader/Action pattern — Go functions for data fetching and mutations, props passed as JSON
  • htmx integration — HX-Request aware responses (full page vs HTML fragment)
  • Islands architecture — selective client-side hydration with lazy loading (load, idle, visible)
  • Streaming SSR — shell-first rendering for faster TTFB
  • Nested layouts — composable layouts via route groups
  • Form validation — field-level errors with form data preservation
  • Sessions — HMAC-signed cookie sessions with flash messages
  • AuthenticationRequireAuth middleware with htmx-aware redirects
  • Head management — per-page <title>, <meta>, and OpenGraph tags
  • API routes — JSON endpoints alongside page routes
  • Dev mode — hot reload, error overlay with source maps, TypeScript type generation
  • SSR caching — LRU in-memory cache with ETag / 304 Not Modified
  • CSRF protection — session-based tokens with automatic htmx/TSX integration
  • Concurrent loaders — parallel data fetching with result merging
  • Static site generation — pre-render routes to static HTML at build time
  • Scaffold CLIdark new / dark generate for project and component scaffolding

Quick Start

package main

import (
    "log"
    "net/http"

    "github.com/i2y/dark"
)

func main() {
    app, err := dark.New(
        dark.WithLayout("layouts/default.tsx"),
        dark.WithTemplateDir("views"),
        dark.WithDevMode(true),
    )
    if err != nil {
        log.Fatal(err)
    }
    defer app.Close()

    app.Use(dark.Logger())
    app.Use(dark.Recover())

    app.Get("/", dark.Route{
        Component: "pages/index.tsx",
        Loader: func(ctx dark.Context) (any, error) {
            return map[string]any{"message": "Hello, Dark!"}, nil
        },
    })

    log.Fatal(http.ListenAndServe(":3000", app.MustHandler()))
}

Or use the scaffold CLI to generate a new project:

go install github.com/i2y/dark/cmd/dark@latest
dark new myapp
cd myapp && go mod tidy && make dev

The layout wraps every page. Each page component's output is passed as children. On htmx requests (HX-Request header), the layout is skipped and only the page fragment is returned.

// views/layouts/default.tsx
import { h } from "preact"; // required — JSX transpiles to h() calls

export default function Layout({ children }) {
  return (
    <html lang="en">
      <head>
        <meta charset="UTF-8" />
        <title>My App</title>
        <script src="https://unpkg.com/htmx.org@2.0.4"></script>
      </head>
      <body>{children}</body>
    </html>
  );
}
// views/pages/index.tsx
import { h } from "preact"; // required — JSX transpiles to h() calls

export default function IndexPage({ message }) {
  return <h1>{message}</h1>;
}
go run main.go
# => Listening on http://localhost:3000

Routing

Routes use Go 1.22+ ServeMux patterns with {param} wildcards.

app.Get("/", dark.Route{...})
app.Get("/users/{id}", dark.Route{...})
app.Post("/users/{id}/orders", dark.Route{...})
app.Put("/posts/{id}", dark.Route{...})
app.Delete("/posts/{id}", dark.Route{...})
app.Patch("/settings", dark.Route{...})
Route struct
dark.Route{
    Component: "pages/show.tsx",   // TSX file (relative to template dir)
    Loader:    loaderFunc,          // data fetching (single)
    Loaders:   []dark.LoaderFunc{...}, // concurrent data fetching (merged)
    Action:    actionFunc,          // mutations (POST/PUT/DELETE)
    Layout:    "layouts/extra.tsx", // per-route layout (nests inside global layout)
    Streaming: &boolVal,            // per-route streaming SSR override
    Props:     MyProps{},           // zero value for TypeScript type generation
}
API routes

JSON endpoints that bypass the TSX rendering pipeline:

app.APIGet("/api/status", dark.APIRoute{
    Handler: func(ctx dark.Context) error {
        return ctx.JSON(200, map[string]any{"status": "ok"})
    },
})

app.APIPost("/api/items", dark.APIRoute{
    Handler: func(ctx dark.Context) error {
        var input CreateItemRequest
        if err := ctx.BindJSON(&input); err != nil {
            return dark.NewAPIError(400, "invalid JSON")
        }
        // ...
        return ctx.JSON(201, item)
    },
})

Route Groups

Groups share a URL prefix, layout, and middleware:

app.Group("/admin", "layouts/admin.tsx", func(g *dark.Group) {
    g.Use(dark.RequireAuth())

    g.Get("/dashboard", dark.Route{
        Component: "pages/admin/dashboard.tsx",
        Loader:    dashboardLoader,
    })

    // Nested groups compose layouts
    g.Group("/settings", "layouts/settings.tsx", func(sg *dark.Group) {
        sg.Get("/profile", dark.Route{...})
    })
})

Context

dark.Context wraps the request and response:

ctx.Request() *http.Request
ctx.ResponseWriter() http.ResponseWriter
ctx.Param("id") string              // path parameter ({id})
ctx.Query("page") string            // query string
ctx.FormData() url.Values           // parsed form data
ctx.Redirect("/path") error         // redirect (htmx-aware)
ctx.SetHeader("X-Custom", "value")

// JSON
ctx.JSON(200, data) error
ctx.BindJSON(&input) error

// Validation
ctx.AddFieldError("email", "required")
ctx.HasErrors() bool
ctx.FieldErrors() []FieldError

// Head
ctx.SetTitle("Page Title")
ctx.AddMeta("description", "...")
ctx.AddOpenGraph("og:image", "...")

// Cookies
ctx.SetCookie("theme", "dark", dark.CookieMaxAge(86400))
ctx.GetCookie("theme") (string, error)
ctx.DeleteCookie("theme")

// Session (requires Sessions middleware)
ctx.Session() *Session

// Request-scoped values (set by middleware, read by loaders)
ctx.Set("key", value)
ctx.Get("key") any

Sessions

HMAC-SHA256 signed cookie sessions:

app.Use(dark.Sessions([]byte("secret-key-at-least-32-bytes"),
    dark.SessionName("app_session"),
    dark.SessionMaxAge(86400),
    dark.SessionSecure(true),
))
// In a Loader/Action:
sess := ctx.Session()
sess.Set("user", username)
sess.Get("user")           // returns any
sess.Delete("user")
sess.Clear()

// Flash messages (available for one request)
sess.Flash("notice", "Saved!")
flashes := sess.Flashes()  // map[string]any

Authentication

// Basic usage — checks session key "user", redirects to "/login"
g.Use(dark.RequireAuth())

// Custom options
g.Use(dark.RequireAuth(
    dark.AuthSessionKey("account"),
    dark.AuthLoginURL("/auth/signin"),
    dark.AuthCheck(func(s *dark.Session) bool {
        return s.Get("role") == "admin"
    }),
))

CSRF Protection

Session-based CSRF tokens with automatic htmx integration:

app.Use(dark.Sessions(secret))
app.Use(dark.CSRF())

The middleware automatically:

  • Generates a per-session token
  • Injects <meta name="csrf-token"> into <head>
  • Injects an htmx config script that attaches X-CSRF-Token to all htmx requests
  • Adds _csrfToken to Loader props (use in hidden form fields)
  • Validates X-CSRF-Token header or _csrf form field on POST/PUT/DELETE/PATCH
export default function Form({ _csrfToken }) {
  return (
    <form method="POST" action="/submit">
      <input type="hidden" name="_csrf" value={_csrfToken} />
      <button type="submit">Submit</button>
    </form>
  );
}

htmx forms require no extra setup — the token header is attached automatically.

Concurrent Loaders

Fetch data from multiple sources in parallel:

app.Get("/dashboard", dark.Route{
    Component: "pages/dashboard.tsx",
    Loaders: []dark.LoaderFunc{
        func(ctx dark.Context) (any, error) {
            return map[string]any{"user": fetchUser(ctx.Param("id"))}, nil
        },
        func(ctx dark.Context) (any, error) {
            return map[string]any{"activity": fetchActivity(ctx.Param("id"))}, nil
        },
        func(ctx dark.Context) (any, error) {
            return map[string]any{"notifications": fetchNotifications()}, nil
        },
    },
})

Results are merged into a single props map. If any loader returns an error, the request fails immediately.

Middleware

Standard func(http.Handler) http.Handler:

app.Use(dark.Logger())                 // request logging
app.Use(dark.Recover())                // panic recovery → 500
app.Use(app.RecoverWithErrorPage())    // panic recovery → custom error page
app.Use(dark.Sessions(secret))         // session management

Any existing net/http middleware works:

app.Use(func(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        w.Header().Set("X-Frame-Options", "DENY")
        next.ServeHTTP(w, r)
    })
})

Islands Architecture

Register interactive components for client-side hydration:

app.Island("counter", "islands/counter.tsx")
// views/islands/counter.tsx
import { h } from "preact";
import { useState } from "preact/hooks";

function Counter({ initial = 0 }) {
  const [count, setCount] = useState(initial);
  return <button onClick={() => setCount(count + 1)}>Count: {count}</button>;
}

// Wrap with dark.island() — loaded immediately by default
export default dark.island("counter", Counter);

// Lazy loading strategies:
// dark.island("counter", Counter, { load: "idle" })    — requestIdleCallback
// dark.island("counter", Counter, { load: "visible" }) — IntersectionObserver

Use in any page TSX:

import Counter from "../islands/counter.tsx";

export default function Page() {
  return (
    <div>
      <h1>My Page</h1>
      <Counter initial={5} />
    </div>
  );
}

Static Files

app.Static("/static/", "public")

Options

dark.New(
    dark.WithPoolSize(4),                    // ramune RuntimePool workers (default: runtime.NumCPU())
    dark.WithTemplateDir("views"),           // TSX file directory (default: "views")
    dark.WithLayout("layouts/default.tsx"),   // global layout
    dark.WithDependencies("lodash"),          // npm packages (preact is always included)
    dark.WithDevMode(true),                  // hot reload + error overlay
    dark.WithStreaming(true),                // streaming SSR globally
    dark.WithSSRCache(1000),                 // LRU SSR output cache (enables ETag)
    dark.WithLogger(slog.Default()),         // structured logger for framework internals
    dark.WithErrorComponent("errors/500.tsx"),
    dark.WithNotFoundComponent("errors/404.tsx"),
)

Project Structure

myapp/
├── main.go
├── views/
│   ├── layouts/
│   │   └── default.tsx
│   ├── pages/
│   │   ├── index.tsx
│   │   └── users/
│   │       └── show.tsx
│   ├── islands/
│   │   └── counter.tsx
│   └── errors/
│       ├── 404.tsx
│       └── 500.tsx
└── public/
    └── style.css

Static Site Generation

Pre-render routes to static HTML at build time:

err := app.GenerateStaticSite("dist", []dark.StaticRoute{
    {
        Path:      "/",
        Component: "pages/index.tsx",
        Loader:    indexLoader,
    },
    {
        Path:      "/about",
        Component: "pages/about.tsx",
    },
    {
        // Parameterized routes: StaticPaths returns all concrete paths
        Component: "pages/post.tsx",
        StaticPaths: func() []string {
            return []string{"/posts/1", "/posts/2", "/posts/3"}
        },
        Loader: postLoader,
    },
})

Output is written to dist/ as index.html files with all CSS and island assets copied.

React Support

Dark defaults to Preact but also supports React. Pass WithUILibrary(dark.React) to switch:

app, err := dark.New(
    dark.WithUILibrary(dark.React),
    dark.WithLayout("layouts/default.tsx"),
    dark.WithTemplateDir("views"),
)

With React, components use standard React imports:

// views/pages/index.tsx
import React from 'react';

export default function IndexPage({ message }) {
  return <h1>{message}</h1>;
}

Islands use React hooks directly:

// views/islands/counter.tsx
import React, { useState } from 'react';

export default function Counter({ initial }) {
  const [count, setCount] = useState(initial || 0);
  return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
}

MCP Apps also support React via WithMCPUILibrary(dark.React).

Scaffold CLI

Generate projects and components:

go install github.com/i2y/dark/cmd/dark@latest

# New project (defaults to Preact)
dark new myapp
cd myapp && go mod tidy && make dev

# New project with React
dark new myapp --ui react

# Generate components
dark generate route users    # → views/pages/users.tsx
dark generate island counter # → views/islands/counter.tsx

MCP Apps (experimental)

Note: This feature has not been fully tested yet. The API may change.

Dark supports MCP Apps — interactive HTML UIs returned by MCP tools, rendered inside host sandboxed iframes. Built on the official Go SDK github.com/modelcontextprotocol/go-sdk.

mcpApp, err := dark.NewMCPApp("my-server", "1.0.0",
    dark.WithMCPTemplateDir("views"),
)
defer mcpApp.Close()

// UI tool: returns an interactive TSX component
if err := dark.AddUITool(mcpApp, "dashboard", dark.UIToolDef{
    Description: "Show analytics dashboard",
    Component:   "mcp/dashboard.tsx",
}, func(ctx context.Context, args DashboardArgs) (map[string]any, error) {
    return map[string]any{"data": fetchData(args.Period)}, nil
}); err != nil {
    log.Fatal(err)
}

// Text tool: standard MCP tool returning plain text
dark.AddTextTool(mcpApp, "stats", "Get statistics",
    func(ctx context.Context, args StatsArgs) (string, error) {
        return "Stats: ...", nil
    })

mcpApp.RunStdio(ctx)           // stdio transport
// or
mcpApp.StreamableHTTPHandler() // HTTP transport

UI tools produce self-contained HTML through the following pipeline:

  1. Go handler returns props
  2. Preact SSR via ramune pool → initial HTML (instant display)
  3. esbuild bundles the client (Preact inlined, cached)
  4. SSR HTML + props + App Bridge + hydration JS assembled into a single HTML
  5. Returned as an inline resource in the MCP tool result, rendered in the host iframe

Example: examples/mcp-app/

Examples

  • hello — feature-rich demo: routing, layouts, sessions, islands, streaming SSR, form validation
  • showcase — CSRF, concurrent loaders, SSR cache + ETag, SSG, Context.Set/Get
  • database — SQLite CRUD with sessions and authentication
  • deploy — production setup with Dockerfile and Fly.io config
  • mcp-app — MCP Apps: interactive UI tools with SSR + hydration

Deploy

See _examples/deploy for a production-ready setup with Docker multi-stage build and Fly.io configuration.

License

MIT

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func AddTextTool added in v0.2.0

func AddTextTool[Args any](app *MCPApp, name, description string, handler func(ctx context.Context, args Args) (string, error))

AddTextTool registers a standard text-returning MCP tool.

func AddUITool added in v0.2.0

func AddUITool[Args any](app *MCPApp, name string, def UIToolDef, handler func(ctx context.Context, args Args) (map[string]any, error)) error

AddUITool registers an MCP tool that returns an interactive TSX-based UI. The handler receives typed args and returns props for the TSX component. dark SSR-renders the component, then assembles a self-contained HTML with hydration support and returns it as an inline resource in the tool result.

AddUITool is a package-level function (not a method) because Go does not support generic methods.

func SetValue added in v0.2.0

func SetValue(r *http.Request, key string, value any) *http.Request

SetValue stores a request-scoped value that can be retrieved via Context.Get. Use this in middleware to pass data to Loaders/Actions.

Types

type APIError

type APIError struct {
	Status  int
	Message string
}

APIError represents an error with an HTTP status code for API responses.

func NewAPIError

func NewAPIError(status int, message string) *APIError

NewAPIError creates an APIError with the given status code and message.

func (*APIError) Error

func (e *APIError) Error() string

type APIRoute

type APIRoute struct {
	Handler HandlerFunc
}

APIRoute defines a handler for a JSON API endpoint.

type ActionFunc

type ActionFunc func(ctx Context) error

ActionFunc handles mutations (e.g., form submissions).

type App

type App struct {
	// contains filtered or unexported fields
}

App is the main dark application.

func New

func New(opts ...Option) (*App, error)

New creates a new dark application.

func (*App) API

func (app *App) API(method, pattern string, route APIRoute)

API registers an API route for the given HTTP method and pattern.

func (*App) APIDelete

func (app *App) APIDelete(pattern string, route APIRoute)

APIDelete registers an API route for DELETE requests.

func (*App) APIGet

func (app *App) APIGet(pattern string, route APIRoute)

APIGet registers an API route for GET requests.

func (*App) APIPatch

func (app *App) APIPatch(pattern string, route APIRoute)

APIPatch registers an API route for PATCH requests.

func (*App) APIPost

func (app *App) APIPost(pattern string, route APIRoute)

APIPost registers an API route for POST requests.

func (*App) APIPut

func (app *App) APIPut(pattern string, route APIRoute)

APIPut registers an API route for PUT requests.

func (*App) Close

func (app *App) Close() error

Close releases all resources held by the application.

func (*App) Delete

func (app *App) Delete(pattern string, route Route)

Delete registers a page route for DELETE requests.

func (*App) GenerateStaticSite added in v0.2.0

func (app *App) GenerateStaticSite(outputDir string, routes []StaticRoute) error

GenerateStaticSite pre-renders the given routes to static HTML files. Each route is rendered through the full dark SSR pipeline (Loader -> TSX -> layout) and written to outputDir as HTML files.

func (*App) GenerateTypes

func (app *App) GenerateTypes() error

GenerateTypes generates TypeScript type definitions from Props fields on registered routes. Output is written to <templateDir>/_generated/props.d.ts.

func (*App) Get

func (app *App) Get(pattern string, route Route)

Get registers a page route for GET requests.

func (*App) Group

func (app *App) Group(prefix, layout string, fn func(g *Group))

Group creates a route group with a shared URL prefix and layout. All routes registered within the group inherit the layout. Nested groups compose layouts from outer to inner.

func (*App) Handler

func (app *App) Handler() (http.Handler, error)

Handler returns the application as an http.Handler with middleware applied.

func (*App) Island

func (app *App) Island(name, tsxPath string)

Island registers a component for client-side hydration.

func (*App) MustHandler added in v0.2.0

func (app *App) MustHandler() http.Handler

MustHandler is like Handler but panics on error. Useful in main() or tests.

func (*App) Patch

func (app *App) Patch(pattern string, route Route)

Patch registers a page route for PATCH requests.

func (*App) Post

func (app *App) Post(pattern string, route Route)

Post registers a page route for POST requests.

func (*App) Put

func (app *App) Put(pattern string, route Route)

Put registers a page route for PUT requests.

func (*App) RecoverWithErrorPage

func (app *App) RecoverWithErrorPage() MiddlewareFunc

RecoverWithErrorPage returns a middleware that recovers from panics and renders the configured error page. Use this instead of Recover() to get custom error pages.

func (*App) Static

func (app *App) Static(urlPrefix, dir string)

Static registers a static file server for the given URL prefix and directory.

func (*App) Use

func (app *App) Use(mw MiddlewareFunc)

Use adds a middleware to the application.

type AuthOption

type AuthOption func(*authConfig)

AuthOption configures the RequireAuth middleware.

func AuthCheck

func AuthCheck(fn func(*Session) bool) AuthOption

AuthCheck sets a custom function to determine if a session is authenticated. When set, this overrides the default session key check.

func AuthLoginURL

func AuthLoginURL(url string) AuthOption

AuthLoginURL sets the URL to redirect unauthenticated users to (default "/login").

func AuthSessionKey

func AuthSessionKey(key string) AuthOption

AuthSessionKey sets the session key to check for authentication (default "user").

type CSRFOption added in v0.2.0

type CSRFOption func(*csrfConfig)

CSRFOption configures the CSRF middleware.

func CSRFFieldName added in v0.2.0

func CSRFFieldName(name string) CSRFOption

CSRFFieldName sets the form field name for the CSRF token.

func CSRFHeaderName added in v0.2.0

func CSRFHeaderName(name string) CSRFOption

CSRFHeaderName sets the header name for the CSRF token.

type Context

type Context interface {
	Request() *http.Request
	ResponseWriter() http.ResponseWriter
	Param(name string) string
	Query(name string) string
	FormData() url.Values
	Redirect(url string) error
	RenderError(err error) error
	SetHeader(key, value string)
	JSON(status int, data any) error
	BindJSON(v any) error
	AddFieldError(field, message string)
	HasErrors() bool
	FieldErrors() []FieldError
	SetTitle(title string)
	AddMeta(name, content string)
	AddOpenGraph(property, content string)
	SetCookie(name, value string, opts ...CookieOption)
	GetCookie(name string) (string, error)
	DeleteCookie(name string)
	Session() *Session
	Set(key string, value any)
	Get(key string) any
}

Context provides access to the request, response, and route parameters.

type CookieOption

type CookieOption func(*cookieConfig)

CookieOption configures a cookie set via Context.SetCookie.

func CookieHTTPOnly

func CookieHTTPOnly(enabled bool) CookieOption

CookieHTTPOnly sets the HttpOnly flag (default true).

func CookieMaxAge

func CookieMaxAge(seconds int) CookieOption

CookieMaxAge sets the cookie max age in seconds. 0 means session cookie.

func CookiePath

func CookiePath(path string) CookieOption

CookiePath sets the cookie path (default "/").

func CookieSameSite

func CookieSameSite(mode http.SameSite) CookieOption

CookieSameSite sets the SameSite attribute (default Lax).

func CookieSecure

func CookieSecure(enabled bool) CookieOption

CookieSecure sets the Secure flag.

type FieldError

type FieldError struct {
	Field   string `json:"field"`
	Message string `json:"message"`
}

FieldError represents a validation error for a specific form field.

type Group

type Group struct {
	// contains filtered or unexported fields
}

Group defines a set of routes that share a common URL prefix, layout, and middleware.

func (*Group) API

func (g *Group) API(method, pattern string, route APIRoute)

API registers an API route within the group.

func (*Group) APIDelete

func (g *Group) APIDelete(pattern string, route APIRoute)

APIDelete registers an API route for DELETE requests within the group.

func (*Group) APIGet

func (g *Group) APIGet(pattern string, route APIRoute)

APIGet registers an API route for GET requests within the group.

func (*Group) APIPatch

func (g *Group) APIPatch(pattern string, route APIRoute)

APIPatch registers an API route for PATCH requests within the group.

func (*Group) APIPost

func (g *Group) APIPost(pattern string, route APIRoute)

APIPost registers an API route for POST requests within the group.

func (*Group) APIPut

func (g *Group) APIPut(pattern string, route APIRoute)

APIPut registers an API route for PUT requests within the group.

func (*Group) Delete

func (g *Group) Delete(pattern string, route Route)

Delete registers a page route for DELETE requests within the group.

func (*Group) Get

func (g *Group) Get(pattern string, route Route)

Get registers a page route for GET requests within the group.

func (*Group) Group

func (g *Group) Group(prefix, layout string, fn func(g *Group))

Group creates a nested group within this group.

func (*Group) Patch

func (g *Group) Patch(pattern string, route Route)

Patch registers a page route for PATCH requests within the group.

func (*Group) Post

func (g *Group) Post(pattern string, route Route)

Post registers a page route for POST requests within the group.

func (*Group) Put

func (g *Group) Put(pattern string, route Route)

Put registers a page route for PUT requests within the group.

func (*Group) Use

func (g *Group) Use(mw MiddlewareFunc)

Use adds a middleware to the group. It applies only to routes in this group and any nested groups.

type HandlerFunc

type HandlerFunc func(ctx Context) error

HandlerFunc handles an API request.

type HeadData

type HeadData struct {
	Title string    `json:"title,omitempty"`
	Meta  []MetaTag `json:"meta,omitempty"`
}

HeadData holds metadata for the HTML <head> section.

type LoaderFunc

type LoaderFunc func(ctx Context) (any, error)

LoaderFunc fetches data for a route. The returned value is passed as props to the TSX component.

type MCPApp added in v0.2.0

type MCPApp struct {
	// contains filtered or unexported fields
}

MCPApp is an MCP server that uses dark's SSR + esbuild toolchain to render TSX components as self-contained MCP App UIs.

func NewMCPApp added in v0.2.0

func NewMCPApp(name, version string, opts ...MCPOption) (*MCPApp, error)

NewMCPApp creates a new MCP application server with dark SSR rendering.

func (*MCPApp) Close added in v0.2.0

func (m *MCPApp) Close() error

Close releases all resources held by the MCP application.

func (*MCPApp) RunStdio added in v0.2.0

func (m *MCPApp) RunStdio(ctx context.Context) error

RunStdio runs the MCP server over stdio transport. Blocks until the client disconnects or the context is cancelled.

func (*MCPApp) Server added in v0.2.0

func (m *MCPApp) Server() *mcp.Server

Server returns the underlying mcp.Server for advanced configuration (adding prompts, resources, or non-UI tools directly).

func (*MCPApp) StreamableHTTPHandler added in v0.2.0

func (m *MCPApp) StreamableHTTPHandler() http.Handler

StreamableHTTPHandler returns an http.Handler for Streamable HTTP transport.

type MCPOption added in v0.2.0

type MCPOption func(*mcpConfig)

MCPOption configures an MCPApp.

func WithMCPDevMode added in v0.2.0

func WithMCPDevMode(enabled bool) MCPOption

WithMCPDevMode enables development mode (source maps, no minification, cache invalidation).

func WithMCPMinify added in v0.2.0

func WithMCPMinify(enabled bool) MCPOption

WithMCPMinify enables minification of client-side bundles (default: true).

func WithMCPPoolSize added in v0.2.0

func WithMCPPoolSize(n int) MCPOption

WithMCPPoolSize sets the number of ramune RuntimePool workers for SSR.

func WithMCPTemplateDir added in v0.2.0

func WithMCPTemplateDir(dir string) MCPOption

WithMCPTemplateDir sets the directory for TSX template files.

func WithMCPUILibrary added in v0.2.0

func WithMCPUILibrary(lib UILibrary) MCPOption

WithMCPUILibrary selects the JSX library for MCP App SSR and client bundles.

type MetaTag

type MetaTag struct {
	Name     string `json:"name,omitempty"`
	Property string `json:"property,omitempty"`
	Content  string `json:"content"`
}

MetaTag represents a <meta> element.

type MiddlewareFunc

type MiddlewareFunc func(http.Handler) http.Handler

MiddlewareFunc is a standard Go HTTP middleware.

func CSRF added in v0.2.0

func CSRF(opts ...CSRFOption) MiddlewareFunc

CSRF returns a middleware that provides CSRF protection. Requires the Sessions middleware to be applied first.

On GET/HEAD/OPTIONS requests, a token is generated (if not already in the session) and stored in the request context for automatic injection into rendered pages.

On state-mutating requests (POST/PUT/DELETE/PATCH), the token is validated from either the X-CSRF-Token header or the _csrf form field.

Dark-specific integration:

  • The token is automatically added to Loader props as _csrfToken
  • A <meta name="csrf-token"> tag and htmx config script are injected into HTML

func Logger

func Logger() MiddlewareFunc

Logger returns a middleware that logs each request.

func Recover

func Recover() MiddlewareFunc

Recover returns a middleware that recovers from panics and returns a 500 response.

func RequireAuth

func RequireAuth(opts ...AuthOption) MiddlewareFunc

RequireAuth returns a middleware that redirects unauthenticated users to the login page. It requires the Sessions middleware to be applied first.

Usage:

app.Group("/admin", "layouts/admin.tsx", func(g *dark.Group) {
    g.Use(dark.RequireAuth())
    g.Get("/dashboard", dark.Route{...})
})

func Sessions

func Sessions(secret []byte, opts ...SessionOption) MiddlewareFunc

Sessions returns a middleware that provides cookie-based sessions with HMAC signing. The secret is used to sign and verify session cookies.

type Option

type Option func(*config)

Option configures the dark application.

func WithDependencies

func WithDependencies(pkgs ...string) Option

WithDependencies adds additional npm dependencies beyond the UI library.

func WithDevMode

func WithDevMode(enabled bool) Option

WithDevMode enables development mode with cache invalidation on file changes.

func WithErrorComponent

func WithErrorComponent(file string) Option

WithErrorComponent sets a TSX component for rendering 500 error pages.

func WithLayout

func WithLayout(file string) Option

WithLayout sets the layout TSX file path relative to the template directory.

func WithLogger added in v0.2.0

func WithLogger(logger *slog.Logger) Option

WithLogger sets the structured logger for dark's internal log output. Defaults to slog.Default().

func WithNotFoundComponent

func WithNotFoundComponent(file string) Option

WithNotFoundComponent sets a TSX component for rendering 404 pages.

func WithPoolSize

func WithPoolSize(n int) Option

WithPoolSize sets the number of ramune RuntimePool workers.

func WithSSRCache

func WithSSRCache(maxEntries int) Option

WithSSRCache enables SSR output caching. maxEntries sets the maximum number of cached component+props combinations. When the cache is full, it is cleared. 0 (default) disables caching.

func WithStreaming

func WithStreaming(enabled bool) Option

WithStreaming enables streaming SSR (shell-first rendering for faster TTFB).

func WithTemplateDir

func WithTemplateDir(dir string) Option

WithTemplateDir sets the directory for TSX template files.

func WithUILibrary added in v0.2.0

func WithUILibrary(lib UILibrary) Option

WithUILibrary selects the JSX library for SSR and client-side hydration. Defaults to Preact. Use dark.React to switch to React/ReactDOM.

type Route

type Route struct {
	Component string       // TSX file path relative to the template directory
	Loader    LoaderFunc   // data loader (single)
	Loaders   []LoaderFunc // concurrent data loaders; results are merged into one props map
	Action    ActionFunc   // mutation handler
	Layout    string       // layout TSX file path relative to the template directory (nests inside global layout)
	Streaming *bool        // nil = use global default, true/false = per-route override
	Props     any          // optional: Go struct zero value for TypeScript type generation
}

Route defines a handler for a URL pattern with SSR rendering.

type Session

type Session struct {
	// contains filtered or unexported fields
}

Session holds per-request session data backed by a signed cookie.

func (*Session) Clear

func (s *Session) Clear()

Clear removes all session data.

func (*Session) Delete

func (s *Session) Delete(key string)

Delete removes a key from the session.

func (*Session) Flash

func (s *Session) Flash(key string, value any)

Flash sets a flash message that will be available in the next request.

func (*Session) Flashes

func (s *Session) Flashes() map[string]any

Flashes returns and clears all flash messages from the previous request. Returns nil if there are no flashes.

func (*Session) Get

func (s *Session) Get(key string) any

Get returns the session value for key, or nil if not set.

func (*Session) Set

func (s *Session) Set(key string, value any)

Set stores a value in the session.

type SessionOption

type SessionOption func(*sessionConfig)

SessionOption configures the session middleware.

func SessionHTTPOnly

func SessionHTTPOnly(enabled bool) SessionOption

SessionHTTPOnly sets the HttpOnly flag on the session cookie (default true).

func SessionMaxAge

func SessionMaxAge(seconds int) SessionOption

SessionMaxAge sets the session max age in seconds (default 86400 = 1 day).

func SessionName

func SessionName(name string) SessionOption

SessionName sets the session cookie name (default "_dark_session").

func SessionPath

func SessionPath(path string) SessionOption

SessionPath sets the session cookie path (default "/").

func SessionSameSite

func SessionSameSite(mode http.SameSite) SessionOption

SessionSameSite sets the SameSite attribute on the session cookie (default Lax).

func SessionSecure

func SessionSecure(enabled bool) SessionOption

SessionSecure sets the Secure flag on the session cookie.

type StaticRoute added in v0.2.0

type StaticRoute struct {
	// Path is the URL path to generate (e.g., "/" or "/about").
	Path string

	// Component is the TSX file path relative to the template directory.
	Component string

	// Layout is an optional layout override (comma-separated for nested layouts).
	Layout string

	// Loader is an optional data loader. If nil, empty props are used.
	Loader LoaderFunc

	// StaticPaths returns all concrete paths for parameterized routes.
	// For example, a route "/posts/{id}" might return ["/posts/1", "/posts/2"].
	// If set, Path is ignored and each returned path is generated.
	StaticPaths func() []string
}

StaticRoute defines a route to be pre-rendered at build time.

type UILibrary added in v0.2.0

type UILibrary int

UILibrary selects the JSX library used for SSR and client-side hydration.

const (
	// Preact is the default UI library.
	Preact UILibrary = iota
	// React selects React/ReactDOM for SSR and hydration.
	React
)

type UIToolDef added in v0.2.0

type UIToolDef struct {
	Description string // Tool description for the LLM
	Component   string // TSX file path relative to template directory
	Title       string // Optional human-readable title
}

UIToolDef defines a UI tool's metadata.

Directories

Path Synopsis
_examples
database command
Example: dark + SQLite database integration
Example: dark + SQLite database integration
deploy/cmd/server command
Production-ready dark application entry point.
Production-ready dark application entry point.
hello command
hello-react command
showcase command
showcase demonstrates several dark framework features:
showcase demonstrates several dark framework features:
cmd
dark command
examples
mcp-app command

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL