Skip to content

[FEAT] Serve Assets from Docker FileSystem#5

Merged
bbengfort merged 4 commits intomainfrom
assets
Apr 2, 2026
Merged

[FEAT] Serve Assets from Docker FileSystem#5
bbengfort merged 4 commits intomainfrom
assets

Conversation

@bbengfort
Copy link
Copy Markdown
Contributor

@bbengfort bbengfort commented Apr 1, 2026

Scope of changes

Serves static assets from the filesystem rather than building them into the binary.

Estimated PR Size:

  • Tiny
  • Small
  • Medium
  • Large
  • Huge

Acceptance criteria

This PR will be merged without review.

Author checklist

  • I have manually tested the change and/or added automation in the form of unit tests or integration tests
  • I have updated the dependencies list
  • I have added new test fixtures as needed to support added tests
  • I have added or updated the documentation
  • I have run go generate to update generated code

Note

Medium Risk
Changes how static assets are packaged and served (Docker image + runtime routing/config), so misconfiguration or missing files could break UI assets in production. Validation and tests reduce risk, but it affects runtime behavior and deployment defaults.

Overview
Updates the service to serve static assets from the container filesystem instead of embedding them in the Go binary.

The Docker image now copies pkg/web/static into /var/www/html, exposes port 8000, and sets new env defaults (UPTIME_STATIC_SERVE, UPTIME_STATIC_ROOT, UPTIME_STATIC_URL). Runtime routing switches /static (plus /favicon.ico and /robots.txt) to be served via os.DirFS when enabled, and configuration adds/validates a new Static section (including URL vs filesystem constraints) with accompanying unit tests.

Written by Cursor Bugbot for commit af34fe9. Configure here.

Copy link
Copy Markdown

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Template helper uses hardcoded path ignoring configurable URL
    • Updated the template static helper to build URLs from the configured Static.URL (with /static fallback) and wired that value through renderer initialization with tests for custom and CDN URLs.

Create PR

Or push these changes by commenting:

@cursor push 33bfa46b60
Preview (33bfa46b60)
diff --git a/pkg/server/routes.go b/pkg/server/routes.go
--- a/pkg/server/routes.go
+++ b/pkg/server/routes.go
@@ -14,7 +14,7 @@
 
 func (s *Server) setupRoutes() (err error) {
 	// Setup HTML template renderer.
-	if s.router.HTMLRender, err = web.NewRender(web.Templates()); err != nil {
+	if s.router.HTMLRender, err = web.NewRender(web.Templates(), s.conf.Static.URL); err != nil {
 		return err
 	}
 

diff --git a/pkg/web/web.go b/pkg/web/web.go
--- a/pkg/web/web.go
+++ b/pkg/web/web.go
@@ -7,6 +7,7 @@
 	"html/template"
 	"io/fs"
 	"log/slog"
+	"net/url"
 	"path/filepath"
 	"strings"
 	"time"
@@ -43,9 +44,10 @@
 }
 
 // Creates a new template renderer from the default templates.
-func NewRender(fsys fs.FS) (render *Render, err error) {
+func NewRender(fsys fs.FS, staticURL string) (render *Render, err error) {
 	render = &Render{
 		templates: make(map[string]*template.Template),
+		staticURL: staticURL,
 	}
 
 	// Add the pages patterns
@@ -77,6 +79,7 @@
 type Render struct {
 	templates map[string]*template.Template
 	funcs     template.FuncMap
+	staticURL string
 }
 
 var _ render.HTMLRender = (*Render)(nil)
@@ -119,7 +122,7 @@
 func (r *Render) FuncMap() template.FuncMap {
 	if r.funcs == nil {
 		r.funcs = template.FuncMap{
-			"static":      static,
+			"static":      r.static,
 			"titlecase":   titlecase,
 			"lowercase":   lowercase,
 			"uppercase":   uppercase,
@@ -150,10 +153,21 @@
 	return t.Format("January 2, 2006 3:04 PM")
 }
 
-func static(path string) string {
-	return filepath.Join("/static", path)
+func (r *Render) static(path string) string {
+	return staticURL(r.staticURL, path)
 }
 
+func staticURL(base, path string) string {
+	if base == "" {
+		base = "/static"
+	}
+
+	if joined, err := url.JoinPath(base, path); err == nil {
+		return joined
+	}
+	return strings.TrimSuffix(base, "/") + "/" + strings.TrimPrefix(path, "/")
+}
+
 func currentYear() int {
 	return time.Now().Year()
 }

diff --git a/pkg/web/web_test.go b/pkg/web/web_test.go
--- a/pkg/web/web_test.go
+++ b/pkg/web/web_test.go
@@ -9,7 +9,7 @@
 )
 
 func TestRender(t *testing.T) {
-	renderer, err := web.NewRender(web.Templates())
+	renderer, err := web.NewRender(web.Templates(), "/static")
 	require.NoError(t, err)
 	require.NotNil(t, renderer)
 }
@@ -104,4 +104,23 @@
 			require.Equal(t, testCase.expected, f(testCase.input))
 		}
 	})
+
+	t.Run("static configurable base url", func(t *testing.T) {
+		renderer, err := web.NewRender(web.Templates(), "/assets")
+		require.NoError(t, err)
+		funcMap := renderer.FuncMap()
+
+		f := funcMap["static"].(func(string) string)
+		require.Equal(t, "/assets/favicon.ico", f("/favicon.ico"))
+		require.Equal(t, "/assets/css/style.css", f("css/style.css"))
+	})
+
+	t.Run("static configurable remote url", func(t *testing.T) {
+		renderer, err := web.NewRender(web.Templates(), "https://cdn.example.com/uptime")
+		require.NoError(t, err)
+		funcMap := renderer.FuncMap()
+
+		f := funcMap["static"].(func(string) string)
+		require.Equal(t, "https://cdn.example.com/uptime/img/logo.png", f("img/logo.png"))
+	})
 }

This Bugbot Autofix run was free. To enable autofix for future PRs, go to the Cursor dashboard.

@bbengfort bbengfort merged commit 4a30d81 into main Apr 2, 2026
4 checks passed
@bbengfort bbengfort deleted the assets branch April 2, 2026 00:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant