Documentation
¶
Overview ¶
Package server provides an HTTP server for serving generated blog content with support for live content updates via atomic handler hot-swapping.
The server implements thread-safe handler replacement, allowing blog content to be regenerated and swapped in without dropping in-flight requests or requiring server restart.
Basic Usage ¶
Create and start a server:
import (
"context"
"log/slog"
"os"
"github.com/harrydayexe/GoBlog/v2/pkg/server"
"github.com/harrydayexe/GoBlog/v2/pkg/config"
)
// Create server with posts from directory
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
postsFS := os.DirFS("posts/")
cfg := config.ServerConfig{
Server: []config.ServerOption{
config.WithPort(8080),
},
}
srv, err := server.New(logger, postsFS, cfg)
if err != nil {
log.Fatal(err)
}
// Start serving (blocks until interrupted)
if err := srv.Run(context.Background(), os.Stdout); err != nil {
log.Fatal(err)
}
Middleware ¶
The server supports pluggable HTTP middleware for cross-cutting concerns like logging, metrics, authentication, or rate limiting. Middleware uses the standard pattern from github.com/harrydayexe/GoWebUtilities/middleware.
Adding middleware to a server:
import (
"github.com/harrydayexe/GoBlog/v2/pkg/config"
"github.com/harrydayexe/GoBlog/v2/pkg/server"
"github.com/harrydayexe/GoWebUtilities/logging"
"github.com/harrydayexe/GoWebUtilities/middleware"
)
// Create server with built-in logging middleware
cfg := config.ServerConfig{
Server: []config.BaseServerOption{
config.WithPort(8080),
config.WithMiddleware(logging.New(logger)),
},
}
srv, err := server.New(logger, postsFS, cfg)
Custom middleware can be added following the standard pattern:
func customMiddleware(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Pre-request logic
h.ServeHTTP(w, r)
// Post-request logic
})
}
cfg := config.ServerConfig{
Server: []config.BaseServerOption{
config.WithMiddleware(
logging.New(logger), // Built-in
customMiddleware, // Custom
),
},
}
Middleware are applied in order: the first middleware in the list is executed first (outermost wrapper). The middleware chain is reapplied whenever the handler is refreshed via UpdatePosts().
Any middleware compatible with the standard http.Handler interface can be used, including third-party middleware packages following the func(http.Handler) http.Handler pattern.
Live Content Updates ¶
Update blog content while server is running:
// Load updated posts
updatedFS := os.DirFS("posts/")
// Atomically swap in new content
if err := srv.UpdatePosts(updatedFS, context.Background()); err != nil {
log.Printf("Update failed: %v", err)
}
The handler swap is atomic - in-flight requests complete with the old handler while new requests immediately see the updated content.
Concurrency ¶
All Server methods are safe for concurrent use by multiple goroutines. The handler is stored in an atomic.Value, providing lock-free reads during request serving and atomic writes during updates. This design supports high-concurrency request handling without lock contention.
Server implements http.Handler interface, delegating to the current handler via atomic load operations.
Index ¶
Examples ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func Handler ¶
func Handler(blog *generator.GeneratedBlog, logger *slog.Logger, opts ...config.BaseOption) http.Handler
Handler creates an HTTP handler that serves the generated blog content. It accepts a GeneratedBlog, a logger for error reporting, and optional configuration options to customize the handler behavior, such as setting a custom blog root path.
The handler serves the following routes (assuming default root "/"):
- GET / and GET /posts - serves the blog index page
- GET /posts/{postName} - serves individual blog posts
- GET /tags - serves the tags index page
- GET /tags/{tagName} - serves tag-specific pages
The handler is safe for concurrent use by multiple goroutines. It does not modify the GeneratedBlog instance.
Types ¶
type Cache ¶
type Cache interface {
// Get retrieves the cached blog content.
// It returns nil if no content is cached.
// Get must be safe to call concurrently with other Cache methods.
Get(ctx context.Context) (*generator.GeneratedBlog, error)
// Set stores the provided blog content in the cache.
// Any previously cached content is replaced.
// Set must be safe to call concurrently with other Cache methods.
Set(ctx context.Context, blog *generator.GeneratedBlog) error
// Clear removes all cached content.
// After calling Clear, Get will return nil until Set is called again.
// Clear must be safe to call concurrently with other Cache methods.
Clear(ctx context.Context) error
}
Cache provides an interface for caching generated blog content. Implementations must be safe for concurrent use by multiple goroutines.
The cache stores a single GeneratedBlog instance and provides methods to get, set, and clear the cached content. This is typically used to avoid regenerating blog content on every request.
type HandlerConfig ¶
HandlerConfig holds configuration options for the blog HTTP handler. It embeds config.BlogRoot to specify the root path where the blog is served.
type Server ¶
type Server struct {
config.BlogRoot
config.Port
config.Host
// contains filtered or unexported fields
}
Server is an HTTP server that serves generated blog content with support for live content updates.
The server uses atomic.Value to store its HTTP handler, enabling thread-safe handler hot-swapping without locks. This allows blog content to be regenerated and swapped in while serving requests, without dropping connections or requiring server restart.
Server implements http.Handler interface, delegating requests to the current handler loaded atomically.
All methods are safe for concurrent use by multiple goroutines.
Example (WithMiddleware) ¶
ExampleServer_withMiddleware demonstrates using middleware with the server.
package main
import (
"log/slog"
"net/http"
"os"
"testing/fstest"
"github.com/harrydayexe/GoBlog/v2/pkg/config"
"github.com/harrydayexe/GoBlog/v2/pkg/server"
)
func main() {
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
postsFS := fstest.MapFS{
"post1.md": &fstest.MapFile{
Data: []byte("---\ntitle: Test Post\ndescription: A test post\ndate: 2024-01-01\n---\nContent"),
},
}
// Custom middleware that adds a header
customMiddleware := func(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("X-Powered-By", "GoBlog")
h.ServeHTTP(w, r)
})
}
cfg := config.ServerConfig{
Server: []config.BaseServerOption{
config.WithPort(8080),
config.WithMiddleware(customMiddleware),
},
}
srv, err := server.New(logger, postsFS, cfg)
if err != nil {
logger.Error("failed to create server", "error", err)
return
}
// Server is ready with middleware applied
_ = srv
}
Output:
func New ¶
New creates a new Server instance with the specified configuration.
The logger is used for structured logging throughout the server lifecycle. The posts filesystem contains the markdown blog posts to be served. The opts parameter configures server behavior via the functional options pattern.
New initializes the server's HTTP handler by generating blog content from the posts filesystem. If initial generation fails, an error is returned.
Returns an error if template rendering or initial blog generation fails. The server is ready to serve requests immediately upon successful return.
func (*Server) Run ¶
Run starts the HTTP server and blocks until interrupted via context cancellation or OS signal (SIGINT). It handles graceful shutdown with a 10-second timeout.
The server uses atomic handler swapping, allowing UpdatePosts to be called while the server is running without interrupting in-flight requests.
Run is safe for concurrent use, though typically only called once per Server. It captures OS interrupt signals (Ctrl+C) and initiates graceful shutdown.
Returns an error only if server initialization fails. Shutdown errors are logged to stderr but don't prevent clean exit.
func (*Server) ServeHTTP ¶
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request)
ServeHTTP implements http.Handler, delegating requests to the current handler. The handler is loaded atomically, allowing it to be safely updated via UpdatePosts while requests are being served.
ServeHTTP is safe for concurrent use by multiple goroutines. It performs a lock-free atomic load of the current handler on each request.
If the handler has not been initialized (nil), ServeHTTP returns a 503 Service Unavailable error.
func (*Server) UpdatePosts ¶
UpdatePosts updates the posts directory and refreshes the HTTP handler with the new content. This triggers a complete regeneration of the blog and an atomic swap of the HTTP handler.
UpdatePosts is safe to call while the server is running and serving requests. The handler swap is atomic, ensuring that requests see either the old or new content without any intermediate inconsistent state.
If handler refresh fails, an error is returned and the previous handler remains active, continuing to serve the old content.