Download
70 lines 2.4 KiBGoModified 2026-10-27 raw
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970
// Command repo-render-preview serves a rendered site over HTTP, for
// previewing it locally. It is a plain file server: no caching rules, no
// login for the private site, and of the security headers only the
// sandbox on the repositories' own files. A real web server
// serves the site in production (see examples/httpd.conf).
//
//	repo-render-preview [--listen ADDR] [--private PRIVATE_HTDOCS] HTDOCS
package main

import (
	"flag"
	"fmt"
	"log"
	"net/http"
	"os"
	"path"
	"path/filepath"
	"strings"
)

func main() {
	listen := flag.String("listen", "127.0.0.1:8080", "address to listen on")
	private := flag.String("private", "", "the config's private_htdocs, served at /private/")
	flag.Usage = func() {
		fmt.Fprintln(flag.CommandLine.Output(), "usage: repo-render-preview [--listen ADDR] [--private PRIVATE_HTDOCS] HTDOCS")
		flag.PrintDefaults()
	}
	flag.Parse()
	if flag.NArg() != 1 {
		flag.Usage()
		os.Exit(2)
	}
	log.Printf("serving %s at http://%s/", flag.Arg(0), *listen)
	log.Fatal(http.ListenAndServe(*listen, handler(flag.Arg(0), *private)))
}

// handler serves htdocs, and private under /private/. A page's URL has no
// .html: /code/r/src is the file code/r/src.html, as is /code/r/src/.
func handler(htdocs, private string) http.Handler {
	pub := http.FileServer(http.Dir(htdocs))
	var priv http.Handler = http.NotFoundHandler()
	if private != "" {
		priv = http.FileServer(http.Dir(private))
	}
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		p := path.Clean("/" + r.URL.Path)
		root, files := htdocs, pub
		if p == "/private" || strings.HasPrefix(p, "/private/") {
			root, files = private, priv
		}
		page := strings.TrimPrefix(p, "/private")
		if strings.HasPrefix(page, "/raw/") || strings.HasPrefix(page, "/svg/") {
			// The repositories' own files, which may be HTML: kept from
			// running as part of the site, as httpd.conf keeps them.
			w.Header().Set("Content-Security-Policy", "sandbox")
		}
		if root != "" && (strings.HasPrefix(page, "/code/") || strings.HasPrefix(page, "/types/")) {
			// Not http.ServeFile, which redirects .../index.html: the page
			// of a file named index.
			if f, err := os.Open(filepath.Join(root, filepath.FromSlash(p)+".html")); err == nil {
				defer func() { _ = f.Close() }() // read only
				if st, err := f.Stat(); err == nil && st.Mode().IsRegular() {
					http.ServeContent(w, r, "page.html", st.ModTime(), f)
					return
				}
			}
		}
		files.ServeHTTP(w, r)
	})
}