> ## Documentation Index
> Fetch the complete documentation index at: https://docs.go-mizu.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# SPA

> Single Page Application middleware for client-side routing support.

## Overview

The `spa` middleware serves Single Page Applications by returning the index file for any unmatched routes, enabling client-side routing to work correctly.

Use it when you need:

* React/Vue/Angular app hosting
* Client-side routing support
* HTML5 pushState routing

## Installation

```go theme={null}
import "github.com/go-mizu/mizu/middlewares/spa"
```

## Quick Start

```go theme={null}
app := mizu.New()

// Serve SPA from ./dist directory
app.Use(spa.New("./dist"))

// API routes still work
app.Get("/api/users", listUsers)
```

## Configuration

### Options

| Option      | Type       | Default        | Description                  |
| ----------- | ---------- | -------------- | ---------------------------- |
| `Root`      | `string`   | Required       | Root directory               |
| `FS`        | `fs.FS`    | `nil`          | Filesystem to serve from     |
| `Index`     | `string`   | `"index.html"` | Index file name              |
| `Prefix`    | `string`   | `""`           | URL prefix                   |
| `SkipPaths` | `[]string` | `[]`           | Paths to skip (pass through) |

## Examples

### Basic Usage

```go theme={null}
app.Use(spa.New("./dist"))
```

### From Embedded FS

```go theme={null}
//go:embed dist/*
var distFS embed.FS

app.Use(spa.WithFS(distFS, "dist"))
```

### Skip API Routes

```go theme={null}
app.Use(spa.WithOptions(spa.Options{
    Root:      "./dist",
    SkipPaths: []string{"/api", "/health"},
}))
```

### Custom Index

```go theme={null}
app.Use(spa.WithOptions(spa.Options{
    Root:  "./public",
    Index: "app.html",
}))
```

### With Prefix

```go theme={null}
app.Use(spa.WithOptions(spa.Options{
    Root:   "./dist",
    Prefix: "/app",
}))
// SPA available at /app/*
```

## How It Works

1. Request comes in for `/users/123`
2. Check if file exists at `./dist/users/123`
3. If file exists: serve it
4. If not: serve `./dist/index.html`
5. Client-side router handles `/users/123`

## API Reference

### Functions

```go theme={null}
// New creates SPA middleware for directory
func New(root string) mizu.Middleware

// WithFS creates SPA middleware for fs.FS
func WithFS(fsys fs.FS, root string) mizu.Middleware

// WithOptions creates SPA middleware with configuration
func WithOptions(opts Options) mizu.Middleware
```

## Complete Example

```go theme={null}
package main

import (
    "embed"
    "github.com/go-mizu/mizu"
    "github.com/go-mizu/mizu/middlewares/spa"
)

//go:embed dist/*
var distFS embed.FS

func main() {
    app := mizu.New()

    // API routes
    api := app.Group("/api")
    api.Get("/users", listUsers)
    api.Post("/users", createUser)

    // Serve SPA for everything else
    app.Use(spa.WithOptions(spa.Options{
        FS:        distFS,
        Root:      "dist",
        SkipPaths: []string{"/api"},
    }))

    app.Listen(":3000")
}
```

## Technical Details

### Implementation Overview

The SPA middleware provides a fallback mechanism for single-page applications by serving the index file when a requested file does not exist. This enables client-side routing to work correctly.

#### Path Resolution Process

1. **Ignore Path Check**: Checks if the request path matches any configured `IgnorePaths` (default: `/api`)
2. **Prefix Handling**: If a `Prefix` is configured, strips it from the path before file resolution
3. **File Existence Check**: Determines if the requested path corresponds to an actual file:
   * For `fs.FS`: Uses `fs.Stat()` to check file existence
   * For directory path: Uses `os.Stat()` with `filepath.Join()`
4. **File Serving**: If file exists and is not a directory, serves it with appropriate cache headers
5. **Index Fallback**: If file doesn't exist, serves the index file (default: `index.html`)

#### Cache Control Strategy

The middleware implements a dual cache control strategy:

* **Static Assets**: Configurable via `MaxAge` option (default: 0/no cache)
  * Sets `Cache-Control: public, max-age=<MaxAge>` header
  * Recommended: Long cache duration (e.g., 31536000 for 1 year) for versioned assets

* **Index File**: Configurable via `IndexMaxAge` option (default: 0)
  * When `IndexMaxAge = 0`: Sets `Cache-Control: no-cache, no-store, must-revalidate`
  * When `IndexMaxAge > 0`: Sets `Cache-Control: public, max-age=<IndexMaxAge>`
  * No-cache default ensures users always get latest SPA routing configuration

#### File Serving Details

The middleware uses `http.ServeContent()` for file delivery, which provides:

* Automatic MIME type detection based on file extension
* HTTP range request support for partial content
* Proper `Last-Modified` header handling
* Efficient byte streaming with `io.ReadSeeker`

#### Security Considerations

* All file paths are cleaned using `filepath.Clean()` to prevent directory traversal attacks
* The middleware only serves files from the configured root directory or filesystem
* Directory listings are disabled (directories trigger index fallback)

## Best Practices

* Define API routes before SPA middleware
* Use embedded FS for portable deployments
* Skip API and health check paths
* Set proper cache headers for static assets
* Use long cache durations for static assets with content hashing
* Keep index.html uncached to ensure SPA updates are immediately available

## Testing

The SPA middleware includes comprehensive test coverage for all configuration scenarios:

| Test Case                   | Description                                              | Expected Behavior                                                       |
| --------------------------- | -------------------------------------------------------- | ----------------------------------------------------------------------- |
| Serve static file           | Request for existing static file (`/app.js`)             | Returns file content with status 200                                    |
| Fallback to index for route | Request for non-existent route (`/users/123`)            | Returns `index.html` content with status 200                            |
| Root path                   | Request for root path (`/`)                              | Returns `index.html` content with status 200                            |
| Serve from FS               | Request for file in embedded FS (`/main.js`)             | Returns file content from `fs.FS`                                       |
| Fallback from FS            | Request for non-existent file in FS (`/dashboard`)       | Returns `index.html` from `fs.FS`                                       |
| Ignore API path             | Request to ignored path (`/api/users`)                   | Passes through to registered route handler                              |
| Ignore health path          | Request to ignored path (`/health`)                      | Passes through to registered route handler                              |
| Non-ignored path gets SPA   | Request to non-ignored path (`/profile`)                 | Returns SPA index file                                                  |
| With prefix                 | Request with configured prefix (`/app/dashboard`)        | Strips prefix and serves SPA                                            |
| Without prefix              | Request without prefix when prefix configured (`/other`) | Passes through to other handlers                                        |
| Static asset cache          | Request for static file with MaxAge set                  | Returns file with `Cache-Control: public, max-age=31536000`             |
| Index no cache              | Request for route with IndexMaxAge=0                     | Returns index with `Cache-Control: no-cache, no-store, must-revalidate` |
| Custom index                | Request with custom index file (`app.html`)              | Returns custom index instead of `index.html`                            |
| Panics without FS or Root   | Create middleware with empty options                     | Panics with error message                                               |

## Related Middlewares

* [static](/middlewares/static) - Static file serving
* [favicon](/middlewares/favicon) - Favicon serving
* [embed](/middlewares/embed) - Embedded filesystem
