4.8 KB
home.go
package controllers

import (
	"fmt"
	"log"
	"net/http"
	"strings"
	"sync"

	"github.com/readysite/readysite/readysite.org/internal/websites"
	"github.com/readysite/readysite/readysite.org/models"
	"github.com/readysite/readysite/pkg/application"
)

// waking tracks sites currently being woken to prevent concurrent wake calls.
var waking sync.Map

// HomeController handles the homepage.
type HomeController struct {
	application.BaseController
}

// Home returns the controller name and instance.
func Home() (string, *HomeController) {
	return "home", &HomeController{}
}

// Setup registers routes.
func (c *HomeController) Setup(app *application.App) {
	c.BaseController.Setup(app)

	// Marketing pages
	http.Handle("GET /{$}", app.Serve("index.html", nil))
	http.Handle("GET /about", app.Serve("about.html", nil))
	http.Handle("GET /pricing", app.Serve("pricing.html", nil))

	// Docs — Learn
	http.Handle("GET /docs/quickstart", app.Serve("docs/quickstart.html", nil))
	http.Handle("GET /docs/ai-assistant", app.Serve("docs/ai-assistant.html", nil))
	http.Handle("GET /docs/pages", app.Serve("docs/pages.html", nil))
	http.Handle("GET /docs/collections", app.Serve("docs/collections.html", nil))

	// Docs — Build
	http.Handle("GET /docs/getting-started", app.Serve("docs/docker.html", nil))
	http.Handle("GET /docs/api", app.Serve("docs/api.html", nil))
	http.Handle("GET /docs/templates", app.Serve("docs/templates.html", nil))
	http.Handle("GET /docs/endpoints", app.Serve("docs/endpoints.html", nil))
	http.Handle("GET /docs/developers", app.Serve("docs/developers.html", nil))

	http.Handle("GET /docs", http.RedirectHandler("/docs/quickstart", http.StatusSeeOther))
}

// Handle returns a request-scoped controller instance.
func (c HomeController) Handle(r *http.Request) application.Controller {
	c.Request = r
	return &c
}

// WakeMiddleware returns an http.Handler that intercepts requests for sleeping
// sites (*.readysite.app) and serves a wake page. Non-matching requests are
// passed through to the next handler.
func WakeMiddleware(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		siteID := extractSiteID(r.Host)
		if siteID == "" {
			next.ServeHTTP(w, r)
			return
		}

		site, err := models.Sites.Get(siteID)
		if err != nil || site == nil {
			http.NotFound(w, r)
			return
		}

		if site.Status == "sleeping" {
			// Trigger wake in background (deduplicated — only one wake per site)
			if _, alreadyWaking := waking.LoadOrStore(site.ID, true); !alreadyWaking {
				go func() {
					defer waking.Delete(site.ID)
					if err := websites.Wake(site); err != nil {
						log.Printf("[wake] failed to wake %s: %v", site.ID, err)
					}
				}()
			}

			// Serve a simple wake page with auto-refresh
			w.Header().Set("Content-Type", "text/html; charset=utf-8")
			fmt.Fprintf(w, wakePage, site.Name, site.ID)
			return
		}

		// Site is active (just woke up or race condition) — redirect back to same URL
		target := fmt.Sprintf("https://%s%s%s", siteID, websites.DomainSuffix, r.URL.Path)
		if r.URL.RawQuery != "" {
			target += "?" + r.URL.RawQuery
		}
		http.Redirect(w, r, target, http.StatusTemporaryRedirect)
	})
}

// extractSiteID extracts the site ID from a host like "{id}.readysite.app".
// Returns empty string if the host doesn't match the pattern.
func extractSiteID(host string) string {
	// Strip port if present
	if i := strings.LastIndex(host, ":"); i != -1 {
		host = host[:i]
	}
	suffix := websites.DomainSuffix // ".readysite.app"
	if !strings.HasSuffix(host, suffix) {
		return ""
	}
	return strings.TrimSuffix(host, suffix)
}

const wakePage = `<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>%s — Waking up</title>
    <meta http-equiv="refresh" content="3">
    <style>
        * { margin: 0; padding: 0; box-sizing: border-box; }
        body {
            min-height: 100vh; display: flex; align-items: center; justify-content: center;
            background: #0a0a0a; color: #888; font-family: system-ui, -apple-system, sans-serif;
        }
        .container { text-align: center; padding: 2rem; }
        .spinner {
            width: 32px; height: 32px; border: 2px solid rgba(255,255,255,0.1);
            border-top-color: #a78bfa; border-radius: 50%%;
            animation: spin 0.8s linear infinite; margin: 0 auto 1.5rem;
        }
        @keyframes spin { to { transform: rotate(360deg); } }
        h1 { color: #fff; font-size: 1.25rem; font-weight: 600; margin-bottom: 0.5rem; }
        p { font-size: 0.875rem; color: #666; }
    </style>
</head>
<body>
    <div class="container">
        <div class="spinner"></div>
        <h1>Waking up your site</h1>
        <p>%s.readysite.app will be ready in a few seconds...</p>
    </div>
</body>
</html>`
← Back