// Package www assembles the pages of fiesta.rastrillo.org: the landing // page at the root, the design system under Mount, and the site's own // static files. It is the half of the site that is not a release; // internal/site puts the two together. // // Pages returns the whole tree in memory, keyed by path relative to // the site root, and it is deterministic: the same tree from the same // source every time. // // index.html the landing page, in English // /index.html the landing page in each other language // design-system/... internal/gallery's tree, rendered for // Mount, so every URL in it is absolute // under /design-system/ // static/... the site's own files, copied from the // site source as they are package www import ( "fmt" "io/fs" "strings" "amadan.net/rastrillo/fiesta/internal/gallery" "amadan.net/rastrillo/fiesta/internal/site" ) // Mount is the URL path the design system is served from. The gallery // writes every URL it owns as an absolute path under it, so the tree // cannot be served anywhere else. const Mount = "/design-system" // Origin is where the site is served from, with no trailing slash. // It is the one place the host is written: a URL that has to be fully // qualified, such as a page's hreflang links, is Origin plus the // page's root-relative path, and a check that has to tell the site's // own pages from outside ones strips it. const Origin = "https://fiesta.rastrillo.org" // staticDir is the directory under the site source whose files are // copied to the site at the same path. const staticDir = "static" // hidden reports whether any segment of a slash path starts with a // dot: an editor's swap file, a Finder's .DS_Store, a stamp. func hidden(p string) bool { for _, seg := range strings.Split(p, "/") { if strings.HasPrefix(seg, ".") { return true } } return false } // Pages assembles the site's pages. src is fiesta.FS() or any tree // laid out the same way, and is what the design system renders from. // siteDir is the site source, the repository's site/ directory: its // static/ is copied whole, its landing/ holds the landing page's // template and catalogs, which render one page per language in // Languages, and the rest of it is the site's own business, not // published. A hidden file or directory anywhere in // siteDir is refused by name rather than skipped: the directory is // committed, so a dotfile there is a mistake someone should see. // // Each path is written once. The parts cannot overlap today, since // each writes under a directory of its own, but a part that grew into // another's would otherwise win or lose by map order. func Pages(src fs.FS, siteDir fs.FS) (map[string][]byte, error) { out := map[string][]byte{} put := func(name string, data []byte) error { if _, dup := out[name]; dup { return fmt.Errorf("www: %s is written twice", name) } out[name] = data return nil } ds, err := gallery.Render(src, gallery.Options{Mount: Mount}) if err != nil { return nil, fmt.Errorf("www: %w", err) } prefix := strings.TrimPrefix(Mount, "/") + "/" for name, data := range ds { if err := put(prefix+name, data); err != nil { return nil, err } } err = fs.WalkDir(siteDir, ".", func(p string, d fs.DirEntry, err error) error { if err != nil { return fmt.Errorf("www: reading the site source: %w", err) } if p == "." { return nil } if hidden(p) { return fmt.Errorf("www: %s is hidden, and a hidden file is never published; remove it from the site source", p) } if d.IsDir() || (p != staticDir && !strings.HasPrefix(p, staticDir+"/")) { return nil } if !d.Type().IsRegular() { return fmt.Errorf("www: %s is not a regular file", p) } data, err := fs.ReadFile(siteDir, p) if err != nil { return fmt.Errorf("www: reading %s: %w", p, err) } return put(p, data) }) if err != nil { return nil, err } for _, lang := range Languages { doc, err := landing(siteDir, lang) if err != nil { return nil, err } if err := put(pagePath(lang), doc); err != nil { return nil, err } } return out, nil } // Rules are what the pages ask of the site's rules files, for // site.Build: their redirects and their immutable addresses. pages is // what Pages returned. The callers take both from here, so a site // built one place cannot lose a rule another place keeps. func Rules(pages map[string][]byte) site.Rules { return site.Rules{Redirects: Redirects(pages), Immutable: Immutable(pages)} } // designSystem is the part of pages under Mount, keyed by its path // there, as internal/gallery rendered it. func designSystem(pages map[string][]byte) map[string][]byte { prefix := strings.TrimPrefix(Mount, "/") + "/" ds := map[string][]byte{} for name, data := range pages { if rest, ok := strings.CutPrefix(name, prefix); ok { ds[rest] = data } } return ds } // Immutable are the addresses of the design system's hashed assets // (gallery.Fingerprinted): a new version of one is a new name, so the // bytes at each address never change. pages is what Pages returned. func Immutable(pages map[string][]byte) []string { return gallery.Fingerprinted(Mount, designSystem(pages)) } // Redirects are the _redirects rules the pages need beside the files: // the design system's (gallery.Redirects), which send the addresses // its pages had before they moved under a locale to the pages there // now. pages is what Pages returned; site.Build writes the rules after // the release rewrites. func Redirects(pages map[string][]byte) []string { return gallery.Redirects(Mount, designSystem(pages)) }