NexGo Reference
Complete documentation for the NexGo web framework β the Go-powered alternative to Next.js.
Quick Start
Get a NexGo project running in under 2 minutes.
1. Install NexGo
β οΈ PATH Setup (Important):
export PATH=$PATH:$(go env GOPATH)/bin echo 'export PATH=$PATH:$(go env GOPATH)/bin' >> ~/.bashrc && source ~/.bashrc
2. Create a New Project
nexgo create my-app
cd my-app
go mod tidy
nexgo dev
β Created project my-app with NexGo template
β pages/index.html
β layouts/default.html
β static/css/global.css
β nexgo.config.json
β main.go
β Dependencies installed
[NexGo] Dev server β http://localhost:3000
[NexGo] Hot reload enabled. Watching 5 filesβ¦
[NexGo] Press Ctrl+C to stop.
Open http://localhost:3000 β your app is live with hot reload enabled.
Project Structure
Running nexgo create my-app generates this structure:
my-app/
βββ main.go # Application entry point
βββ nexgo.config.json # Framework configuration
βββ go.mod
β
βββ pages/ # File-based routes
β βββ index.html # β /
β βββ about.html # β /about
β βββ blog/
β β βββ index.html # β /blog
β β βββ [slug].html # β /blog/:slug (dynamic)
β βββ api/
β βββ hello.go # β /api/hello
β
βββ layouts/
β βββ default.html # Default layout
βββ components/ # Reusable partials
βββ static/ # Static assets β /static/
β βββ css/global.css
β βββ js/
βββ .nexgo/out/ # Build output (generated)
File-Based Routing
Drop HTML files in pages/ and NexGo creates routes automatically β no manual registration needed.
| File Path | URL Pattern | Example |
|---|---|---|
pages/index.html | / | / |
pages/about.html | /about | /about |
pages/blog/index.html | /blog | /blog |
pages/blog/[slug].html | /blog/:slug | /blog/my-post |
pages/docs/[...path].html | /docs/* | /docs/api/auth |
pages/api/users.go | /api/users | /api/users |
Dynamic Routes
Use [param] syntax for dynamic URL segments:
pages/
βββ users/
β βββ [id].html β /users/123
β βββ [id]/posts.html β /users/123/posts
βββ [...catchall].html β /any/path/here
Access parameters in templates:
<h1>User {"{"}{ .Params.id }{"}"}</h1>
<p>Post: {"{"}{ .Params.slug }{"}"}</p>
/users/42User 42
Post: my-first-blog
Layouts & Components
Layouts wrap page content. NexGo walks up the directory tree to find the nearest layout:
<!-- layouts/default.html -->
<!DOCTYPE html>
<html>
<head>
<title>{"{"}{ .Title }{"}"}</title>
<link rel="stylesheet" href="/static/css/global.css">
</head>
<body>
<nav>...</nav>
<footer>...</footer>
</body>
</html>
Reusable template partials stored in components/:
<!-- components/card.html -->
<div class="card">
</div>
<!-- Usage in pages -->
Template Engine
NexGo uses Go's html/template package. Every page receives a PageData struct:
type PageData struct {
Title string
Description string
Path string // Current URL path
Params map[string]string // Route params
Query map[string]string // Query string
Props map[string]interface{} // From DataLoader
DevMode bool
BuildID string
}
API Routes
Create .go files in pages/api/ to build REST endpoints:
// pages/api/hello.go
package api
import (
"net/http"
"github.com/salmanfaris22/nexgo/pkg/api"
"github.com/salmanfaris22/nexgo/pkg/router"
)
func init() {
router.RegisterAPI("/api/hello", Hello)
}
func Hello(w http.ResponseWriter, r *http.Request) {
api.JSON(w, map[string]interface{}{
"message": "Hello from NexGo!",
"method": r.Method,
})
}
GET /api/helloHTTP/1.1 200 OK
Content-Type: application/json
{
"message": "Hello from NexGo!",
"method": "GET"
}
Data Loaders
Data loaders work like Next.js getServerSideProps β fetch data server-side before rendering:
// main.go
srv.RegisterDataLoader("/blog/[slug]", func(
req *http.Request,
params map[string]string,
) (map[string]interface{}, error) {
slug := params["slug"]
post, err := db.GetPost(slug)
if err != nil {
return nil, err
}
return map[string]interface{}{"post": post}, nil
})
<!-- pages/blog/[slug].html -->
<h1>{"{"}{ .Props.post.Title }{"}"}</h1>
/blog/hello-worldGetting Started with NexGo
Data loaded server-side via DataLoader before page render
Middleware
Built-in middleware is applied in this order:
Request β Recover β Logger β SecurityHeaders β [Gzip] β Handler
[NexGo] GET /about 200 1.2ms
[NexGo] POST /api/users 201 3.8ms
[NexGo] GET /not-found 404 0.4ms
[NexGo] GET /api/crash 500 0.1ms recovered: nil pointer
| Middleware | Description |
|---|---|
Recover | Catches panics, returns 500 |
Logger | Logs method, path, status, duration |
SecurityHeaders | X-Content-Type-Options, X-Frame-Options, etc. |
Gzip | Compresses responses (configurable) |
CORS(origins...) | Configurable CORS headers |
Cache(maxAge) | Sets Cache-Control headers |
Hot Reload (HMR)
Run nexgo dev β file watcher polls every 500ms, broadcasts reload via SSE to all connected browsers.
nexgo dev
[NexGo] Dev server β http://localhost:3000
[NexGo] Hot reload enabled. Watching 12 filesβ¦
[NexGo] Press Ctrl+C to stop.
β you edit pages/about.html β
[HMR] Change detected: pages/about.html
[HMR] β Browser reloaded (12ms)
DevTools Panel
In dev mode, visit http://localhost:3000/_nexgo/devtools:
| Tab | Shows |
|---|---|
| Routes | All routes with type badges (PAGE / API) |
| Requests | Live log with method, status, path, duration |
| Logs | Server logs, HMR events, errors |
| Performance | Total requests, avg response time, error rate |
| Config | Current server and build configuration |
Building for Production
nexgo build
nexgo start
nexgo start --port 4000
$ nexgo build
[NexGo] Building static siteβ¦
β Compiled 8 pages
β Processed 3 API routes
β Copied static assets (14 files)
β Output β .nexgo/out/ (234 KB)
$ nexgo start
[NexGo] Production server β http://localhost:3000
[NexGo] Gzip enabled Β· HMR disabled
Deploying
NexGo compiles to a single binary β deploy anywhere Go runs:
# Build the binary
go build -o myapp ./main.go
# Copy to server
scp myapp server:/opt/myapp/
scp -r pages/ layouts/ static/ nexgo.config.json server:/opt/myapp/
# Run on server
ssh server '/opt/myapp/myapp'
Configuration
// nexgo.config.json
{
"projectName": "my-app",
"port": 3000,
"host": "localhost",
"pagesDir": "pages",
"staticDir": "static",
"layoutsDir": "layouts",
"componentsDir": "components",
"outputDir": ".nexgo/out",
"hotReload": true,
"compression": true,
"minify": true,
"defaultRenderMode": "ssr"
}
Template Functions Reference
String
| Function | Usage | Output | Description |
|---|---|---|---|
upper | {"{"}{ upper "hello" }{"}"} | HELLO | Uppercase |
lower | {"{"}{ lower "HI" }{"}"} | hi | Lowercase |
title | {"{"}{ title "hello world" }{"}"} | Hello World | Title case |
trim | {"{"}{ trim " hi " }{"}"} | hi | Trim whitespace |
replace | {"{"}{ replace "hi" "i" "ey" }{"}"} | hey | Replace substring |
split | {"{"}{ split "a,b" "," }{"}"} | [a b] | Split to slice |
Utility
| Function | Description |
|---|---|
json .Data | JSON encode a value |
safeHTML "<b>bold</b>" | Render as raw HTML (trusted content only) |
dict "key" "val" | Create a key-value map |
asset "/static/css/app.css" | Cache-busted asset URL |
default "fallback" .Value | Default if nil/empty |
times 5 | Iterate n times |
API Helpers Reference
import "github.com/salmanfaris22/nexgo/pkg/api"
// Responses
api.JSON(w, data) // 200 + JSON
api.JSONStatus(w, 201, data) // Custom status
api.Error(w, 400, "bad input") // JSON error
// Status shortcuts
api.BadRequest(w, "invalid id") // 400
api.NotFound(w, "not found") // 404
api.Unauthorized(w) // 401
api.InternalError(w, err) // 500
// Request parsing
var input CreateUserInput
if !api.Decode(w, r, &input) { return }
// Method routing
api.Route(w, r, api.Methods{
"GET": listUsers,
"POST": createUser,
})
// Pagination
page, limit := api.Paginate(r) // ?page=2&limit=10
HTMX Helpers
Send HTML fragments from Go and let HTMX update the DOM β no page reloads needed.
api.IsHTMX(r) β Check if request is from HTMX
if api.IsHTMX(r) {
// Return HTML fragment
api.HTMXHTML(w, "<li>New item</li>")
} else {
// Return JSON
api.JSON(w, data)
}
api.HTMXHTML(w, html) β Send HTML fragment response
api.HTMXHTML(w, "<div>Updated content</div>")
HTTP/1.1 200 OK
Content-Type: text/html
<div>Updated content</div>
β³ HTMX swaps this into the target element β no page reload
api.HTMXTrigger(w, event) β Fire client-side event after response
api.HTMXTrigger(w, "todo-added")
api.HTMXHeader(w, key, value) β Set any HTMX header
api.HTMXHeader(w, "HX-Redirect", "/dashboard")
State Management
Thread-safe global state shared across all handlers. No mutex boilerplate.
api.SetState(key, value) β Store a value
api.SetState("todos", []Todo{...})
api.SetState("counter", 42)
api.GetState(key) β Retrieve a value
todos := api.GetState("todos").([]Todo)
api.DeleteState(key) β Remove a key
api.DeleteState("todos")
api.NewState() β Create isolated state (optional)
sessionState := api.NewState()
sessionState.Set("user", "alice")
HTML Helpers
api.Escape(s) β Sanitize HTML output
safe := api.Escape("<script>alert('xss')</script>")
<script>alert('xss')</script>
api.HTML(w, html) β Send raw HTML response
api.HTML(w, "<h1>Hello</h1>")
SEO (Meta, Sitemap, OpenGraph)
Meta tags, OpenGraph, Twitter Cards, sitemap.xml, robots.txt, and JSON-LD structured data.
Meta Tags
import "github.com/salmanfaris22/nexgo/pkg/seo"
meta := seo.DefaultMeta("My Site", "A great website", "https://mysite.com")
meta.Keywords = []string{"go", "web", "framework"}
meta.OGImage = "https://mysite.com/og.png"
meta.TwitterCard = "summary_large_image"
meta.TwitterSite = "@mysite"
// Use in template: {"{"}{ .MetaTags }{"}"}
Article Meta
meta := seo.ArticleMeta(
"How to Build APIs in Go",
"A guide to building fast APIs",
"John Doe",
"https://mysite.com/images/cover.jpg",
"https://mysite.com/blog/go-apis",
time.Now(),
)
Sitemap
entries := []seo.SitemapEntry{
{Loc: "https://mysite.com/", ChangeFreq: "daily", Priority: 1.0},
{Loc: "https://mysite.com/about", ChangeFreq: "monthly", Priority: 0.8},
{Loc: "https://mysite.com/blog", ChangeFreq: "weekly", Priority: 0.9},
}
xml, _ := seo.RenderSitemap(entries)
// Write xml to /sitemap.xml response
Robots.txt
robots := seo.RobotsTxt(
[]string{"/"}, // allow
[]string{"/admin", "/api"}, // disallow
"https://mysite.com/sitemap.xml", // sitemap URL
)
// Write robots to /robots.txt response
JSON-LD Structured Data
data := map[string]interface{}{
"@context": "https://schema.org",
"@type": "Organization",
"name": "My Company",
"url": "https://mysite.com",
}
jsonld := seo.JSONLD(data)
// In template: {"{"}{ .JSONLD }{"}"}
Template Caching
Templates are compiled once and cached for fast subsequent renders.
import "github.com/salmanfaris22/nexgo/pkg/renderer"
r := renderer.New(cfg)
r.LoadAll() // Compiles and caches all templates
// Check cache stats
info := r.CacheInfo()
// map[templates:5 layouts:1 components:3 loaded_at:2026-04-03...]
Parallel Template Loading
r.LoadParallel() // Uses worker pool to compile templates concurrently
Parallel Data Loading
Load multiple data sources concurrently and merge results β like Promise.all() in JS.
import (
"github.com/salmanfaris22/nexgo/pkg/renderer"
"net/http"
)
// Create parallel loader
loader := renderer.NewParallelLoader().
Add("users", fetchUsers).
Add("posts", fetchPosts).
Add("stats", fetchStats)
// Execute all concurrently
props, err := loader.Execute(req, params)
// props = map[string]interface{}{
// "users": [...], "posts": [...], "stats": {...}
// }
func fetchUsers(req *http.Request, params map[string]string) (map[string]interface{}, error) {
users := db.Query("SELECT * FROM users")
return map[string]interface{}{"users": users}, nil
}
func fetchPosts(req *http.Request, params map[string]string) (map[string]interface{}, error) {
posts := db.Query("SELECT * FROM posts LIMIT 10")
return map[string]interface{}{"posts": posts}, nil
}
Streaming SSR
Send HTML progressively as it's ready β show the shell immediately, stream content later.
import "github.com/salmanfaris22/nexgo/pkg/stream"
func handlePage(w http.ResponseWriter, r *http.Request) {
sw := stream.StreamHTML(w)
// Send head + layout immediately
sw.WriteString(`<!DOCTYPE html><html><head><title>My Page</title></head><body>`)
sw.WriteString(`<header>Navigation</header><main>`)
sw.Flush()
// Do slow work (DB query, API call)
data := slowDatabaseQuery()
// Stream content
sw.WriteString(fmt.Sprintf("<h1>%s</h1>", data.Title))
sw.WriteString(fmt.Sprintf("<p>%s</p>", data.Body))
// Close HTML
sw.WriteString(`</main></body></html>`)
}
Server-Sent Events (SSE)
func handleSSE(w http.ResponseWriter, r *http.Request) {
sse := stream.NewSSE(w)
defer sse.Flush()
for {
sse.Send("update", `{"count": 42}`)
sse.Ping()
time.Sleep(1 * time.Second)
}
}
Response Caching
Cache GET responses in memory with TTL. Automatic cache headers.
Middleware
import "github.com/salmanfaris22/nexgo/pkg/api"
// Cache a handler for 5 minutes
mux.HandleFunc("/api/posts", api.Cache(5*time.Minute)(handlePosts))
// Cache with custom TTL
mux.HandleFunc("/api/slow", api.Cache(30*time.Second)(handleSlow))
Manual Cache
// Set cache
api.CacheSet("/api/data", 200, headers, body)
// Get cache
code, headers, body, ok := api.CacheGet("/api/data")
if ok {
w.WriteHeader(code)
w.Write(body)
return
}
// Delete specific key
api.CacheDelete("/api/data")
// Clear all
api.CacheClear()
First request: X-Cache: MISS β generated fresh (45ms)
Second request: X-Cache: HIT β served from cache (0.2ms)
After TTL: X-Cache: MISS β cache expired, regenerated
Worker Pools
Run tasks concurrently with controlled parallelism.
Basic Pool
import "github.com/salmanfaris22/nexgo/pkg/worker"
pool := worker.New(4) // 4 workers
pool.Start()
pool.Submit(func() error {
return processFile("data1.csv")
})
pool.Submit(func() error {
return processFile("data2.csv")
})
errs := pool.Wait() // blocks until all done
Map (concurrent transform)
results := worker.Map(4, items, func(item Item) Result {
return process(item)
})
// results are in original order
Map with Errors
results, err := worker.MapErr(4, urls, func(url string) (Page, error) {
return fetchPage(url)
})
ForEach
worker.ForEach(8, emails, func(email string) {
sendEmail(email)
})
Global Pool
worker.Start() // starts global pool with 4 workers
worker.Submit(func() error { return doWork() })
errs := worker.Wait()
Incremental Static Regeneration (ISR)
Cache pages and rebuild them in the background after a configurable interval β like Next.js ISR.
Basic ISR
import "github.com/salmanfaris22/nexgo/pkg/isr"
// Create ISR with 60-second revalidation
cache := isr.New(60 * time.Second)
func handlePage(w http.ResponseWriter, r *http.Request) {
cache.Serve(w, r, func() (int, http.Header, []byte, error) {
// This runs only on first request or after 60s
html := renderExpensivePage()
return 200, nil, []byte(html), nil
})
}
How ISR Works
| Step | Behavior | Header |
|---|---|---|
| First request | Generates page, caches it, returns | X-ISR: MISS |
| Subsequent (within TTL) | Serves cached page | X-ISR: STALE |
| After TTL expires | Serves stale, regenerates in background | X-ISR: REVALIDATING |
| Next request | Gets fresh cached content | X-ISR: HIT |
Manual Revalidation
// Trigger background rebuild of a specific path
cache.Revalidate("/blog/post-1", func() (int, http.Header, []byte, error) {
return 200, nil, []byte(renderPost()), nil
})
// Purge from cache
cache.Purge("/blog/post-1")
cache.PurgeAll()
Global ISR
isr.SetRevalidate(120 * time.Second)
isr.Serve(w, r, func() (int, http.Header, []byte, error) {
return 200, nil, []byte(html), nil
})
isr.Revalidate("/page", generateFunc)
isr.Purge("/page")
Environment Variables
The pkg/env package loads .env files with priority-based overrides and provides type-safe access to environment variables. Files are loaded in order: .env, .env.local, .env.{mode}, with later files taking precedence.
import "github.com/salmanfaris22/nexgo/pkg/env"
// Load .env files (call once at startup)
env.LoadGlobal() // loads .env, .env.local, .env.development
env.LoadGlobal("production") // loads .env, .env.local, .env.production
// Create a store for typed access
store := env.NewStore()
store.Get("DATABASE_URL") // returns value or ""
store.MustGet("SECRET_KEY") // panics if missing
store.GetInt("PORT", 3000) // with default
store.GetBool("DEBUG", false)
.env files & loaded values# .env
DATABASE_URL=postgres://localhost/myapp
PORT=3000
# .env.production
DATABASE_URL=postgres://prod-db/myapp β overrides .env
SECRET_KEY=a1b2c3d4...
store.GetInt("PORT", 8080) β 3000
store.GetBool("DEBUG", false) β false
Sessions
Cookie-based session management with pluggable backends. Ships with MemoryStore (development) and FileStore (production). Supports flash messages for one-time notifications.
import "github.com/salmanfaris22/nexgo/pkg/session"
mgr := session.NewManager(session.Options{
Store: session.NewMemoryStore(), // or session.NewFileStore("./sessions")
MaxAge: 3600,
HTTPOnly: true,
Secure: true,
})
func handler(w http.ResponseWriter, r *http.Request) {
sess := mgr.Start(w, r)
sess.Set("user", "alice")
name := sess.Get("user") // "alice"
// Flash messages (auto-cleared after read)
sess.Flash("success", "Item created!")
msgs := sess.Flashes("success") // ["Item created!"]
}
Set-Cookie: nexgo_session=abc123def456;
Path=/; HttpOnly; Secure; Max-Age=3600
Session Store:
"user" β "alice"
"flash:success" β ["Item created!"] (cleared after read)
Authentication (JWT)
JWT token generation, verification, middleware, role-based access control, and password hashing via pkg/auth.
import "github.com/salmanfaris22/nexgo/pkg/auth"
// Generate a JWT
token, err := auth.GenerateToken(auth.Claims{
UserID: "123",
Role: "admin",
}, "my-secret-key", 24*time.Hour)
// Verify a JWT
claims, err := auth.VerifyToken(token, "my-secret-key")
// Middleware: protect routes
mux.Handle("/dashboard", auth.RequireAuth("my-secret-key")(dashboardHandler))
mux.Handle("/admin", auth.RequireRole("admin", "my-secret-key")(adminHandler))
// Password hashing
hash, _ := auth.HashPassword("s3cret")
ok := auth.CheckPassword("s3cret", hash) // true
CSRF Protection
Token-based CSRF protection via pkg/csrf. Generates a per-session token, validates it on POST/PUT/DELETE requests, and provides a template helper for forms.
import "github.com/salmanfaris22/nexgo/pkg/csrf"
// Apply middleware
handler := csrf.Protect("my-csrf-secret")(myHandler)
// In templates, use the helper to embed the token in forms:
// <form method="POST">
// {"{"}{ csrfField }{"}"} <!-- renders hidden input -->
// <button type="submit">Submit</button>
// </form>
// For AJAX, read the token from the meta tag or cookie:
// X-CSRF-Token: <token>
Rate Limiting
Protect endpoints from abuse with token bucket and sliding window algorithms via pkg/ratelimit. Apply as global middleware or per-route.
import "github.com/salmanfaris22/nexgo/pkg/ratelimit"
// Token bucket: 100 requests per minute per IP
limiter := ratelimit.PerMinute(100)
mux.Handle("/api/", limiter.Middleware()(apiHandler))
// Sliding window: 10 requests per second
strict := ratelimit.PerSecond(10)
mux.Handle("/api/login", strict.Middleware()(loginHandler))
// Custom configuration
limiter := ratelimit.New(ratelimit.Config{
Rate: 50,
Window: time.Minute,
Strategy: ratelimit.SlidingWindow,
KeyFunc: func(r *http.Request) string { return r.RemoteAddr },
})
Internationalization (i18n)
Locale routing, JSON translation files, RTL support, and automatic locale detection via pkg/i18n. Place translation files in locales/.
import "github.com/salmanfaris22/nexgo/pkg/i18n"
// locales/en.json: {"greeting": "Hello, {"{"}name{"}"}!", "nav.home": "Home"}
// locales/ar.json: {"greeting": "!{"{"}name{"}"} ,Ω
Ψ±ΨΨ¨Ψ§", "nav.home": "Ψ§ΩΨ±Ψ¦ΩΨ³ΩΨ©"}
bundle := i18n.New(i18n.Config{
DefaultLocale: "en",
Locales: []string{"en", "fr", "ar"},
Directory: "locales",
RTLLocales: []string{"ar", "he"},
})
// In handlers
t := bundle.T("en")
t.Get("greeting", map[string]string{"name": "World"}) // "Hello, World!"
// Locale detection middleware (Accept-Language, cookie, URL prefix)
mux.Handle("/", bundle.Middleware()(handler))
Image Optimization
Automatic responsive images, lazy loading, srcset generation, and blur placeholders via pkg/image.
import "github.com/salmanfaris22/nexgo/pkg/image"
opt := image.NewOptimizer(image.Config{
Widths: []int{320, 640, 1024, 1920},
Quality: 80,
LazyLoad: true,
Placeholder: image.BlurPlaceholder,
})
// In templates, generates <img> with srcset, sizes, loading="lazy"
// {"{"}{ optimizeImage "/static/hero.jpg" "Hero image" }{"}"}
<!-- Output -->
<img src="/static/hero.jpg"
srcset="/static/hero-320w.jpg 320w, /static/hero-640w.jpg 640w, /static/hero-1024w.jpg 1024w"
sizes="(max-width: 640px) 100vw, 50vw"
loading="lazy"
alt="Hero image">
WebSocket
Native WebSocket support using HTTP hijacking via pkg/websocket. Includes a Hub for managing rooms and broadcasting messages.
import "github.com/salmanfaris22/nexgo/pkg/websocket"
hub := websocket.NewHub()
go hub.Run()
mux.HandleFunc("/ws", func(w http.ResponseWriter, r *http.Request) {
conn, err := websocket.Upgrade(w, r)
if err != nil { return }
client := hub.Register(conn)
defer hub.Unregister(client)
// Join a room
hub.Join(client, "chat-room-1")
// Broadcast to all clients
hub.Broadcast([]byte("Hello everyone!"))
// Broadcast to a specific room
hub.BroadcastTo("chat-room-1", []byte("Room message"))
})
Health Checks
Kubernetes-compatible health check endpoints via pkg/health. Exposes /health, /ready, and /live with dependency checks.
import "github.com/salmanfaris22/nexgo/pkg/health"
checker := health.New()
// Register dependency checks
checker.AddCheck("database", func() error {
return db.Ping()
})
checker.AddCheck("redis", func() error {
return redis.Ping()
})
// Mount endpoints
mux.Handle("/health", checker.Handler()) // overall status
mux.Handle("/ready", checker.ReadyHandler()) // readiness probe
mux.Handle("/live", checker.LiveHandler()) // liveness probe
// GET /health β {"status":"healthy","checks":{"database":"ok","redis":"ok"}}
GET /healthHTTP/1.1 200 OK
{
"status": "healthy",
"checks": {
"database": "ok",
"redis": "ok"
},
"uptime": "2h 14m 32s"
}
Structured Logging
Leveled, structured logging with JSON output, colored console mode, and file rotation via pkg/logger.
import "github.com/salmanfaris22/nexgo/pkg/logger"
log := logger.New(logger.Config{
Level: logger.Info,
Format: logger.JSON, // or logger.Console (colored)
Output: "logs/app.log", // file path or "stdout"
Rotation: logger.Daily, // Daily, Hourly, or by Size
})
log.Info("server started", "port", 3000)
log.Error("query failed", "err", err, "query", sql)
log.Debug("request", "method", r.Method, "path", r.URL.Path)
// Request logger middleware
mux.Handle("/", logger.RequestMiddleware(log)(handler))
{"level":"info","msg":"server started","port":3000}
{"level":"info","method":"GET","path":"/","status":200,"duration":"1.2ms"}
{"level":"error","msg":"query failed","err":"connection refused"}
Metrics
Prometheus-compatible metrics with counters, gauges, and histograms via pkg/metrics. Includes built-in HTTP middleware and a /metrics endpoint.
import "github.com/salmanfaris22/nexgo/pkg/metrics"
m := metrics.New()
// Define metrics
requestCount := m.Counter("http_requests_total", "Total HTTP requests")
activeConns := m.Gauge("http_active_connections", "Active connections")
latency := m.Histogram("http_request_duration_seconds", "Request latency")
// Use in handlers
requestCount.Inc()
activeConns.Set(42)
latency.Observe(0.023)
// Expose /metrics endpoint (Prometheus scrape target)
mux.Handle("/metrics", m.Handler())
// Auto-instrument all HTTP traffic
mux.Handle("/", m.Middleware()(handler))
GET /metrics# HELP http_requests_total Total HTTP requests
# TYPE http_requests_total counter
http_requests_total 1247
# HELP http_active_connections Active connections
# TYPE http_active_connections gauge
http_active_connections 42
# HELP http_request_duration_seconds Request latency
# TYPE http_request_duration_seconds histogram
http_request_duration_seconds_sum 28.6
http_request_duration_seconds_count 1247
Database
Lightweight data layer via pkg/db with a JSON file database for prototyping and a SQL query builder for production.
import "github.com/salmanfaris22/nexgo/pkg/db"
// JSON file database (zero setup)
store := db.NewJSONStore("data/")
store.Insert("users", map[string]interface{}{"name": "Alice", "age": 30})
users := store.FindAll("users")
store.Update("users", "id-123", map[string]interface{}{"age": 31})
store.Delete("users", "id-123")
// SQL query builder
q := db.Table("users").
Where("age > ?", 18).
OrderBy("name ASC").
Limit(10).
SelectSQL()
SELECT * FROM users WHERE age > 18 ORDER BY name ASC LIMIT 10
Migrations
Version-tracked database migrations with up/down support via pkg/migrate.
import "github.com/salmanfaris22/nexgo/pkg/migrate"
mgr := migrate.New(db)
mgr.RegisterSQL("001_create_users",
"CREATE TABLE users (id SERIAL PRIMARY KEY, name TEXT, email TEXT)",
"DROP TABLE users",
)
mgr.RegisterSQL("002_add_role",
"ALTER TABLE users ADD COLUMN role TEXT DEFAULT 'user'",
"ALTER TABLE users DROP COLUMN role",
)
mgr.Up() // run all pending migrations
mgr.Down() // rollback last migration
mgr.Status() // list applied and pending
mgr.Pending() // list unapplied migrations
$ mgr.Up()
β Applied 001_create_users (2ms)
β Applied 002_add_role (1ms)
$ mgr.Status()
β 001_create_users applied 2026-04-06 10:23:01
β 002_add_role applied 2026-04-06 10:23:01
Plugin System
Hook-based plugin architecture via pkg/plugin. Plugins can add middleware, routes, template functions, and respond to lifecycle events.
import "github.com/salmanfaris22/nexgo/pkg/plugin"
// Define a plugin
myPlugin := plugin.New("analytics", func(p *plugin.Plugin) {
// Add middleware to all routes
p.AddMiddleware(trackingMiddleware)
// Add a new route
p.AddRoute("GET", "/analytics", analyticsHandler)
// Add template functions
p.AddTemplateFunc("trackEvent", func(name string) string {
return fmt.Sprintf(`<script>track("%s")</script>`, name)
})
// Lifecycle hooks
p.OnStart(func() { log.Println("Analytics plugin started") })
p.OnShutdown(func() { flushEvents() })
})
// Register with the app
app.Use(myPlugin)
Server Actions
Form-based server mutations with built-in validation via pkg/actions. Similar to Next.js Server Actions but using standard HTML forms.
import "github.com/salmanfaris22/nexgo/pkg/actions"
actions.Register("createPost", func(ctx *actions.Context) error {
title := ctx.FormValue("title")
body := ctx.FormValue("body")
// Validation
ctx.Validate("title", title != "", "Title is required")
ctx.Validate("body", len(body) > 10, "Body must be at least 10 chars")
if ctx.HasErrors() { return ctx.ValidationError() }
db.Insert("posts", map[string]interface{}{"title": title, "body": body})
return ctx.RedirectTo("/blog")
})
<!-- In templates -->
<form method="POST" action="/actions/createPost">
{"{"}{ actionField "createPost" }{"}"} <!-- hidden input with action name -->
<input name="title" type="text">
<textarea name="body"></textarea>
<button type="submit">Create Post</button>
</form>
Error Pages
Convention-based error pages via pkg/errorpage. Place 404.html or 500.html in pages/ or any subdirectory for per-section error boundaries.
pages/
βββ 404.html # Global 404
βββ 500.html # Global 500
βββ admin/
βββ 404.html # Admin-specific 404
import "github.com/salmanfaris22/nexgo/pkg/errorpage"
// Apply middleware (auto-discovers error pages)
handler := errorpage.Middleware("pages")(mux)
// Error pages receive:
// {"{"}{ .StatusCode }{"}"} β 404, 500, etc.
// {"{"}{ .Message }{"}"} β human-readable error message
// {"{"}{ .Path }{"}"} β the requested URL path
Testing
Test client with HTTP method helpers and assertions via pkg/testing. Spin up your app in-process and make real HTTP requests.
import nextest "github.com/salmanfaris22/nexgo/pkg/testing"
func TestHomePage(t *testing.T) {
app := nextest.NewClient(myHandler)
// GET request
resp := app.GET("/")
resp.AssertStatus(200)
resp.AssertContains("Welcome")
// POST with form data
resp = app.POST("/api/users", nextest.Form{"name": "Alice", "email": "a@b.com"})
resp.AssertStatus(201)
resp.AssertJSON("name", "Alice")
// PUT / DELETE
app.PUT("/api/users/1", nextest.JSON{"name": "Bob"}).AssertStatus(200)
app.DELETE("/api/users/1").AssertStatus(204)
}
// Benchmarking
func BenchmarkAPI(b *testing.B) {
app := nextest.NewClient(myHandler)
nextest.Bench(b, func() { app.GET("/api/data") })
}
Per-Route Middleware
Apply middleware to individual routes or groups via pkg/middleware. Includes built-in timeout, request ID, and CSP middleware with nonce support.
import "github.com/salmanfaris22/nexgo/pkg/middleware"
// Per-route middleware
mux.Handle("/api/slow", middleware.RouteMiddleware(
middleware.Timeout(5*time.Second),
middleware.RequestID(),
)(slowHandler))
// Route groups
api := middleware.RouteGroup("/api",
middleware.Timeout(10*time.Second),
middleware.RequestID(),
)
api.Handle("/users", usersHandler)
api.Handle("/posts", postsHandler)
// CSP with nonce (auto-injected into templates as .CSPNonce)
mux.Handle("/", middleware.CSP(middleware.CSPConfig{
ScriptSrc: "'nonce-{"{"}nonce{"}"}'",
StyleSrc: "'self'",
})(handler))
Deployment Adapters
One-command deployment to 8 platforms via pkg/deploy. Each adapter generates the necessary config files (Dockerfile, serverless handler, etc.).
import "github.com/salmanfaris22/nexgo/pkg/deploy"
// Generate deployment config
deploy.Generate("docker") // Dockerfile + docker-compose.yml
deploy.Generate("vercel") // vercel.json
deploy.Generate("cloudflare") // wrangler.toml
deploy.Generate("lambda") // serverless.yml + handler wrapper
deploy.Generate("netlify") // netlify.toml
deploy.Generate("fly") // fly.toml
deploy.Generate("railway") // railway.json
deploy.Generate("k8s") // deployment.yaml + service.yaml
# CLI shortcut
nexgo deploy docker # generates Dockerfile and builds image
nexgo deploy vercel # generates vercel.json and deploys
nexgo deploy k8s # generates K8s manifests
nexgo deploy docker[NexGo] Generating Docker deploymentβ¦
β Created Dockerfile
β Created docker-compose.yml
β Created .dockerignore
[NexGo] Building image my-app:latestβ¦
β Image built (23.4 MB)
Run with: docker compose up
Redis Cache
Zero-dependency Redis client using raw RESP protocol via pkg/cache. Includes connection pooling, key prefixing, and a CacheAdapter interface for swappable backends.
import "github.com/salmanfaris22/nexgo/pkg/cache"
rc := cache.NewRedis(cache.RedisConfig{
Addr: "localhost:6379",
Password: "",
DB: 0,
PoolSize: 10,
KeyPrefix: "myapp:",
})
rc.Set("user:123", userData, 5*time.Minute)
val, err := rc.Get("user:123")
rc.Delete("user:123")
// Use as CacheAdapter (swap in-memory / Redis without code changes)
var store cache.CacheAdapter = rc
store.Set("key", value, ttl)
store.Get("key")
Asset Pipeline
CSS and JS minification, bundling, fingerprinting, and critical CSS extraction via pkg/asset.
import "github.com/salmanfaris22/nexgo/pkg/asset"
pipeline := asset.New(asset.Config{
SourceDir: "static",
OutputDir: ".nexgo/assets",
Minify: true,
Bundle: true,
Fingerprint: true,
})
// Process all assets
pipeline.Build()
// In templates, reference fingerprinted URLs:
// <link rel="stylesheet" href="{"{"}{ asset "/css/app.css" }{"}"}">
// β <link rel="stylesheet" href="/static/css/app.a1b2c3d4.css">
// Critical CSS (inline above-the-fold styles)
critical := pipeline.ExtractCritical("pages/index.html")
// Inject into <head> as <style>...</style>
ORM
Lightweight ORM with model definitions, auto-migration SQL generation, CRUD operations (JSON backend), and a fluent query DSL via pkg/orm.
import "github.com/salmanfaris22/nexgo/pkg/orm"
// Define a model
type User struct {
orm.Model
Name string `json:"name"`
Email string `json:"email"`
Age int `json:"age"`
}
db := orm.New("data/")
db.AutoMigrate(&User{}) // generates CREATE TABLE SQL
// CRUD
db.Create(&User{Name: "Alice", Email: "alice@example.com", Age: 30})
var user User
db.Find(&user, "id = ?", 1)
db.Where("age > ?", 18).Order("name").Limit(10).FindAll(&users)
// Fluent query with joins
db.Table("users").
Join("posts", "users.id = posts.user_id").
Where("posts.published = ?", true).
Select("users.name", "COUNT(posts.id) as post_count").
GroupBy("users.name").
QuerySQL()
SELECT users.name, COUNT(posts.id) AS post_count
FROM users
JOIN posts ON users.id = posts.user_id
WHERE posts.published = true
GROUP BY users.name
Cluster Mode
Run your app across multiple worker goroutines with graceful shutdown and zero-downtime restart via pkg/cluster. The built-in load balancer distributes requests across workers.
import "github.com/salmanfaris22/nexgo/pkg/cluster"
c := cluster.New(cluster.Config{
Workers: 4, // number of worker goroutines
GracefulTimeout: 30 * time.Second, // max shutdown wait
Handler: mux,
Addr: ":3000",
})
// Start with automatic load balancing
c.Start()
// Zero-downtime restart (new workers start before old ones stop)
// Send SIGUSR2 to trigger:
// kill -USR2 <pid>
// Graceful shutdown on SIGINT/SIGTERM
// Workers finish in-flight requests before exiting
SEO Killer Mode
The most SEO-complete Go framework. Every feature works out-of-the-box with zero configuration. Auto sitemap, robots.txt, canonical URLs, 10 JSON-LD schema types, OpenGraph, Twitter Cards, RSS feeds, Core Web Vitals, hreflang, SEO audit, and more.
Quick Start
Add SEO to any NexGo app in 2 steps:
// 1. Add to nexgo.config.json
"seo": { "siteName": "My Site", "siteURL": "https://example.com", "autoSitemap": true }
// 2. Add to layouts/default.html
{{ seoTags (seoMeta .Title .Description .Canonical) }}
{{ breadcrumbs .Path }}
That's it. You now have: auto sitemap.xml, robots.txt, canonical URLs, OpenGraph, Twitter Cards, breadcrumb schema, and SEO meta tags on every page.
SEO Template Functions (Reference)
| Function | Usage | Description |
|---|---|---|
seoTags | {{ seoTags (seoMeta .Title .Description .Canonical) }} | Render all SEO meta tags |
seoMeta | {{ seoMeta "Title" "Desc" "/url" }} | Create a Meta struct |
breadcrumbs | {{ breadcrumbs .Path }} | Auto BreadcrumbList JSON-LD |
jsonld | {{ jsonld (dict "@type" "Event" ...) }} | Generic JSON-LD tag |
websiteSchema | {{ websiteSchema "Name" "URL" "SearchURL" }} | WebSite schema |
orgSchema | {{ orgSchema "Name" "URL" "Logo" }} | Organization schema |
faqSchema | {{ faqSchema "Q?" "A." "Q?" "A." }} | FAQ schema |
preload | {{ preload "/path" "style" }} | Resource preload hint |
vitals | {{ vitals }} | Core Web Vitals script |
slugify | {{ slugify "Hello World" }} | SEO-friendly URL slug |
SEO Configuration
All SEO settings live in nexgo.config.json under the "seo" key:
{
"seo": {
"siteName": "My Awesome Site",
"siteURL": "https://example.com",
"titleTemplate": "%s | My Awesome Site",
"defaultOGImage": "https://example.com/static/images/og-default.png",
"twitterSite": "@mysite",
"language": "en",
"autoSitemap": true,
"autoRobotsTxt": true,
"autoCanonical": true,
"coreWebVitals": false,
"robotsDisallow": ["/api/", "/_nexgo/", "/admin/"],
"redirects": [
{"from": "/old-page", "to": "/new-page", "status": 301}
]
}
}
Auto Sitemap
NexGo automatically generates /sitemap.xml from your route table. No configuration needed.
Includes <lastmod>, <changefreq>, <priority> tags. Homepage gets priority 1.0. API and dynamic routes are excluded. Supports sitemap index for 50k+ URLs and hreflang alternates in sitemap.
// Custom sitemap with dynamic routes
entries := seo.AutoSitemap("https://example.com", staticRoutes, 0.5)
for _, post := range posts {
entries = append(entries, seo.SitemapEntry{
Loc: "https://example.com/blog/" + post.Slug,
LastMod: post.UpdatedAt.Format("2006-01-02"),
ChangeFreq: "monthly",
Priority: 0.7,
})
}
Auto Robots.txt
Auto-served at /robots.txt based on your config. Supports multiple user-agent blocks, crawl delay, and AI bot blocking (GPTBot, CCBot).
content := seo.RobotsTxtAdvanced([]seo.RobotsRule{
{UserAgent: "*", Allow: []string{"/"}, Disallow: []string{"/api/"}},
{UserAgent: "GPTBot", Disallow: []string{"/"}},
}, []string{"https://example.com/sitemap.xml"})
Meta Tags & Title Templates
Use seoTags in your layout to generate all SEO meta tags in one call:
<head>
{{ seoTags (seoMeta .Title .Description .Canonical) }}
</head>
Generates: viewport, title (with template), description, og:title/description/url/image/type/site_name/locale, twitter:card/title/description/image/site, canonical, theme-color, and favicon.
OpenGraph & Twitter Cards
Full support for all OG types (website, article, product, profile) and Twitter cards (summary, summary_large_image). Article-specific tags: article:published_time, article:modified_time, article:author, article:section, article:tag.
meta := seo.ArticleMeta(
"How to Build Fast Web Apps",
"A guide to building performant apps...",
"Salman Faris",
"https://example.com/img/article.png",
"https://example.com/blog/fast-apps",
time.Now(),
)
JSON-LD Structured Data
10 pre-built structured data types plus a generic builder:
- WebSite β with Sitelinks Searchbox
- Organization β name, logo, social profiles
- LocalBusiness β address, phone, coordinates
- Article β author, publisher, dates, tags
- BreadcrumbList β auto-generated from URL path
- FAQ β question/answer pairs
- Product β price, SKU, availability, reviews
- Review β author, rating, body
- HowTo β step-by-step instructions
- Generic β any JSON-LD via
jsonldtemplate func
// In templates
{{ breadcrumbs .Path }}
{{ faqSchema "What is NexGo?" "A Go web framework." "Is it fast?" "200k+ req/sec." }}
{{ websiteSchema "My Site" "https://example.com" "https://example.com/search?q=" }}
RSS Feed Generation
Built-in RSS 2.0 renderer. Register an endpoint, build your feed items, and serve:
data, _ := seo.RenderRSS(seo.Feed{
Title: "My Site Blog",
Link: "https://example.com",
Description: "Latest articles",
Language: "en",
Items: items,
})
w.Header().Set("Content-Type", "application/rss+xml")
w.Write(data)
Hreflang Alternates
For multilingual sites, add alternate language links. Supports x-default and hreflang in both HTML and sitemap:
meta.Alternates = []seo.AlternateLink{
{HrefLang: "en", Href: "https://example.com/about"},
{HrefLang: "fr", Href: "https://example.com/fr/about"},
{HrefLang: "x-default", Href: "https://example.com/about"},
}
Core Web Vitals
Track LCP, CLS, and INP automatically. Enable in config, add {{ vitals }} to your layout, and create an API endpoint to receive beacon reports.
// nexgo.config.json
"coreWebVitals": true,
"vitalsEndpoint": "/api/vitals"
// In layout
{{ vitals }}
SEO Audit & Validation
Programmatic auditing catches SEO regressions in tests:
results := seo.AuditMeta(meta)
// Checks: MISSING_TITLE, TITLE_TOO_LONG, TITLE_TOO_SHORT,
// MISSING_DESCRIPTION, DESC_TOO_LONG, DESC_TOO_SHORT,
// MISSING_CANONICAL, MISSING_OG_IMAGE, NO_TWITTER_CARD
htmlResults := seo.AuditHTML(htmlString)
// Checks: MISSING_H1, MULTIPLE_H1, IMG_MISSING_ALT, MISSING_VIEWPORT
Redirects & Middleware
Config-based 301/302 redirects, trailing slash normalization, HSTS, and SEO HTTP headers:
// Config-based redirects
"redirects": [
{"from": "/old-blog", "to": "/blog", "status": 301}
]
// Trailing slash normalization
handler := seo.TrailingSlashMiddleware(false, yourHandler)
// HSTS (1 year)
handler := seo.HSTSMiddleware(31536000, yourHandler)
// Auto SEO headers: Content-Language, X-Content-Type-Options,
// Referrer-Policy, Permissions-Policy
High-Performance Mode
Push throughput from 50k to 200,000+ req/sec on multi-core machines β all without switching away from Go's standard net/http.
| Config | Throughput | Latency (p99) | CPU Cores |
|---|---|---|---|
| v2.1.0 (default) | ~50k req/sec | ~2ms | 1 |
| v2.2.0 (cluster, 4 cores) | ~200k req/sec | ~0.8ms | 4 |
| v2.2.0 (cluster, 8 cores) | ~400k req/sec | ~0.5ms | 8 |
| v2.2.0 (cluster + cache) | ~1M+ req/sec | ~0.05ms | 8 (cached) |
Quick Start β Enable Performance Mode
// nexgo.config.json
{
"clusterMode": true,
"asyncLogging": true,
"responseCache": true,
"responseCacheTTL": 300,
"compression": true
}
nexgo start (cluster mode)β‘ NexGo β Go-powered web framework
Local: http://localhost:3000
Mode: production
Cluster: 4 workers (auto-detected 4 CPU cores)
Cache: response caching ON (TTL: 300s)
Logging: async (buffer: 8192)
Islands: 3 (counter, search, chart)
Pages: ./pages/
Cluster Mode
Creates multiple http.Server instances that share a single TCP listener. The Go runtime distributes incoming connections across workers. Each worker has its own goroutine pool.
// Config
{
"clusterMode": true,
"clusterWorkers": 0 // 0 = auto-detect (runtime.NumCPU())
}
// Programmatic access
import "github.com/salmanfaris22/nexgo/v2/pkg/cluster"
stats := cluster.GetStats()
fmt.Printf("Active: %d, Total: %d, Uptime: %s\n",
stats.ActiveRequests, stats.TotalRequests, stats.Uptime)
| Machine | Workers | Expected Throughput |
|---|---|---|
| 2 vCPU (t3.small) | 2 | ~100k req/sec |
| 4 vCPU (t3.medium) | 4 | ~200k req/sec |
| 8 vCPU (c5.2xlarge) | 8 | ~400k req/sec |
| 16 vCPU (c5.4xlarge) | 16 | ~750k+ req/sec |
Zero-downtime restart: New workers start with updated handler, old workers drain in-flight requests (up to 30s), then shut down. No connections dropped.
Async Logging
Writes to a buffered channel (capacity: 8,192 entries). A background goroutine drains the channel. Requests are never blocked by log I/O.
// Config
{
"asyncLogging": true
}
// Behavior at high load:
// - Normal: logs every request
// - Buffer full: drops log entry, request continues unblocked
// - Crash: up to 8,192 entries may be lost
Note: Async logging is only active in production mode. Dev mode always uses synchronous logging for accurate debugging.
Response Caching
Stores complete HTTP responses (status + headers + body) in memory. Cache hits return in microseconds.
// Config
{
"responseCache": true,
"responseCacheTTL": 300 // seconds
}
// Programmatic invalidation
import "github.com/salmanfaris22/nexgo/v2/pkg/cache"
cache.CacheDelete(cache.Key(req)) // delete specific
cache.GlobalCache().DeletePrefix("/blog") // delete by prefix
cache.CacheClear() // clear all
| Content Type | Recommended TTL | Why |
|---|---|---|
| Marketing pages | 3600s (1 hour) | Rarely changes |
| Blog posts | 300s (5 min) | Updates after publish |
| User dashboards | 0 (disabled) | Personalized content |
| API responses | 30-60s | Data freshness |
Headers: X-Cache: HIT or X-Cache: MISS on every response. Only GET requests are cached.
Route Match Caching
Caches route match results in a sync.Map β a lock-free concurrent map. After the first request to a path, subsequent requests skip regex matching entirely.
// First request to /about:
// β Regex scan (20 patterns) β Match found β Cache β 800ns
// Second request to /about:
// β sync.Map lookup β Cache hit β 15ns
// Automatic β no configuration needed
// Cache clears on: route rescan (dev), server restart
Atomic Template Reads
After templates load, NexGo publishes an immutable snapshot via sync/atomic.Value. Production reads use this snapshot with zero locking.
| Concurrent Requests | RWMutex Lookup | Atomic Lookup |
|---|---|---|
| 100 | ~50ns | ~5ns |
| 1,000 | ~200ns (contention) | ~5ns |
| 10,000 | ~800ns (high contention) | ~5ns |
Atomic reads are O(1) constant time regardless of concurrency level. Always enabled β no config needed.
Optimization Summary
| Optimization | Impact | Config |
|---|---|---|
| Cluster mode | 4-8x throughput | clusterMode: true |
| Async logging | 10-15% gain | asyncLogging: true |
| Route caching | 5-10% gain | Always on |
| Atomic templates | 5% gain | Always on |
| Response caching | 10-100x for cached | responseCache: true |
$ wrk -t8 -c256 -d30s http://localhost:3000/
Running 30s test @ http://localhost:3000/
8 threads and 256 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 0.81ms 0.35ms 8.21ms 89.23%
Req/Sec 25.4k 1.2k 28.9k 72.15%
203,472 requests in 30.00s
Requests/sec: 203,472.18
Transfer/sec: 9.82MB
Islands Architecture
Ship zero JavaScript by default. Only interactive components ("islands") load JS β with fine-grained control over when they hydrate.
How it works: Pages are static HTML. Only interactive components ("islands") ship JavaScript to the browser. Each island has a server-side template (.html) and an optional client-side script (.js). The JS is only loaded when the island needs to hydrate β based on the strategy you choose.
| Metric | Next.js (React) | NexGo Islands |
|---|---|---|
| JS on a static page | ~85KB (React runtime) | 0 KB |
| JS for 1 interactive button | ~85KB + component | ~2KB (just the island) |
| Time to Interactive (blog) | ~1.2s | ~0.1s |
| Hydration model | Full page | Per-component |
| JS loading control | None | 5 strategies |
Creating Islands
An island is a pair of files in the islands/ directory:
| File | Purpose | Required |
|---|---|---|
islands/name.html | Server-side Go template | Yes |
islands/name.js | Client-side JavaScript (ES module) | No (SSR-only if omitted) |
Step 1: Create the Template
<!-- islands/counter.html -->
<div class="counter">
<span class="counter-value">{{ .count }}</span>
<button class="counter-btn" data-action="decrement">-</button>
<button class="counter-btn" data-action="increment">+</button>
</div>
Step 2: Create the Client JS
// islands/counter.js
export default function init(el, props) {
let count = props.count || 0;
const display = el.querySelector('.counter-value');
el.querySelectorAll('.counter-btn').forEach(btn => {
btn.addEventListener('click', () => {
count += btn.dataset.action === 'increment' ? 1 : -1;
display.textContent = count;
});
});
}
Rules for island JS:
- Export a
defaultfunction or a namedinitfunction - Receives two arguments:
el(the<nexgo-island>DOM element) andprops - Use vanilla JS β no framework required (but you can import one)
Step 3: Use in a Page
<!-- pages/index.html -->
<h1>My Page</h1>
<p>This is static HTML. Zero JavaScript.</p>
{{ island "counter" (props "count" 0) "client:load" }}
<h1>My Page</h1>
<p>This is static HTML. Zero JavaScript.</p>
<nexgo-island data-name="counter"
data-strategy="client:load"
data-has-js="true"
data-props='{"count":0}'>
<div class="counter">
<span class="counter-value">0</span>
<button data-action="decrement">-</button>
<button data-action="increment">+</button>
</div>
</nexgo-island>
Hydration Strategies
Hydration strategies control when an island's JavaScript loads and executes.
| Strategy | JS Loaded | When | Best For |
|---|---|---|---|
client:load | Immediately | Page load | Above-the-fold interactive UI |
client:visible | On scroll | Enter viewport | Below-the-fold widgets |
client:idle | When idle | Browser free time | Non-critical interactive UI |
client:media | On match | Media query | Device-specific UI |
client:none | Never | β | Reusable SSR-only components |
client:load β Immediate
Hydrate immediately when the page loads. Use for above-the-fold interactive elements.
{{ island "search-bar" (props "placeholder" "Search...") "client:load" }}
client:visible β On Scroll
Hydrate when the island scrolls into the viewport (using IntersectionObserver with 200px margin).
{{ island "comment-form" (props "postId" .Params.slug) "client:visible" }}
client:idle β When Free
Hydrate when the browser is idle (using requestIdleCallback).
{{ island "analytics-widget" (props "data" .Props.stats) "client:idle" }}
client:media β Device-Specific
Hydrate only when a CSS media query matches. The island JS is never loaded on non-matching devices.
{{ island "mobile-menu" nil "client:media=(max-width:768px)" }}
client:none β SSR Only
Never hydrate. The island is rendered server-side and sent as static HTML. No JS is ever shipped.
{{ island "sidebar" (props "user" .State.user) "client:none" }}
<!-- pages/dashboard.html -->
<h1>Dashboard</h1>
{{ island "search" (props "endpoint" "/api/search") "client:load" }}
<!-- β Loads JS immediately -->
<section class="summary">
Revenue: $
</section>
<!-- β Static HTML β 0 KB JS -->
{{ island "chart" (props "data" .Props.chartData) "client:visible" }}
<!-- β Loads when scrolled into view -->
{{ island "notifications" (props "count" 5) "client:idle" }}
<!-- β Loads when browser is idle -->
{{ island "mobile-filters" nil "client:media=(max-width:768px)" }}
<!-- β Only loads on mobile -->
{{ island "activity" (props "events" .Props.events) "client:none" }}
<!-- β SSR only β zero JS ever -->
Passing Props
Use the props template function to pass data from the server to an island.
Basic Props
{{ island "greeting" (props "name" "World" "emoji" "hi") }}
<!-- islands/greeting.html -->
<p>{{ .emoji }} Hello, {{ .name }}!</p>
// islands/greeting.js
export default function(el, props) {
console.log(props.name); // "World"
}
Props from Data Loaders
// main.go
srv.RegisterDataLoader("/dashboard", func(req *http.Request, params map[string]string) (map[string]interface{}, error) {
return map[string]interface{}{
"stats": fetchStats(),
"chartData": fetchChartData(),
}, nil
})
<!-- pages/dashboard.html -->
<div>Revenue: $</div>
{{ island "chart" (props "data" .Props.chartData "type" "line") "client:visible" }}
Props from Route Parameters
<!-- pages/blog/[slug].html -->
{{ island "comment-section" (props "postSlug" .Params.slug) "client:visible" }}
No Props
{{ island "theme-toggle" }}
{{ island "scroll-to-top" nil "client:visible" }}
Writing Island JavaScript
Every island JS file exports a default function that receives the element and props.
Basic Structure
// islands/my-island.js
export default function init(el, props) {
// el β the <nexgo-island> DOM element
// props β the serialized props from the template
}
DOM Queries (Scoped to Island)
// islands/tabs.js
export default function(el, props) {
const buttons = el.querySelectorAll('[data-tab]');
const panels = el.querySelectorAll('[data-panel]');
buttons.forEach(btn => {
btn.addEventListener('click', () => {
const target = btn.dataset.tab;
panels.forEach(p => p.hidden = p.dataset.panel !== target);
buttons.forEach(b => b.classList.toggle('active', b === btn));
});
});
}
Fetching Data from API Routes
// islands/live-count.js
export default function(el, props) {
const display = el.querySelector('.count');
async function refresh() {
const res = await fetch('/api/visitors/count');
const data = await res.json();
display.textContent = data.count;
}
refresh();
setInterval(refresh, 5000);
}
Importing Third-Party Libraries (CDN)
// islands/chart.js
import Chart from 'https://cdn.jsdelivr.net/npm/chart.js@4/+esm';
export default function(el, props) {
const canvas = el.querySelector('canvas');
new Chart(canvas, {
type: props.type || 'bar',
data: props.data,
});
}
<!-- islands/search.html -->
<div class="search-box">
<input type="text" class="search-input"
placeholder="{{ .placeholder }}" />
<div class="search-results"></div>
</div>
// islands/search.js
export default function(el, props) {
const input = el.querySelector('.search-input');
const results = el.querySelector('.search-results');
let timeout;
input.addEventListener('input', () => {
clearTimeout(timeout);
timeout = setTimeout(async () => {
const q = input.value.trim();
const res = await fetch(`/api/search?q=${"{"}q{"}"}`);
const data = await res.json();
results.innerHTML = data.items
.map(i => `<a href="${"{"}i.url{"}"}">${"{"}i.title{"}"}</a>`)
.join('');
}, 300);
});
}
<!-- Usage -->
{{ island "search" (props "placeholder" "Search...") "client:load" }}
SSR-Only Islands
Create islands without a .js file for reusable server-rendered components. No JavaScript is ever shipped.
<!-- islands/product-card.html β No .js file -->
<div class="product-card">
<img src="{{ .image }}" alt="{{ .name }}" loading="lazy">
<h3>{{ .name }}</h3>
<p class="price">${{ .price }}</p>
{{ if .sale }}<span class="badge">Sale</span>{{ end }}
</div>
<!-- pages/shop.html β No JS shipped, pure HTML -->
<div class="product-grid">
{{ range .Props.products }}
{{ island "product-card" (props "name" .Name "price" .Price "image" .Image "sale" .OnSale) "client:none" }}
{{ end }}
</div>
<div class="product-grid">
<nexgo-island data-name="product-card" data-strategy="client:none">
<div class="product-card">
<img src="/img/headphones.jpg" loading="lazy">
<h3>Wireless Headphones</h3>
<p class="price">$79.99</p>
<span class="badge">Sale</span>
</div>
</nexgo-island>
<!-- ... more cards, all pure HTML -->
</div>
Total JS shipped: 0 KB
Island Runtime
The island runtime is a lightweight (~1KB) JavaScript file that handles discovery and hydration.
How It Works
- On page load, the runtime scans for
<nexgo-island>elements - For each island, it reads the
data-strategyattribute - Based on the strategy, it schedules hydration:
- load β runs immediately
- visible β sets up
IntersectionObserver - idle β calls
requestIdleCallback - media β listens to
matchMedia
- When triggered, it dynamically imports the island's JS module
- The module's default export is called with
(element, props)
Including the Runtime
<!-- Option A: Script tag (recommended) -->
<script src="/_nexgo/island-runtime.js" defer></script>
<!-- Option B: Inline via template function -->
{{ islandRuntime }}
Template Functions Reference
| Function | Usage | Description |
|---|---|---|
island | {{ island "name" }} | Render with default strategy (client:load) |
island | {{ island "name" (props "k" "v") }} | Render with props |
island | {{ island "name" (props "k" "v") "client:visible" }} | Render with props and strategy |
island | {{ island "name" nil "client:idle" }} | No props, custom strategy |
props | {{ props "key" "val" "key2" "val2" }} | Build a props map for islands |
islandRuntime | {{ islandRuntime }} | Inline the hydration runtime |
HTTP Endpoints
| Endpoint | Description |
|---|---|
/_nexgo/islands/{name}.js | Serves individual island JS files |
/_nexgo/island-runtime.js | Serves the hydration runtime |
Configuration
// nexgo.config.json
{
"islandsDir": "islands", // Directory containing island files (default)
// ... other config unchanged
}
Project Structure with Islands
my-app/
βββ main.go
βββ nexgo.config.json
βββ pages/ # File-based routes
β βββ index.html
β βββ blog/[slug].html
βββ islands/ # Island components (NEW)
β βββ counter.html # Server template
β βββ counter.js # Client JS (ES module)
β βββ search.html
β βββ search.js
β βββ product-card.html # SSR-only (no .js)
β βββ chart.html
β chart.js
βββ layouts/
βββ components/
βββ static/
βββ .nexgo/out/
nexgo dev with Islands β‘ NexGo β Go-powered web framework
Local: http://localhost:3000
Mode: development (hot reload on)
Pages: ./pages/
Islands: 3 (counter, search, chart)
Devtools: http://localhost:3000/_nexgo/devtools
Migration from v2.0.x
No breaking changes. Islands are purely additive. Steps to start using them:
- Update NexGo:
go get github.com/salmanfaris22/nexgo/v2@v2.1.0 - Create
islands/directory:mkdir islands - Add the runtime to your layout:
<script src="/_nexgo/island-runtime.js" defer></script> - Start creating islands!
CLI Commands
| Command | Description |
|---|---|
nexgo create <name> | Scaffold a new NexGo project |
nexgo dev | Start dev server with hot reload |
nexgo build | Static site generation (SSG) |
nexgo start | Start production server |
nexgo version | Print version |
nexgo help | Show help |
nexgo versionNexGo v2.2.0
Go go1.22.5 linux/arm64
Built 2026-04-06
Islands enabled
Cluster 4 workers
π¬ Need help? Open an issue on GitHub, or reach out to salmanfariskalm@gmail.com.