On this page
Building the whole assembly from local checkouts and serving it on loopback, through the production build, graft, shared-file generation and verification.
#internal/blog/preview
#internal/blog/preview
Package preview builds the whole assembly from local checkouts and serves it on loopback.
The preview is the look-before-you-ship step: it does everything a deploy does to produce the tree, stops short of everything a deploy does to publish it, and then serves the result so a person can look at it.
The point is that it looks at the real tree. Every step below is the production function the deploy itself calls -- the same build, the same [github.com/smm-h/selfdoc/internal/blog/site.SplitBuildOutput] graft, the same [github.com/smm-h/selfdoc/internal/blog/assembly.GenerateSharedFiles] (chrome asset included), the same [github.com/smm-h/selfdoc/internal/blog/verify.VerifyAssembly]. A preview that rendered through a second implementation would be a picture of something that is not going to be published, which is worse than no preview at all.
What the deploy does that a preview cannot are the remote-coupled steps: it clones from tags, it pushes manifests and membership records to the assembly repository, and it deploys. Those have local equivalents here, built from the same functions' outputs into the preview tree -- a roster rendered by [github.com/smm-h/selfdoc/internal/blog/site.RenderRoster] from the checkouts named on the command line, a projects.json written by [github.com/smm-h/selfdoc/internal/blog/site.RecordMembership], manifests copied by the graft. The tree the preview serves is therefore an assembly checkout in every respect verification can see, which is why verification runs against it unchanged.
Verification REPORTS, it does not block. A deploy refuses to publish a tree that fails; a preview exists to be looked at when something is wrong, so the report is printed first and loudly and the server starts either way.
The output directory is refused when it sits inside a git working tree at a path that is not ignored. A build tree dropped into a checkout pollutes "git status" for every other session sharing it, and a few thousand generated files are exactly the kind of thing that gets committed by accident.
#BareNotFound
const BareNotFound = "<!DOCTYPE html>\n<html lang=\"en\"><head><title>404</title></head>" +BareNotFound is the page served for an address the tree does not carry, when the tree does not carry a 404 page either. The assembly always writes one, so this is the shape of a preview of a tree that was never generated.
#ServerVersion
const ServerVersion = "selfdoc-preview"ServerVersion is the identity the preview server answers under.
#Summary
type Summary structSummary is what a preview assembled: where it wrote, what it served, and what verification made of the result.
#Error
type Error structError is the failure every operation here reports: an output directory a preview may not be written to, a checkout that declares no slug, two checkouts declaring one slug, a missing canonical base, an unknown theme.
It is the one error type a caller needs to recognize with errors.As to render a preview refusal distinctly from an unexpected internal failure.
#PreviewHandler
type PreviewHandler structPreviewHandler answers one request against the preview tree.
What it imitates about a static host is exactly what changes whether a page looks right: a directory address serves that directory's "index.html", an address missing its trailing slash is redirected to the one that has it, everything is served under its real content type, and an address the tree does not carry answers with the tree's own "404.html" AND a 404 status. A 404 page served as 200 is how a broken link survives a preview.
#Server
type Server structServer is one bound preview server: a listener on loopback and the handler over it.
It binds at construction so the caller can read the port before anything is served, which is what lets the suite ask for an ephemeral one.
#PreviewAssembly
func PreviewAssembly(PreviewAssembly assembles every named checkout into outDir and verifies the result.
homeDir is the checkout of the home project -- the one served at the site root. Required: a site needs a front page. projectDirs are the checkouts of the other projects, each served under its own declared slug, and may be empty.
outDir is where the preview tree is written. It is refused when it sits un-ignored inside a git working tree; see [OutDirRefusal].
canonicalBase is the site's canonical base URL. This is the DEPLOYED base, not the loopback address the preview is served from: the pages carry the canonicals they would ship with, and verification asserts against those.
legacyBlogHost is a retired blog subdomain the generated worker 301s, as the deploy passes it. Empty when there is none.
build decides whether each checkout's build runs. False previews whatever is already in each checkout's "docs/_build", which is what the suite does and what a second look after one edit wants.
theme is a theme name every checkout is built under, overriding each project's own configured theme for this preview only. Empty means every project keeps its configured theme, which is what a deploy always does. With build it reaches each build. Without it, the build trees already on disk have to have been produced under that theme, and a checkout whose has not is a hard error naming it -- see [BuiltUnderTheme].
#ReadSlug
func ReadSlug(sourceDir string) (string, error)ReadSlug returns the assembly slug the project at sourceDir declares.
#ExpectedStylesheet
func ExpectedStylesheet(theme string) (string, error)ExpectedStylesheet is the stylesheet "selfdoc build" writes for theme, byte for byte.
Recomputed the same way the build computes it -- the theme's CSS, the highlight rules its metadata names, minified together -- so a comparison against a build's "style.css" is an equality rather than a resemblance. It is deliberately sensitive to the theme file changing after a build: a page rendered against an older version of the theme is exactly the thing the caller wants to be told about.
#BuiltUnderTheme
func BuiltUnderTheme(sourceDir, theme string) (bool, error)BuiltUnderTheme reports whether sourceDir's existing build output was made with theme.
False for a checkout with no build output at all: nothing to graft is not the same claim as grafting something styled correctly, and the graft will fail on its own terms a moment later anyway.
#EnclosingWorktree
func EnclosingWorktree(path string) stringEnclosingWorktree returns the git working tree path sits in, or "" when it sits in none.
The nearest ancestor carrying ".git" wins, and path itself counts. ".git" is tested for existence rather than for being a directory, so a linked worktree (where it is a file) is found too.
#OutDirRefusal
func OutDirRefusal(outDir string, handle *effects.Handle) (string, error)OutDirRefusal returns why outDir may not be written to, or "" when it may.
A directory outside every git working tree is always fine. Inside one, the only acceptable location is a path git ignores: a preview writes thousands of generated files, and a checkout that another session shares must not grow them as untracked noise.
#RefuseUnsafeOutDir
func RefuseUnsafeOutDir(outDir string, handle *effects.Handle) errorRefuseUnsafeOutDir reports an error when outDir is not a place a preview may be written.
#RenderReport
func RenderReport(report *verify.VerifyReport, outDir string) stringRenderReport returns the verification report as the preview prints it.
Loud, and first: a preview that quietly served a broken tree would be the very failure the command exists to prevent.
#NewPreviewHandler
func NewPreviewHandler(root string) *PreviewHandlerNewPreviewHandler builds a handler serving root.
#MakePreviewServer
func MakePreviewServer(root string, port int) (*Server, error)MakePreviewServer binds a preview server for root to loopback on port.
port 0 binds an ephemeral port, which is what the suite uses; the command itself requires an explicit one. The bind address is not configurable: the preview serves an unreleased site and authenticates nothing, so it is reachable from this machine only.
#ServePreview
func ServePreview(root string, port int, onReady func(port int)) (int, error)ServePreview serves root until interrupted, then closes the listening socket, and reports the exit code the command exits with.
onReady, when given, is called with the bound port once the socket is listening and before anything is served.
#Error.Error
func (e *Error) Error() string { return e.Message }Error returns the diagnostic.
#PreviewHandler.ServeHTTP
func (h *PreviewHandler) ServeHTTP(w http.ResponseWriter, r *http.Request)ServeHTTP dispatches one request. GET and HEAD share their whole path: the head answers the headers a get would answer and stops before the body, so the two can never disagree about what is there.
#Server.Handler
func (s *Server) Handler() *PreviewHandler { return s.handler }Handler returns the handler this server answers through, so a caller can redirect its request log.
#Server.Port
func (s *Server) Port() intPort returns the port the server bound.
#Server.Serve
func (s *Server) Serve() errorServe serves until [Server.Stop], and reports nothing when that is why it returned.
#Server.Stop
func (s *Server) Stop() errorStop ends the accept loop and closes the listening socket.