Skip to content

Commit 7e1eb90

Browse files
committed
Add SPA fallback support and detailed project guidelines
- Introduced Single Page Application (SPA) fallback mode for 404 handling. - Added a comprehensive `CLAUDE.md` guide with project architecture, configuration, and commands.
1 parent 66f597e commit 7e1eb90

6 files changed

Lines changed: 290 additions & 190 deletions

File tree

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
.idea
2+
.claude
23
dist
4+
asws
35

46
# Binaries for programs and plugins
57
*.exe

CLAUDE.md

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
# CLAUDE.md
2+
3+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4+
5+
## Project Overview
6+
7+
ASWS (A Static Web Server) is a lightweight, configurable static web server built with Go. It uses the Gin web framework and includes built-in Prometheus metrics support. The project is designed to be deployed as a Docker container, with multi-architecture support via GoReleaser.
8+
9+
## Architecture
10+
11+
### Single Binary Application
12+
- Main entry point: `cmd/asws.go`
13+
- Self-contained Go binary with no external dependencies
14+
- All configuration via environment variables or command-line flags
15+
- No test files present in the codebase
16+
17+
### Core Components
18+
19+
**Web Server (`cmd/asws.go`)**
20+
- Uses Gin web framework in release mode (or debug mode when DEBUG=true)
21+
- Structured logging via uber/zap with request logging middleware
22+
- Two-server architecture:
23+
- Main server: Serves static content (configurable IP/port)
24+
- Metrics server: Prometheus metrics endpoint (runs in goroutine on separate port)
25+
26+
**Static File Serving**
27+
- Primary static path: Configurable via STATIC_PATH/STATIC_DIR (default: "/" serves from "./www")
28+
- Optional filesystem mode: Additional file browsing via FS_PATH/FS_DIR (default: "/files" from "./files")
29+
- Custom 404 handling: Can serve custom 404.html or redirect to a path
30+
31+
**Metrics & Observability**
32+
- Prometheus metrics exposed on separate port (default: 2112)
33+
- Service info metrics include Go version, service version, and service name
34+
- Request logging integrated with zap logger
35+
36+
## Common Commands
37+
38+
### Development
39+
40+
**Build the application:**
41+
```bash
42+
go build -o asws ./cmd/asws.go
43+
```
44+
45+
**Run locally:**
46+
```bash
47+
go run ./cmd/asws.go -debug=true -port=8080
48+
```
49+
50+
**Run with custom configuration:**
51+
```bash
52+
DEBUG=true PORT=8080 STATIC_DIR=./www go run ./cmd/asws.go
53+
```
54+
55+
### Docker
56+
57+
**Build Docker image:**
58+
```bash
59+
docker build -t asws:local .
60+
```
61+
62+
**Run Docker container:**
63+
```bash
64+
docker run -e DEBUG=true -p 2701:80 -v "$(pwd)"/www:/www txn2/asws:latest
65+
```
66+
67+
### Release Management
68+
69+
**Test release locally (without publishing):**
70+
```bash
71+
goreleaser --skip-publish --rm-dist --skip-validate
72+
```
73+
74+
**Create and publish release:**
75+
```bash
76+
GITHUB_TOKEN=$GITHUB_TOKEN goreleaser --rm-dist
77+
```
78+
79+
## Configuration
80+
81+
All configuration uses environment variables or command-line flags (flags override env vars). Key variables:
82+
83+
- **IP/PORT**: Bind address and main server port (default: 127.0.0.1:8080)
84+
- **STATIC_DIR/STATIC_PATH**: Where and how to serve static files (default: ./www at /)
85+
- **FS_ENABLED/FS_DIR/FS_PATH**: Optional file browsing endpoint (default: disabled)
86+
- **NOT_FOUND_REDIRECT/NOT_FOUND_REDIRECT_PATH**: Redirect behavior for 404s (default: false)
87+
- **NOT_FOUND_FILE**: Custom 404 page (default: ./www/404.html)
88+
- **DEBUG**: Enable debug mode and verbose logging (default: false)
89+
- **METRICS/METRICS_PORT**: Prometheus metrics server (default: enabled on 2112)
90+
- **APP_NAME/APP_VERSION**: Application metadata for logging
91+
92+
See `cmd/asws.go:20-35` for complete configuration reference with defaults.
93+
94+
## Release Process
95+
96+
GoReleaser handles multi-platform builds and Docker image creation:
97+
98+
- **Platforms**: Linux and Darwin (macOS) for 386, amd64, arm, arm64
99+
- **Docker variants**:
100+
- Scratch-based images (minimal size, multi-tag: latest, versioned, major version)
101+
- Alpine-based images (with shell/debugging tools)
102+
- **Artifacts**: Binaries, checksums, deb/rpm/apk packages
103+
- **Version injection**: Version string injected at build time via ldflags (`-X main.Version={{.Version}}`)
104+
105+
The build is configured in `goreleaser.yml`.
106+
107+
## Directory Structure
108+
109+
```
110+
cmd/ - Main application entry point
111+
www/ - Default static content directory
112+
files/ - Default file browsing directory (when FS_ENABLED=true)
113+
dockerfiles/ - Docker build configurations (scratch and alpine variants)
114+
```
115+
116+
## Dependencies
117+
118+
Core dependencies:
119+
- `github.com/gin-gonic/gin` - HTTP web framework
120+
- `github.com/gin-contrib/zap` - Zap logging middleware for Gin
121+
- `go.uber.org/zap` - Structured logging
122+
- `github.com/prometheus/client_golang` - Prometheus metrics
123+
124+
## Notes
125+
126+
- No automated tests exist in this codebase
127+
- The application is designed for container deployment with security considerations (runs as nobody user in Docker)
128+
- CGO is disabled for static binary compilation
129+
- Version information is injected at build time, not hardcoded

README.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,24 @@ The following example exposes port 2701 on your local machine and forwards all t
88
docker run -e DEBUG=true -p 2701:80 -v "$(pwd)"/www:/www txn2/asws:v1.6.1
99
```
1010

11+
## SPA (Single Page Application) Mode
12+
13+
For modern React, Vue, or Angular applications with client-side routing, enable SPA fallback mode:
14+
15+
```bash
16+
docker run -e SPA_FALLBACK=true -p 8080:80 -v "$(pwd)"/dist:/www txn2/asws:latest
17+
```
18+
19+
This mode serves `index.html` with a 200 status code for any non-file request, allowing client-side routers to handle routing.
20+
21+
**Note:** SPA_FALLBACK takes precedence over NOT_FOUND_REDIRECT and NOT_FOUND_FILE.
22+
1123
## Environment Variable Defaults
1224

1325
- PORT="80"
1426
- STATIC_DIR="./www"
1527
- STATIC_PATH="./www"
28+
- SPA_FALLBACK="false"
1629
- FS_ENABLED="no"
1730
- FS_DIR="./files"
1831
- FS_PATH="/files"

cmd/asws.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ var (
2424
notFoundRedirectEnv = getEnv("NOT_FOUND_REDIRECT", "false")
2525
notFoundRedirectPathEnv = getEnv("NOT_FOUND_REDIRECT_PATH", "/")
2626
notFoundFileEnv = getEnv("NOT_FOUND_FILE", "./www/404.html")
27+
spaFallbackEnv = getEnv("SPA_FALLBACK", "false")
2728
fsEnabledEnv = getEnv("FS_ENABLED", "false")
2829
fsDirEnv = getEnv("FS_DIR", "./files")
2930
fsPathEnv = getEnv("FS_PATH", "/files")
@@ -47,6 +48,7 @@ func main() {
4748
notFoundRedirect = flag.String("notFoundRedirect", notFoundRedirectEnv, "redirect on not found?")
4849
notFoundRedirectPath = flag.String("notFoundRedirectPath", notFoundRedirectPathEnv, "not found redirect path")
4950
notFoundFile = flag.String("notFoundFile", notFoundFileEnv, "not found file to serve")
51+
spaFallback = flag.String("spaFallback", spaFallbackEnv, "SPA fallback mode (serve index.html on 404)")
5052
fsEnabled = flag.String("fsEnabled", fsEnabledEnv, "filesystem enabled")
5153
fsDir = flag.String("fsDir", fsDirEnv, "filesystem directory")
5254
fsPath = flag.String("fsPath", fsPathEnv, "filesystem path")
@@ -102,12 +104,35 @@ func main() {
102104
r.Static(*staticPath, *staticDir)
103105

104106
r.NoRoute(func(c *gin.Context) {
107+
// SPA fallback mode - serve index.html with 200 status for non-file requests
108+
if *spaFallback == "true" {
109+
indexPath := *staticDir + "/index.html"
110+
content, err := os.ReadFile(indexPath)
111+
if err != nil {
112+
logger.Error("SPA index.html not found", zap.String("file", indexPath))
113+
c.String(http.StatusNotFound, http.StatusText(http.StatusNotFound))
114+
return
115+
}
116+
117+
c.Writer.WriteHeader(http.StatusOK) // Important: 200 not 404
118+
c.Writer.Header().Set("Content-Type", "text/html; charset=utf-8")
119+
_, err = c.Writer.Write(content)
120+
if err != nil {
121+
logger.Error("SPA write error", zap.Error(err))
122+
c.String(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
123+
return
124+
}
125+
return
126+
}
127+
128+
// Redirect mode (existing functionality)
105129
if *notFoundRedirect == "true" {
106130
c.Redirect(http.StatusTemporaryRedirect, *notFoundRedirectPath)
107131
c.Abort()
108132
return
109133
}
110134

135+
// Custom 404 file mode (existing functionality)
111136
content, err := os.ReadFile(*notFoundFile)
112137
if err != nil {
113138
logger.Error("404 content not found", zap.String("file", *notFoundFile))

go.mod

Lines changed: 37 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,49 +1,53 @@
11
module github.com/txn2/asws
22

3-
go 1.21
4-
toolchain go1.24.1
3+
go 1.24.7
54

65
require (
7-
github.com/gin-contrib/zap v0.1.0
8-
github.com/gin-gonic/gin v1.9.1
9-
github.com/prometheus/client_golang v1.16.0
10-
go.uber.org/zap v1.25.0
6+
github.com/gin-contrib/zap v1.1.5
7+
github.com/gin-gonic/gin v1.11.0
8+
github.com/prometheus/client_golang v1.23.2
9+
go.uber.org/zap v1.27.0
1110
)
1211

1312
require (
1413
github.com/beorn7/perks v1.0.1 // indirect
15-
github.com/bytedance/sonic v1.10.0 // indirect
16-
github.com/cespare/xxhash/v2 v2.2.0 // indirect
17-
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect
18-
github.com/chenzhuoyu/iasm v0.9.0 // indirect
19-
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
20-
github.com/gin-contrib/sse v0.1.0 // indirect
14+
github.com/bytedance/gopkg v0.1.3 // indirect
15+
github.com/bytedance/sonic v1.14.2 // indirect
16+
github.com/bytedance/sonic/loader v0.4.0 // indirect
17+
github.com/cespare/xxhash/v2 v2.3.0 // indirect
18+
github.com/cloudwego/base64x v0.1.6 // indirect
19+
github.com/gabriel-vasile/mimetype v1.4.11 // indirect
20+
github.com/gin-contrib/sse v1.1.0 // indirect
2121
github.com/go-playground/locales v0.14.1 // indirect
2222
github.com/go-playground/universal-translator v0.18.1 // indirect
23-
github.com/go-playground/validator/v10 v10.15.1 // indirect
24-
github.com/goccy/go-json v0.10.2 // indirect
25-
github.com/golang/protobuf v1.5.3 // indirect
23+
github.com/go-playground/validator/v10 v10.28.0 // indirect
24+
github.com/goccy/go-json v0.10.5 // indirect
25+
github.com/goccy/go-yaml v1.18.0 // indirect
2626
github.com/json-iterator/go v1.1.12 // indirect
27-
github.com/klauspost/cpuid/v2 v2.2.5 // indirect
28-
github.com/leodido/go-urn v1.2.4 // indirect
29-
github.com/mattn/go-isatty v0.0.19 // indirect
30-
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
27+
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
28+
github.com/leodido/go-urn v1.4.0 // indirect
29+
github.com/mattn/go-isatty v0.0.20 // indirect
3130
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
3231
github.com/modern-go/reflect2 v1.0.2 // indirect
33-
github.com/pelletier/go-toml/v2 v2.0.9 // indirect
34-
github.com/prometheus/client_model v0.4.0 // indirect
35-
github.com/prometheus/common v0.44.0 // indirect
36-
github.com/prometheus/procfs v0.11.1 // indirect
32+
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
33+
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
34+
github.com/prometheus/client_model v0.6.2 // indirect
35+
github.com/prometheus/common v0.66.1 // indirect
36+
github.com/prometheus/procfs v0.16.1 // indirect
37+
github.com/quic-go/qpack v0.5.1 // indirect
38+
github.com/quic-go/quic-go v0.55.0 // indirect
3739
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
38-
github.com/ugorji/go/codec v1.2.11 // indirect
39-
go.opentelemetry.io/otel v1.16.0 // indirect
40-
go.opentelemetry.io/otel/trace v1.16.0 // indirect
40+
github.com/ugorji/go/codec v1.3.1 // indirect
41+
go.uber.org/mock v0.6.0 // indirect
4142
go.uber.org/multierr v1.11.0 // indirect
42-
golang.org/x/arch v0.4.0 // indirect
43-
golang.org/x/crypto v0.36.0 // indirect
44-
golang.org/x/net v0.38.0 // indirect
45-
golang.org/x/sys v0.31.0 // indirect
46-
golang.org/x/text v0.23.0 // indirect
47-
google.golang.org/protobuf v1.33.0 // indirect
48-
gopkg.in/yaml.v3 v3.0.1 // indirect
43+
go.yaml.in/yaml/v2 v2.4.2 // indirect
44+
golang.org/x/arch v0.22.0 // indirect
45+
golang.org/x/crypto v0.43.0 // indirect
46+
golang.org/x/mod v0.29.0 // indirect
47+
golang.org/x/net v0.46.0 // indirect
48+
golang.org/x/sync v0.17.0 // indirect
49+
golang.org/x/sys v0.37.0 // indirect
50+
golang.org/x/text v0.30.0 // indirect
51+
golang.org/x/tools v0.38.0 // indirect
52+
google.golang.org/protobuf v1.36.10 // indirect
4953
)

0 commit comments

Comments
 (0)