Introduction to Twerge
Twerge is a Go library designed to enhance your experience working with Tailwind CSS in Go applications. The name "Twerge" comes from "Tailwind + Merge".
What is Twerge?
Twerge is a comprehensive Go library that performs four key functions for Tailwind CSS integration:
- Intelligent Class Merging - Resolves conflicts between Tailwind CSS classes according to their specificity rules
- Class Name Generation - Creates short, unique CSS class names based on hashes of the merged classes
- Class Mapping Management - Maintains mappings between original class strings and generated class names, with code generation capabilities
Why Use Twerge?
If you're developing Go-based web applications with Tailwind CSS, Twerge offers significant advantages:
- Smaller HTML output - By merging conflicting classes and generating short class names
- Better performance - Through intelligent caching and efficient lookups
- Build-time optimization - Via code generation capabilities
- Runtime flexibility - Through the runtime static hashmap for dynamic class handling
- Simplified workflow - By integrating seamlessly with Go templates, particularly templ
Key Features
- Intelligent class merging - Resolves conflicts according to Tailwind CSS specificity rules
- Short class name generation - Creates compact, unique class names for reduced HTML size
- Runtime class management - Provides a fast lookup system for dynamic applications
- Code generation - Produces optimized Go code for class mappings
- CSS integration - Works with Tailwind CLI and CSS build pipelines
- Flexible configuration - Customizable caching, hash algorithms, and more
- Nix integration - Reproducible development environment
Target Use Cases
Twerge is particularly well-suited for:
- Go web applications using Tailwind CSS
- Projects using the templ templating language
- Applications requiring build-time CSS optimization
- Static site generators with Tailwind CSS integration
Next Steps
To get started with Twerge, check out:
- Installation - How to install Twerge in your Go project
- Configuration - How to configure Twerge for your needs
- Merging Classes - Learn about the core class merging functionality
Installation
Using Go
go get github.com/conneroisu/twerge
Using Git
You can also clone the repository directly:
git clone https://github.com/conneroisu/twerge.git
cd twerge
Nix Integration (Development Environment)
Twerge comes with Nix support for a reproducible development environment:
# Start a nix shell with all dependencies
nix-shell
# Or with nix flakes
nix develop
Verifying Installation
To verify the installation, create a simple Go program:
package main
import (
"fmt"
"github.com/conneroisu/twerge"
)
func main() {
// Merge conflicting Tailwind classes
merged := twerge.Merge("text-red-500 bg-blue-300 text-xl")
fmt.Println(merged) // Should output "bg-blue-300 text-xl text-red-500" or similar order
}
Run the program:
go run main.go
Configuration
Twerge can be configured in several ways to customize its behavior. The library doesn't require a configuration file by default, but you can configure its behavior programmatically. Most of the configuration options are optional and rely on interfaces to provide flexibility.
Merging Tailwind CSS Classes
One of the core features of Twerge is the ability to intelligently merge Tailwind CSS classes, resolving conflicts according to Tailwind's specificity rules.
The Problem
When working with Tailwind CSS, you often need to combine multiple sets of classes, which can lead to conflicts. For example:
// These classes conflict (two text colors)
"text-red-500 text-blue-500"
In this case, you want the last class to win (text-blue-500
), following Tailwind's specificity rules.
How Twerge Solves It
The Merge
function in Twerge intelligently combines Tailwind classes, resolving conflicts correctly:
import "github.com/conneroisu/twerge"
// Example usage
mergedClasses := twerge.Merge("text-red-500 bg-blue-300 text-xl")
// Result: "bg-blue-300 text-xl text-red-500"
// Classes are resolved and ordered by type
Class Resolution Rules
Twerge follows Tailwind's conflict resolution rules:
- Last Declaration Wins - For conflicting classes of the same type, the last one in the string takes precedence
- Type Preservation - Non-conflicting classes are preserved
- Order Optimization - The resulting class string is optimized for readability and consistency
Supported Class Categories
Twerge understands and correctly handles conflicts between these Tailwind categories:
- Layout (display, position, etc.)
- Flexbox & Grid
- Spacing (margin, padding)
- Sizing (width, height)
- Typography (font, text)
- Backgrounds
- Borders
- Effects
- Filters
- Tables
- Transitions & Animation
- Transforms
- Interactivity
- SVG
- Accessibility
Advanced Merging
Twerge can handle complex combinations, including:
// Merging multiple complex class sets
classes1 := "flex items-center space-x-4 text-sm"
classes2 := "grid text-lg font-bold"
merged := twerge.Merge(classes1 + " " + classes2)
// Result includes non-conflicting classes and resolves conflicts
// "items-center space-x-4 grid text-lg font-bold"
Performance Optimization
Twerge uses an LRU cache for frequently used class combinations:
// Subsequent calls with the same input use the cache
result1 := twerge.Merge("p-4 m-2 p-8") // Computed
result2 := twerge.Merge("p-4 m-2 p-8") // Retrieved from cache
Integration Examples
In Go-templ templates, you can use it like this:
// In a templ file
<div class={ twerge.Merge("bg-blue-500 p-4 bg-red-500") }>
This will have a red background with padding
</div>
Related Functions
Merge(classes string) string
- Merges Tailwind classesConfigureCache(size int)
- Configures the cache size for merging operationsDisableCache()
- Disables caching for merging operations
Generating Short Class Names
Twerge can generate short, unique class names based on hashes of the merged Tailwind classes, allowing for smaller HTML output and improved performance.
The Problem
Tailwind CSS is incredibly powerful, but it can lead to long class strings:
<div
class="flex flex-col items-center justify-between p-4 rounded-lg shadow-lg bg-white hover:bg-gray-50 dark:bg-gray-800 dark:hover:bg-gray-700 transition-all duration-300"
>
<!-- Content -->
</div>
These long strings increase HTML size, reduce readability, and can impact performance.
How Twerge Solves It
The Generate
function creates short, unique class names based on the hash of the merged Tailwind classes:
import "github.com/conneroisu/twerge"
// Long class string
classes := "flex flex-col items-center justify-between p-4 rounded-lg shadow-lg bg-white hover:bg-gray-50"
// Generate a short unique class name
shortClassName := twerge.It(classes)
// Result: "tw-1" (example - actual output will vary)
Benefits of Generated Class Names
- Smaller HTML - Dramatically reduces HTML file size
- Better Caching - Improves browser caching of HTML
- Consistent Naming - Same class string always generates the same short name
- Collision-Free - Hash-based generation ensures uniqueness
- Automatic Conflict Resolution - Classes are merged before hashing
How It Works
- First, Twerge merges the provided classes to resolve conflicts
- The merged class string is hashed using a fast algorithm (default is FNV-1a)
- The hash is converted to a short alphanumeric string
- A prefix is added (default is "tw-")
- The mapping from original classes to the generated name is stored
Customizing Generation
Integration Example
In a Go-templ template:
// Instead of this with long class strings
<div class="flex items-center justify-between p-4 bg-white dark:bg-gray-800">...</div>
// You can do this with a short generated class
<div class={ twerge.It("flex items-center justify-between p-4 bg-white dark:bg-gray-800") }>...</div>
// Which results in HTML like:
// <div class="tw-1">...</div>
// <div class="tw-2">...</div>
Performance Considerations
- Generated class names are cached for performance
- The same input always produces the same output
- Short names reduce HTML size and parsing time
Code Generation
Twerge provides powerful code generation capabilities, allowing you to generate Go code from class mappings for improved performance and type safety.
The Problem
When using shortened class names in a production environment, you need:
- A way to consistently map original Tailwind classes to short names
- Fast lookups without runtime overhead
- Type safety and compiler checks
- Integration with build processes
How Twerge Solves It
Twerge can generate Go code that contains the class mappings, providing compile-time checking and improved performance:
package main
import "github.com/conneroisu/twerge"
func main() {
if err := twerge.CodeGen(
twerge.Default(),
"classes/classes.go",
"input.css",
"classes/classes.html",
views.View(),
); err != nil {
panic(err)
}
}
Benefits of Code Generation
Using generated code provides several advantages:
- Performance - No runtime hash computation or map lookups
- Type Safety - Compile-time checking of class names
- Smaller Binary - Compiled code can be optimized by the Go compiler
- Build-time Validation - Issues are caught during the build process
- IDE Support - Auto-completion and refactoring support in IDEs
Basic Examples
This page provides basic examples of how to use Twerge in your Go applications.
Simple Website Example
The "simple" example demonstrates a basic website layout with header, main content, and footer sections. It shows how to use Twerge to optimize Tailwind CSS classes in a Go templ application.
Project Structure
simple/
├── _static/
│ └── dist/ # Directory for compiled CSS
├── classes/
│ ├── classes.go # Generated Go code with class mappings
│ └── classes.html # HTML output of class definitions
├── gen.go # Code generation script
├── go.mod # Go module file
├── input.css # TailwindCSS input file
├── main.go # Web server
├── tailwind.config.js # TailwindCSS configuration
└── views/
├── view.templ # Templ template file
└── view_templ.go # Generated Go code from templ
Code Generation
The gen.go
file handles Twerge code generation and TailwindCSS processing:
//go:build ignore
// +build ignore
package main
import (
"flag"
"fmt"
"os"
"os/exec"
"time"
"github.com/conneroisu/twerge"
"github.com/conneroisu/twerge/examples/simple/views"
)
var cwd = flag.String("cwd", "", "current working directory")
func main() {
start := time.Now()
defer func() {
elapsed := time.Since(start)
fmt.Printf("(update-css) Done in %s.\n", elapsed)
}()
flag.Parse()
if *cwd != "" {
err := os.Chdir(*cwd)
if err != nil {
panic(err)
}
}
fmt.Println("Updating Generated Code...")
start = time.Now()
if err := twerge.CodeGen(
twerge.Default(),
"classes/classes.go",
"input.css",
"classes/classes.html",
views.View(),
); err != nil {
panic(err)
}
fmt.Println("Done Generating Code. (took", time.Since(start), ")")
fmt.Println("Running Tailwind...")
start = time.Now()
runTailwind()
fmt.Println("Done Running Tailwind. (took", time.Since(start), ")")
}
func runTailwind() {
start := time.Now()
defer func() {
elapsed := time.Since(start)
fmt.Printf("(tailwind) Done in %s.\n", elapsed)
}()
cmd := exec.Command("tailwindcss", "-i", "input.css", "-o", "_static/dist/styles.css")
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
panic(err)
}
}
Template Usage
The view.templ
file shows how to use Twerge in a templ template:
package views
import "github.com/conneroisu/twerge"
templ View() {
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>stellar</title>
<link rel="stylesheet" href="/dist/styles.css"/>
</head>
<body class={ twerge.It("bg-gray-50 text-gray-900 flex flex-col min-h-screen") }>
<header class={ twerge.It("bg-indigo-600 text-white shadow-md") }>
<!-- Header content -->
</header>
<main class={ twerge.It("container mx-auto px-4 py-6 flex-grow") }>
<!-- Page content -->
</main>
<footer class={ twerge.It("bg-gray-800 text-white py-6") }>
<!-- Footer content -->
</footer>
</body>
</html>
}
Benefits Demonstrated
- Class Optimization - Long Tailwind class strings are converted to short, efficient class names
- Build Integration - Twerge integrates with the build process to generate optimized CSS
- Maintainability - Templates remain readable with full Tailwind class names
- Performance - Final HTML output uses short class names for improved performance
Running the Example
- Navigate to the example directory:
cd examples/simple
- Generate the templ components:
templ generate ./views
- Run the code generation:
go run gen.go
- Run the server:
go run main.go
- Open your browser and navigate to http://localhost:8080
Multiple Component Example
For applications with multiple components, you can pass all components to the CodeGen
function:
if err := twerge.CodeGen(
twerge.Default(),
"classes/classes.go",
"input.css",
"classes/classes.html",
views.Header(),
views.Footer(),
views.Sidebar(),
views.Content(),
); err != nil {
panic(err)
}
This ensures all components' Tailwind classes are included in the optimization process.
Web Server Integration
This page demonstrates how to integrate Twerge with a Go web server for delivering optimized Tailwind CSS in a production environment.
HTTP Server Example
The example below shows how to set up a simple HTTP server that serves a web application with Twerge-optimized Tailwind CSS classes.
Server Setup
package main
import (
"fmt"
"log"
"net/http"
"os"
"path/filepath"
"time"
"github.com/conneroisu/twerge/examples/simple/views"
)
func main() {
// Serve static files
fs := http.FileServer(http.Dir("_static"))
http.Handle("/dist/", http.StripPrefix("/dist/", fs))
// Index route
http.HandleFunc("/", handleIndex)
// Start server
port := getEnv("PORT", "8080")
log.Printf("Server starting on http://localhost:%s", port)
if err := http.ListenAndServe(":"+port, nil); err != nil {
log.Fatal(err)
}
}
func handleIndex(w http.ResponseWriter, r *http.Request) {
// If not at the root path, return 404
if r.URL.Path != "/" {
http.NotFound(w, r)
return
}
// Render the template
err := views.View().Render(r.Context(), w)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
// Helper to get environment variable with default
func getEnv(key, fallback string) string {
if value, ok := os.LookupEnv(key); ok {
return value
}
return fallback
}
Middleware for Cache Control
For production, you might want to add cache control headers for better performance:
package main
import (
"net/http"
"strings"
"time"
)
// CacheControlMiddleware adds cache control headers for static assets
func CacheControlMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Set cache headers for static assets
if strings.HasPrefix(r.URL.Path, "/dist/") {
w.Header().Set("Cache-Control", "public, max-age=31536000") // 1 year
w.Header().Set("Expires", time.Now().Add(time.Hour*24*365).Format(time.RFC1123))
}
next.ServeHTTP(w, r)
})
}
// Usage example
func setupServer() {
staticHandler := http.FileServer(http.Dir("_static"))
http.Handle("/dist/", CacheControlMiddleware(http.StripPrefix("/dist/", staticHandler)))
}
Template Composition
When building larger applications, you'll want to compose templates. Here's how to use Twerge with template composition:
package views
import "github.com/conneroisu/twerge"
templ Layout(title string) {
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>{ title }</title>
<link rel="stylesheet" href="/dist/styles.css"/>
</head>
<body class={ twerge.It("bg-gray-50 text-gray-900 flex flex-col min-h-screen") }>
<header class={ twerge.It("bg-indigo-600 text-white shadow-md") }>
<!-- Header content -->
</header>
<!-- Slot for page content -->
{ children... }
<footer class={ twerge.It("bg-gray-800 text-white py-6") }>
<!-- Footer content -->
</footer>
</body>
</html>
}
// Usage in page templates
templ AboutPage() {
@Layout("About Us") {
<main class={ twerge.It("container mx-auto px-4 py-6 flex-grow") }>
<h1 class={ twerge.It("text-3xl font-bold mb-6") }>About Us</h1>
<p class={ twerge.It("text-gray-700") }>Content goes here...</p>
</main>
}
}
API Routes with JSON
If you're building an API that serves both HTML and JSON, you can structure your handlers like this:
package main
import (
"encoding/json"
"net/http"
"github.com/conneroisu/twerge/examples/simple/views"
)
// Struct for JSON response
type ApiResponse struct {
Status string `json:"status"`
Message string `json:"message"`
Data interface{} `json:"data,omitempty"`
}
// Handler that can respond with HTML or JSON
func handleData(w http.ResponseWriter, r *http.Request) {
data := map[string]interface{}{
"name": "Twerge Example",
"description": "A demonstration of Twerge with web servers",
}
// Check Accept header for JSON request
if r.Header.Get("Accept") == "application/json" {
w.Header().Set("Content-Type", "application/json")
response := ApiResponse{
Status: "success",
Data: data,
}
json.NewEncoder(w).Encode(response)
return
}
// Otherwise render HTML
err := views.DataView(data).Render(r.Context(), w)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
Performance Considerations
When using Twerge with a web server, consider these performance optimizations:
- Precompiled Templates: Generate all templ components at build time
- Static File Serving: Use proper cache headers for static assets
- Compression: Enable gzip/brotli compression for CSS and HTML
- CDN Integration: Consider serving static assets from a CDN
package main
import (
"net/http"
"strings"
"github.com/NYTimes/gziphandler"
)
func setupCompression() {
// Apply gzip compression to static assets
staticHandler := http.FileServer(http.Dir("_static"))
compressedHandler := gziphandler.GzipHandler(http.StripPrefix("/dist/", staticHandler))
http.Handle("/dist/", compressedHandler)
}
Example Server with All Features
Here's a complete example integrating all the features mentioned above:
package main
import (
"context"
"log"
"net/http"
"os"
"os/signal"
"syscall"
"time"
"github.com/NYTimes/gziphandler"
"github.com/conneroisu/twerge/examples/simple/views"
)
func main() {
// Create router
mux := http.NewServeMux()
// Static files with compression and caching
staticHandler := http.FileServer(http.Dir("_static"))
compressedHandler := gziphandler.GzipHandler(http.StripPrefix("/dist/", staticHandler))
cachedHandler := CacheControlMiddleware(compressedHandler)
mux.Handle("/dist/", cachedHandler)
// Routes
mux.HandleFunc("/", handleIndex)
mux.HandleFunc("/api/data", handleData)
// Configure server
server := &http.Server{
Addr: ":" + getEnv("PORT", "8080"),
Handler: mux,
ReadTimeout: 5 * time.Second,
WriteTimeout: 10 * time.Second,
IdleTimeout: 120 * time.Second,
}
// Start server in a goroutine
go func() {
log.Printf("Server starting on http://localhost%s", server.Addr)
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("Server error: %v", err)
}
}()
// Graceful shutdown
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
log.Println("Shutting down server...")
if err := server.Shutdown(ctx); err != nil {
log.Fatalf("Server forced to shutdown: %v", err)
}
log.Println("Server gracefully stopped")
}
This comprehensive example demonstrates how to integrate Twerge with a production-ready web server, complete with compression, caching, and graceful shutdown.
Tailwind Build Integration
This page shows how to integrate Twerge with your Tailwind CSS build process for optimal performance and developer experience.
Build Process Overview
A typical Twerge-Tailwind integration includes these steps:
- Scan your templates for Tailwind classes
- Generate optimized class mappings in Go code
- Process CSS with Tailwind CLI
- Serve the optimized CSS and HTML
Basic Build Script
Here's a simple build script that handles code generation and Tailwind processing:
//go:build ignore
// +build ignore
package main
import (
"flag"
"fmt"
"os"
"os/exec"
"time"
"github.com/conneroisu/twerge"
"github.com/conneroisu/twerge/examples/simple/views"
)
var cwd = flag.String("cwd", "", "current working directory")
func main() {
start := time.Now()
defer func() {
elapsed := time.Since(start)
fmt.Printf("(update-css) Done in %s.\n", elapsed)
}()
flag.Parse()
if *cwd != "" {
err := os.Chdir(*cwd)
if err != nil {
panic(err)
}
}
fmt.Println("Updating Generated Code...")
start = time.Now()
if err := twerge.CodeGen(
twerge.Default(),
"classes/classes.go",
"input.css",
"classes/classes.html",
views.View(),
); err != nil {
panic(err)
}
fmt.Println("Done Generating Code. (took", time.Since(start), ")")
fmt.Println("Running Tailwind...")
start = time.Now()
runTailwind()
fmt.Println("Done Running Tailwind. (took", time.Since(start), ")")
}
func runTailwind() {
start := time.Now()
defer func() {
elapsed := time.Since(start)
fmt.Printf("(tailwind) Done in %s.\n", elapsed)
}()
cmd := exec.Command("tailwindcss", "-i", "input.css", "-o", "_static/dist/styles.css")
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
panic(err)
}
}
Tailwind Configuration
Here's a sample tailwind.config.js
that works well with Twerge:
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
'./classes/classes.html', // Generated HTML classes from Twerge
'./views/**/*.templ', // Optional - you can include your templates directly too
],
theme: {
extend: {
colors: {
primary: '#3b82f6',
secondary: '#10b981',
accent: '#8b5cf6',
},
},
},
plugins: [],
}
Development Workflow
For development, you'll want to automatically rebuild when files change. Here's a simple watch script you can use:
//go:build ignore
// +build ignore
package main
import (
"log"
"os"
"os/exec"
"path/filepath"
"time"
"github.com/fsnotify/fsnotify"
)
func main() {
watcher, err := fsnotify.NewWatcher()
if err != nil {
log.Fatal(err)
}
defer watcher.Close()
// Start the build once at beginning
runBuild()
// Watch directories
dirsToWatch := []string{"./views", "./input.css", "./tailwind.config.js"}
for _, dir := range dirsToWatch {
err = watcher.Add(dir)
if err != nil {
log.Fatal(err)
}
}
// Watch for changes recursively in views directory
err = filepath.Walk("./views", func(path string, info os.FileInfo, err error) error {
if info.IsDir() {
return watcher.Add(path)
}
return nil
})
if err != nil {
log.Fatal(err)
}
// Debounce builds
var lastBuild time.Time
debounceInterval := 500 * time.Millisecond
log.Println("Watching for changes...")
for {
select {
case event, ok := <-watcher.Events:
if !ok {
return
}
// Only rebuild on write or create events for .templ, .css, or .js files
if (event.Op&fsnotify.Write == fsnotify.Write || event.Op&fsnotify.Create == fsnotify.Create) &&
(filepath.Ext(event.Name) == ".templ" || filepath.Ext(event.Name) == ".css" || filepath.Ext(event.Name) == ".js") {
// Debounce
if time.Since(lastBuild) > debounceInterval {
lastBuild = time.Now()
log.Println("Change detected, rebuilding...")
runBuild()
}
}
case err, ok := <-watcher.Errors:
if !ok {
return
}
log.Println("error:", err)
}
}
}
func runBuild() {
// First generate templ components
cmdTempl := exec.Command("templ", "generate", "./views")
cmdTempl.Stdout = os.Stdout
cmdTempl.Stderr = os.Stderr
if err := cmdTempl.Run(); err != nil {
log.Println("Error generating templ:", err)
return
}
// Then run our build script
cmdGen := exec.Command("go", "run", "gen.go")
cmdGen.Stdout = os.Stdout
cmdGen.Stderr = os.Stderr
if err := cmdGen.Run(); err != nil {
log.Println("Error running gen.go:", err)
return
}
log.Println("Build completed successfully")
}
Production Build Optimization
For production builds, you'll want to minify your CSS and use Tailwind's purge feature to remove unused styles:
//go:build ignore
// +build ignore
package main
import (
"flag"
"fmt"
"os"
"os/exec"
"time"
"github.com/conneroisu/twerge"
"github.com/conneroisu/twerge/examples/simple/views"
)
var (
cwd = flag.String("cwd", "", "current working directory")
prod = flag.Bool("prod", false, "production build (minified)")
)
func main() {
start := time.Now()
defer func() {
elapsed := time.Since(start)
fmt.Printf("(update-css) Done in %s.\n", elapsed)
}()
flag.Parse()
if *cwd != "" {
err := os.Chdir(*cwd)
if err != nil {
panic(err)
}
}
fmt.Println("Updating Generated Code...")
start = time.Now()
if err := twerge.CodeGen(
twerge.Default(),
"classes/classes.go",
"input.css",
"classes/classes.html",
views.View(),
); err != nil {
panic(err)
}
fmt.Println("Done Generating Code. (took", time.Since(start), ")")
fmt.Println("Running Tailwind...")
start = time.Now()
runTailwind(*prod)
fmt.Println("Done Running Tailwind. (took", time.Since(start), ")")
}
func runTailwind(prod bool) {
start := time.Now()
defer func() {
elapsed := time.Since(start)
fmt.Printf("(tailwind) Done in %s.\n", elapsed)
}()
args := []string{"-i", "input.css", "-o", "_static/dist/styles.css"}
if prod {
args = append(args, "--minify")
}
cmd := exec.Command("tailwindcss", args...)
cmd.Env = append(os.Environ(), "NODE_ENV=production")
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
panic(err)
}
}
Usage:
# Development build
go run gen_prod.go
# Production build (minified)
go run gen_prod.go -prod
Makefile Integration
You can create a Makefile to simplify common build tasks:
.PHONY: dev build watch clean prod
dev:
templ generate ./views
go run gen.go
watch:
go run watch.go
build:
templ generate ./views
go run gen.go
go build -o app ./main.go
prod:
templ generate ./views
go run gen_prod.go -prod
go build -o app -ldflags="-s -w" ./main.go
clean:
rm -f app
rm -rf _static/dist/*
GitHub Actions Example
You can automate the build process in CI/CD with GitHub Actions:
name: Build and Deploy
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Go
uses: actions/setup-go@v4
with:
go-version: '1.24'
- name: Install templ
run: go install github.com/a-h/templ/cmd/templ@latest
- name: Install tailwindcss
run: npm install -g tailwindcss
- name: Generate templ components
run: templ generate ./views
- name: Build production CSS
run: go run gen_prod.go -prod
- name: Build application
run: go build -o app -ldflags="-s -w" ./main.go
- name: Upload artifacts
uses: actions/upload-artifact@v3
with:
name: app-build
path: |
app
_static/dist
Docker Integration
For containerized deployments, here's a sample Dockerfile:
# Build stage
FROM golang:1.24-alpine AS builder
# Install Node.js and npm for Tailwind
RUN apk add --no-cache nodejs npm
# Install Tailwind CSS
RUN npm install -g tailwindcss
# Install templ
RUN go install github.com/a-h/templ/cmd/templ@latest
WORKDIR /app
# Copy dependencies first for better caching
COPY go.mod go.sum ./
RUN go mod download
# Copy source files
COPY . .
# Generate templ files
RUN templ generate ./views
# Build with Twerge
RUN go run gen_prod.go -prod
# Build the Go application
RUN CGO_ENABLED=0 GOOS=linux go build -o app -ldflags="-s -w" ./main.go
# Runtime stage
FROM alpine:latest
WORKDIR /app
# Copy only necessary files from the builder stage
COPY --from=builder /app/app .
COPY --from=builder /app/_static/dist _static/dist
# Expose the port the app runs on
EXPOSE 8080
# Run the application
CMD ["./app"]
Multi-Theme Support
You can extend your build script to generate multiple theme variants:
//go:build ignore
// +build ignore
package main
import (
"flag"
"fmt"
"os"
"os/exec"
"path/filepath"
"time"
"github.com/conneroisu/twerge"
"github.com/conneroisu/twerge/examples/simple/views"
)
var (
cwd = flag.String("cwd", "", "current working directory")
theme = flag.String("theme", "default", "theme name (default, dark, custom)")
)
func main() {
start := time.Now()
defer func() {
elapsed := time.Since(start)
fmt.Printf("(update-css) Done in %s.\n", elapsed)
}()
flag.Parse()
if *cwd != "" {
err := os.Chdir(*cwd)
if err != nil {
panic(err)
}
}
// Determine input file based on theme
inputCss := "input.css"
if *theme != "default" {
inputCss = fmt.Sprintf("input-%s.css", *theme)
}
// Determine output directory based on theme
outputDir := filepath.Join("_static", "dist")
outputCss := filepath.Join(outputDir, "styles.css")
if *theme != "default" {
outputCss = filepath.Join(outputDir, fmt.Sprintf("styles-%s.css", *theme))
}
fmt.Printf("Building theme: %s\n", *theme)
fmt.Printf("Input CSS: %s\n", inputCss)
fmt.Printf("Output CSS: %s\n", outputCss)
fmt.Println("Updating Generated Code...")
start = time.Now()
if err := twerge.CodeGen(
twerge.Default(),
"classes/classes.go",
inputCss,
"classes/classes.html",
views.View(),
); err != nil {
panic(err)
}
fmt.Println("Done Generating Code. (took", time.Since(start), ")")
fmt.Println("Running Tailwind...")
start = time.Now()
runTailwind(inputCss, outputCss)
fmt.Println("Done Running Tailwind. (took", time.Since(start), ")")
}
func runTailwind(input, output string) {
start := time.Now()
defer func() {
elapsed := time.Since(start)
fmt.Printf("(tailwind) Done in %s.\n", elapsed)
}()
// Create output directory if it doesn't exist
outputDir := filepath.Dir(output)
if err := os.MkdirAll(outputDir, 0755); err != nil {
panic(err)
}
cmd := exec.Command("tailwindcss", "-i", input, "-o", output)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
panic(err)
}
}
Usage:
# Build default theme
go run gen_themes.go
# Build dark theme
go run gen_themes.go -theme dark
This example demonstrates how to integrate Twerge into your Tailwind CSS build process for both development and production environments.
Complex Web Application
This page demonstrates using Twerge in a more complex web application scenario, showcasing a dashboard example with multiple components and dynamic data.
Dashboard Example Overview
The dashboard example demonstrates a modern analytics UI with:
- Responsive layout using Tailwind CSS
- Multiple templ components for different sections
- Metrics cards with dynamic data
- Data tables for displaying information
- Twerge optimization for class handling
Project Structure
dashboard/
├── _static/
│ └── dist/ # Directory for compiled CSS
├── classes/
│ ├── classes.go # Generated Go code with class mappings
│ └── classes.html # HTML output of class definitions
├── gen.go # Code generation script
├── go.mod # Go module file
├── input.css # TailwindCSS input file
├── main.go # Web server implementation
├── tailwind.config.js # TailwindCSS configuration
└── views/
├── dashboard.templ # Dashboard page component
├── report.templ # Report page component
├── settings.templ # Settings page component
└── view.templ # Layout component
Code Generation with Multiple Components
The dashboard example uses multiple components, all processed by Twerge:
//go:build ignore
// +build ignore
package main
import (
"flag"
"fmt"
"os"
"os/exec"
"time"
"github.com/conneroisu/twerge"
"github.com/conneroisu/twerge/examples/dashboard/views"
)
var cwd = flag.String("cwd", "", "current working directory")
func main() {
start := time.Now()
defer func() {
elapsed := time.Since(start)
fmt.Printf("(update-css) Done in %s.\n", elapsed)
}()
flag.Parse()
if *cwd != "" {
err := os.Chdir(*cwd)
if err != nil {
panic(err)
}
}
fmt.Println("Updating Generated Code...")
start = time.Now()
if err := twerge.CodeGen(
twerge.Default(),
"classes/classes.go",
"input.css",
"classes/classes.html",
views.Dashboard(),
views.Settings(),
views.Report(),
); err != nil {
panic(err)
}
fmt.Println("Done Generating Code. (took", time.Since(start), ")")
fmt.Println("Running Tailwind...")
start = time.Now()
runTailwind()
fmt.Println("Done Running Tailwind. (took", time.Since(start), ")")
}
func runTailwind() {
start := time.Now()
defer func() {
elapsed := time.Since(start)
fmt.Printf("(tailwind) Done in %s.\n", elapsed)
}()
cmd := exec.Command("tailwindcss", "-i", "input.css", "-o", "_static/dist/styles.css")
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
panic(err)
}
}
Dashboard Component
The dashboard component displays metrics cards and a data table:
package views
import "github.com/conneroisu/twerge"
templ Dashboard() {
@Layout("Dashboard") {
<main class={ twerge.It("container mx-auto px-4 py-6 flex-grow") }>
<div class={ twerge.It("mb-6") }>
<h2 class={ twerge.It("text-2xl font-bold text-gray-800 mb-4") }>Overview</h2>
<div class={ twerge.It("grid grid-cols-1 md:grid-cols-3 gap-6") }>
<div class={ twerge.It("bg-white rounded-lg shadow-md p-6") }>
<div class={ twerge.It("flex items-center justify-between") }>
<h3 class={ twerge.It("text-gray-500 text-sm font-medium") }>Total Users</h3>
<span class={ twerge.It("bg-green-100 text-green-800 text-xs font-semibold px-2 py-1 rounded") }>+12%</span>
</div>
<p class={ twerge.It("text-3xl font-bold text-gray-800 mt-2") }>24,521</p>
<div class={ twerge.It("mt-4 text-sm text-gray-500") }>1,250 new users this week</div>
</div>
<!-- Additional metric cards -->
</div>
</div>
<div class={ twerge.It("mb-6") }>
<h2 class={ twerge.It("text-2xl font-bold text-gray-800 mb-4") }>Recent Activity</h2>
<div class={ twerge.It("bg-white rounded-lg shadow-md overflow-hidden") }>
<table class={ twerge.It("min-w-full divide-y divide-gray-200") }>
<!-- Table header and rows -->
</table>
</div>
</div>
</main>
}
}
Settings Component
The settings page demonstrates form handling with Tailwind:
package views
import "github.com/conneroisu/twerge"
templ Settings() {
@Layout("Settings") {
<main class={ twerge.It("container mx-auto px-4 py-6 flex-grow") }>
<h2 class={ twerge.It("text-2xl font-bold text-gray-800 mb-6") }>Account Settings</h2>
<div class={ twerge.It("bg-white rounded-lg shadow-md p-6 mb-6") }>
<h3 class={ twerge.It("text-lg font-medium text-gray-800 mb-4") }>Profile Information</h3>
<form>
<div class={ twerge.It("grid grid-cols-1 gap-6 md:grid-cols-2") }>
<div>
<label for="name" class={ twerge.It("block text-sm font-medium text-gray-700 mb-1") }>Name</label>
<input type="text" id="name" name="name" value="John Doe"
class={ twerge.It("w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500") }/>
</div>
<div>
<label for="email" class={ twerge.It("block text-sm font-medium text-gray-700 mb-1") }>Email</label>
<input type="email" id="email" name="email" value="john@example.com"
class={ twerge.It("w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500") }/>
</div>
<!-- More form fields -->
</div>
<div class={ twerge.It("mt-6") }>
<button type="submit" class={ twerge.It("px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2") }>
Save Changes
</button>
</div>
</form>
</div>
<!-- Additional settings sections -->
</main>
}
}
Report Component
The report page shows how to present data visualizations:
package views
import "github.com/conneroisu/twerge"
templ Report() {
@Layout("Analytics Report") {
<main class={ twerge.It("container mx-auto px-4 py-6 flex-grow") }>
<div class={ twerge.It("flex justify-between items-center mb-6") }>
<h2 class={ twerge.It("text-2xl font-bold text-gray-800") }>Analytics Report</h2>
<div class={ twerge.It("flex space-x-2") }>
<button class={ twerge.It("px-3 py-1 bg-white border border-gray-300 rounded-md text-sm text-gray-700 hover:bg-gray-50") }>
Export PDF
</button>
<button class={ twerge.It("px-3 py-1 bg-white border border-gray-300 rounded-md text-sm text-gray-700 hover:bg-gray-50") }>
Export CSV
</button>
</div>
</div>
<div class={ twerge.It("grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6") }>
<div class={ twerge.It("bg-white rounded-lg shadow-md p-6") }>
<h3 class={ twerge.It("text-lg font-medium text-gray-800 mb-4") }>Revenue Overview</h3>
<div class={ twerge.It("h-64 bg-gray-100 rounded flex items-center justify-center") }>
<p class={ twerge.It("text-gray-500") }>Chart Placeholder</p>
</div>
</div>
<div class={ twerge.It("bg-white rounded-lg shadow-md p-6") }>
<h3 class={ twerge.It("text-lg font-medium text-gray-800 mb-4") }>User Growth</h3>
<div class={ twerge.It("h-64 bg-gray-100 rounded flex items-center justify-center") }>
<p class={ twerge.It("text-gray-500") }>Chart Placeholder</p>
</div>
</div>
</div>
<!-- Additional report sections -->
</main>
}
}
Layout Component with Template Composition
The layout component used by all pages for consistent structure:
package views
import "github.com/conneroisu/twerge"
templ Layout(title string) {
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>{ title } | Dashboard</title>
<link rel="stylesheet" href="/dist/styles.css"/>
</head>
<body class={ twerge.It("bg-gray-100 text-gray-900 flex") }>
<!-- Sidebar -->
<aside class={ twerge.It("hidden md:flex md:flex-col w-64 bg-gray-800 text-white") }>
<div class={ twerge.It("flex items-center justify-center h-16 border-b border-gray-700") }>
<h1 class={ twerge.It("text-xl font-bold") }>Dashboard</h1>
</div>
<nav class={ twerge.It("flex-grow") }>
<ul class={ twerge.It("mt-6") }>
<li>
<a href="/" class={ twerge.It("flex items-center px-4 py-3 text-gray-300 hover:bg-gray-700") }>
<span class={ twerge.It("ml-2") }>Dashboard</span>
</a>
</li>
<li>
<a href="/reports" class={ twerge.It("flex items-center px-4 py-3 text-gray-300 hover:bg-gray-700") }>
<span class={ twerge.It("ml-2") }>Reports</span>
</a>
</li>
<li>
<a href="/settings" class={ twerge.It("flex items-center px-4 py-3 text-gray-300 hover:bg-gray-700") }>
<span class={ twerge.It("ml-2") }>Settings</span>
</a>
</li>
</ul>
</nav>
</aside>
<!-- Main content -->
<div class={ twerge.It("flex flex-col flex-grow min-h-screen") }>
<!-- Top navbar -->
<header class={ twerge.It("bg-white shadow h-16 flex items-center justify-between px-6") }>
<div class={ twerge.It("flex items-center") }>
<button class={ twerge.It("md:hidden mr-4 text-gray-600") }>
<svg class={ twerge.It("h-6 w-6") } fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"/>
</svg>
</button>
<h2 class={ twerge.It("text-lg font-medium") }>{ title }</h2>
</div>
<div class={ twerge.It("flex items-center") }>
<button class={ twerge.It("p-1 mr-4 text-gray-500") }>
<svg class={ twerge.It("h-6 w-6") } fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9"/>
</svg>
</button>
<div class={ twerge.It("relative") }>
<button class={ twerge.It("flex items-center") }>
<img class={ twerge.It("h-8 w-8 rounded-full") } src="https://randomuser.me/api/portraits/men/1.jpg" alt="User profile"/>
<span class={ twerge.It("ml-2 text-sm") }>John Doe</span>
</button>
</div>
</div>
</header>
<!-- Page content -->
{ children... }
</div>
</body>
</html>
}
Web Server Implementation
The main.go file sets up routes for each component:
package main
import (
"log"
"net/http"
"os"
"github.com/conneroisu/twerge/examples/dashboard/views"
)
func main() {
// Static file handler
fs := http.FileServer(http.Dir("_static"))
http.Handle("/dist/", http.StripPrefix("/dist/", fs))
// Routes
http.HandleFunc("/", handleDashboard)
http.HandleFunc("/reports", handleReports)
http.HandleFunc("/settings", handleSettings)
// Start server
port := getEnv("PORT", "8080")
log.Printf("Server starting on http://localhost:%s", port)
if err := http.ListenAndServe(":"+port, nil); err != nil {
log.Fatal(err)
}
}
func handleDashboard(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/" {
http.NotFound(w, r)
return
}
err := views.Dashboard().Render(r.Context(), w)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
func handleReports(w http.ResponseWriter, r *http.Request) {
err := views.Report().Render(r.Context(), w)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
func handleSettings(w http.ResponseWriter, r *http.Request) {
err := views.Settings().Render(r.Context(), w)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
// Helper to get environment variable with default
func getEnv(key, fallback string) string {
if value, ok := os.LookupEnv(key); ok {
return value
}
return fallback
}
Dynamic Data Integration
In a real application, you'd likely integrate with a database or API. Here's an example of how to pass dynamic data to templates:
package models
type User struct {
ID int
Name string
Email string
Role string
Avatar string
LastSeen string
}
type MetricCard struct {
Title string
Value string
Change string
IsUp bool
Subtitle string
}
type Order struct {
ID string
Customer string
Amount string
Status string
Date string
}
// Repository interface
type DataRepository interface {
GetUsers() []User
GetMetrics() []MetricCard
GetRecentOrders() []Order
}
package data
import "github.com/example/dashboard/models"
type Repository struct {
// Database connection or other dependencies
}
func NewRepository() *Repository {
return &Repository{}
}
func (r *Repository) GetMetrics() []models.MetricCard {
return []models.MetricCard{
{
Title: "Total Users",
Value: "24,521",
Change: "+12%",
IsUp: true,
Subtitle: "1,250 new users this week",
},
{
Title: "Total Revenue",
Value: "$45,428",
Change: "+5%",
IsUp: true,
Subtitle: "$2,150 new revenue this week",
},
{
Title: "Total Orders",
Value: "12,234",
Change: "-2%",
IsUp: false,
Subtitle: "345 new orders this week",
},
}
}
// Implement other methods...
package views
import (
"github.com/conneroisu/twerge"
"github.com/example/dashboard/models"
)
templ MetricCard(card models.MetricCard) {
<div class={ twerge.It("bg-white rounded-lg shadow-md p-6") }>
<div class={ twerge.It("flex items-center justify-between") }>
<h3 class={ twerge.It("text-gray-500 text-sm font-medium") }>{ card.Title }</h3>
if card.IsUp {
<span class={ twerge.It("bg-green-100 text-green-800 text-xs font-semibold px-2 py-1 rounded") }>{ card.Change }</span>
} else {
<span class={ twerge.It("bg-red-100 text-red-800 text-xs font-semibold px-2 py-1 rounded") }>{ card.Change }</span>
}
</div>
<p class={ twerge.It("text-3xl font-bold text-gray-800 mt-2") }>{ card.Value }</p>
<div class={ twerge.It("mt-4 text-sm text-gray-500") }>{ card.Subtitle }</div>
</div>
}
templ Dashboard(metrics []models.MetricCard, orders []models.Order) {
@Layout("Dashboard") {
<main class={ twerge.It("container mx-auto px-4 py-6 flex-grow") }>
<div class={ twerge.It("mb-6") }>
<h2 class={ twerge.It("text-2xl font-bold text-gray-800 mb-4") }>Overview</h2>
<div class={ twerge.It("grid grid-cols-1 md:grid-cols-3 gap-6") }>
for _, metric := range metrics {
@MetricCard(metric)
}
</div>
</div>
<!-- Table with orders... -->
</main>
}
}
Benefits of This Approach
The complex dashboard example demonstrates several benefits of using Twerge:
- Component Reusability - Common UI elements can be extracted into reusable components
- Optimized Output - Large Tailwind class strings are converted to short, efficient codes
- Type Safety - Generated Go code provides compile-time checking
- Performance - HTML output is smaller and faster to parse
- Maintainability - Templates remain readable with full Tailwind class names
- Dynamic Data Integration - Easy to integrate with databases or APIs
Running the Example
- Navigate to the example directory:
cd examples/dashboard
- Generate the templ components:
templ generate ./views
- Run the code generation:
go run gen.go
- Run the server:
go run main.go
- Open your browser and navigate to http://localhost:8080
The dashboard example demonstrates how Twerge can be used in complex web applications with multiple components, layouts, and dynamic data integration.
Frequently Asked Questions
This page answers common questions about using Twerge with Tailwind CSS and Go.
General Questions
What is Twerge?
Twerge is a Go library that optimizes Tailwind CSS class usage in Go web applications. It provides class merging, short class name generation, and code generation features to improve performance and developer experience.
Why should I use Twerge?
Twerge solves several common challenges when working with Tailwind CSS:
- It correctly merges Tailwind classes, resolving conflicts according to Tailwind's specificity rules
- It generates short, unique class names to reduce HTML size
- It creates Go code with class mappings for improved performance and type safety
- It integrates smoothly with Go build processes and templ templates
How does Twerge compare to similar tools?
Unlike JavaScript-based tools like tailwind-merge
or clsx
, Twerge is designed specifically for Go applications. It integrates natively with Go code, templ templates, and build processes to provide a seamless development experience.
Technical Questions
Does Twerge support all Tailwind CSS features?
Twerge supports all standard Tailwind CSS classes, including:
- Layout and positioning
- Flexbox and Grid
- Typography
- Colors and backgrounds
- Borders and shadows
- Transitions and animations
- Responsive variants
- Dark mode variants
- Hover, focus, and other state variants
For custom Tailwind plugins or very specific edge cases, check the documentation or submit an issue on GitHub.
How does class conflict resolution work?
Twerge follows Tailwind's own conflict resolution rules:
- The last conflicting class wins (e.g.,
text-red-500 text-blue-500
results in blue text) - Classes are grouped by category for readability and optimization
- An internal mapping handles all standard Tailwind class conflicts
What's the performance impact?
Twerge is designed for performance:
- Class merging uses an LRU cache for frequently used combinations
- Generated code offers zero runtime overhead for class lookups
- Short class names reduce HTML size and improve parsing time
- The build-time approach means no client-side JavaScript overhead
Tests show that using Twerge can reduce HTML size by 30-50% for Tailwind-heavy pages.
Can I use Twerge with existing projects?
Yes, you can integrate Twerge into existing Go web projects that use Tailwind CSS. The integration process is straightforward:
- Install Twerge:
go get github.com/conneroisu/twerge
- Update your templates to use
twerge.It()
ortwerge.Merge()
- Set up code generation in your build process
- Configure TailwindCSS to use the generated HTML classes
See the Examples section for detailed integration guides.
Common Issues
Classes aren't being applied correctly
If classes aren't being applied correctly, check:
- Make sure the generated CSS is being included in your HTML
- Verify that your templates are using
twerge.It()
correctly - Check that code generation is running before the Tailwind build
- Inspect the generated classes.html file to ensure your classes are included
Build errors in code generation
If you're seeing build errors:
- Ensure you're using the latest version of Twerge
- Check that your templ templates are valid and compiling correctly
- Verify that the paths in your CodeGen function are correct
- Check the generated code for any issues
Performance concerns
If you're concerned about performance:
- Use the code generation approach for production builds
- Configure appropriate cache sizes for your application
- Consider enabling minification in your Tailwind build
- Use HTTP compression for serving HTML and CSS
Usage Examples
How do I merge classes conditionally?
// Conditional class merging
func Button(primary bool) templ.Component {
classes := "px-4 py-2 rounded"
if primary {
classes = twerge.Merge(classes + " bg-blue-600 text-white")
} else {
classes = twerge.Merge(classes + " bg-gray-200 text-gray-800")
}
return templ.ComponentFunc(func(ctx context.Context, w io.Writer) error {
return templ.Tag("button", templ.Attributes{
"class": classes,
}).Render(ctx, w)
})
}
How do I integrate with CI/CD pipelines?
For CI/CD pipelines, include the Twerge code generation step in your build process:
# GitHub Actions example
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Go
uses: actions/setup-go@v4
with:
go-version: '1.24'
- name: Install dependencies
run: |
go install github.com/a-h/templ/cmd/templ@latest
npm install -g tailwindcss
- name: Generate templ code
run: templ generate ./views
- name: Run Twerge code generation
run: go run ./gen.go
- name: Build application
run: go build -o app
Getting Help
If you have questions not covered in this FAQ:
- Check the documentation
- Search for existing issues
- Open a new issue if you think you've found a bug
- Join the community discussions on GitHub or Discord
Acknowledgements
Original Works
Special thanks to Oudwins for his work on the original tailwind-merge-go:
https://github.com/Oudwins/tailwind-merge-go
Projects That Inspired Twerge
- Tailwind CSS - The utility-first CSS framework that makes rapid UI development possible
- tailwind-merge - The original JavaScript library for merging Tailwind CSS classes
- tailwind-merge-go - Go port of the tailwind-merge library
- templ - HTML templating language for Go that inspired Twerge's integration patterns
Libraries Used
- Jennifer - Used for code generation capabilities
- testify - For assertions in tests
- Various Go standard library packages
Contributors
Thank you to all contributors who have helped improve Twerge through bug reports, feature suggestions, and code contributions.
Community
Thanks to the broader Go and Tailwind CSS communities for creating an ecosystem where tools like Twerge can thrive.