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>`