From f0eaf46755e04b806e2df07a2fabd4060a72d54c Mon Sep 17 00:00:00 2001 From: grm Date: Sat, 12 Sep 2026 11:43:14 +0300 Subject: Serve the superadmin's blog on the root domain The base domain (and www.) now serves a regular blog owned by the first superadmin, created automatically on startup, alongside the management routes. Literal management paths take precedence over the blog's page wildcards, and the slugs they would shadow are reserved. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01Sd8UPWrvyYCLj97JexNw3A --- internal/web/server.go | 43 +++++++++++++++++++++++++++---------------- 1 file changed, 27 insertions(+), 16 deletions(-) (limited to 'internal/web/server.go') diff --git a/internal/web/server.go b/internal/web/server.go index e8a5e6d..68dba3b 100644 --- a/internal/web/server.go +++ b/internal/web/server.go @@ -29,33 +29,26 @@ type Server struct { func NewServer(cfg *config.Config, st *store.Store) *Server { s := &Server{cfg: cfg, st: st, tpl: newTemplates(cfg.Dev, funcs)} s.root = s.rootRoutes() - s.blog = s.blogRoutes() + s.blog = s.subdomainRoutes() return s } // ServeHTTP dispatches on the Host header: the base domain is the management -// site, one label below it is a blog, anything else is a 404. +// site plus the superadmin's root blog, one label below it is a blog, anything +// else is a 404. func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { host := hostname(r.Host) switch { - case host == s.cfg.BaseDomain, host == "www."+s.cfg.BaseDomain: - s.root.ServeHTTP(w, r) + case host == s.cfg.BaseDomain, host == config.RootSubdomain+"."+s.cfg.BaseDomain: + // The root blog is looked up lazily by hostBlog so management pages work even without one. + s.root.ServeHTTP(w, r.WithContext(context.WithValue(r.Context(), ctxHostSub, config.RootSubdomain))) case strings.HasSuffix(host, "."+s.cfg.BaseDomain): sub := strings.TrimSuffix(host, "."+s.cfg.BaseDomain) if strings.Contains(sub, ".") || reservedSubdomains[sub] { http.NotFound(w, r) return } - blog, err := s.st.BlogBySubdomain(r.Context(), sub) - if err != nil { - if errors.Is(err, store.ErrNotFound) { - s.plainError(w, http.StatusNotFound, "No blog here (yet).") - return - } - s.serverError(w, err) - return - } - s.blog.ServeHTTP(w, r.WithContext(context.WithValue(r.Context(), ctxBlog, blog))) + s.blog.ServeHTTP(w, r.WithContext(context.WithValue(r.Context(), ctxHostSub, sub))) default: s.plainError(w, http.StatusNotFound, fmt.Sprintf("Unknown host %q. Blogs live at .%s", host, s.cfg.BaseDomain)) } @@ -73,8 +66,9 @@ func hostname(h string) string { type ctxKey int const ( - ctxUser ctxKey = iota - ctxBlog + ctxUser ctxKey = iota + ctxBlog // the blog a handler operates on (public page, or /b/{sub}/ management) + ctxHostSub // subdomain derived from the Host header ) func currentUser(r *http.Request) *store.User { @@ -89,6 +83,23 @@ func currentBlog(r *http.Request) *store.Blog { // ---- middleware ---------------------------------------------------------- +// hostBlog resolves the blog named by the Host header into the context for public pages. +func (s *Server) hostBlog(next http.HandlerFunc) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + sub, _ := r.Context().Value(ctxHostSub).(string) + blog, err := s.st.BlogBySubdomain(r.Context(), sub) + if err != nil { + if errors.Is(err, store.ErrNotFound) { + s.plainError(w, http.StatusNotFound, "No blog here (yet).") + return + } + s.serverError(w, err) + return + } + next(w, r.WithContext(context.WithValue(r.Context(), ctxBlog, blog))) + } +} + // session loads the user from the JWT cookie (if any) into the context. func (s *Server) session(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { -- cgit v1.2.3