| 1 | // Package www assembles the pages of fiesta.rastrillo.org: the landing |
| 2 | // page at the root, the design system under Mount, and the site's own |
| 3 | // static files. It is the half of the site that is not a release; |
| 4 | // internal/site puts the two together. |
| 5 | // |
| 6 | // Pages returns the whole tree in memory, keyed by path relative to |
| 7 | // the site root, and it is deterministic: the same tree from the same |
| 8 | // source every time. |
| 9 | // |
| 10 | // index.html the landing page, in English |
| 11 | // <lang>/index.html the landing page in each other language |
| 12 | // design-system/... internal/gallery's tree, rendered for |
| 13 | // Mount, so every URL in it is absolute |
| 14 | // under /design-system/ |
| 15 | // static/... the site's own files, copied from the |
| 16 | // site source as they are |
| 17 | package www |
| 18 | |
| 19 | import ( |
| 20 | "fmt" |
| 21 | "io/fs" |
| 22 | "strings" |
| 23 | |
| 24 | "amadan.net/rastrillo/fiesta/internal/gallery" |
| 25 | "amadan.net/rastrillo/fiesta/internal/site" |
| 26 | ) |
| 27 | |
| 28 | // Mount is the URL path the design system is served from. The gallery |
| 29 | // writes every URL it owns as an absolute path under it, so the tree |
| 30 | // cannot be served anywhere else. |
| 31 | const Mount = "/design-system" |
| 32 | |
| 33 | // Origin is where the site is served from, with no trailing slash. |
| 34 | // It is the one place the host is written: a URL that has to be fully |
| 35 | // qualified, such as a page's hreflang links, is Origin plus the |
| 36 | // page's root-relative path, and a check that has to tell the site's |
| 37 | // own pages from outside ones strips it. |
| 38 | const Origin = "https://fiesta.rastrillo.org" |
| 39 | |
| 40 | // staticDir is the directory under the site source whose files are |
| 41 | // copied to the site at the same path. |
| 42 | const staticDir = "static" |
| 43 | |
| 44 | // hidden reports whether any segment of a slash path starts with a |
| 45 | // dot: an editor's swap file, a Finder's .DS_Store, a stamp. |
| 46 | func hidden(p string) bool { |
| 47 | for _, seg := range strings.Split(p, "/") { |
| 48 | if strings.HasPrefix(seg, ".") { |
| 49 | return true |
| 50 | } |
| 51 | } |
| 52 | return false |
| 53 | } |
| 54 | |
| 55 | // Pages assembles the site's pages. src is fiesta.FS() or any tree |
| 56 | // laid out the same way, and is what the design system renders from. |
| 57 | // siteDir is the site source, the repository's site/ directory: its |
| 58 | // static/ is copied whole, its landing/ holds the landing page's |
| 59 | // template and catalogs, which render one page per language in |
| 60 | // Languages, and the rest of it is the site's own business, not |
| 61 | // published. A hidden file or directory anywhere in |
| 62 | // siteDir is refused by name rather than skipped: the directory is |
| 63 | // committed, so a dotfile there is a mistake someone should see. |
| 64 | // |
| 65 | // Each path is written once. The parts cannot overlap today, since |
| 66 | // each writes under a directory of its own, but a part that grew into |
| 67 | // another's would otherwise win or lose by map order. |
| 68 | func Pages(src fs.FS, siteDir fs.FS) (map[string][]byte, error) { |
| 69 | out := map[string][]byte{} |
| 70 | put := func(name string, data []byte) error { |
| 71 | if _, dup := out[name]; dup { |
| 72 | return fmt.Errorf("www: %s is written twice", name) |
| 73 | } |
| 74 | out[name] = data |
| 75 | return nil |
| 76 | } |
| 77 | |
| 78 | ds, err := gallery.Render(src, gallery.Options{Mount: Mount}) |
| 79 | if err != nil { |
| 80 | return nil, fmt.Errorf("www: %w", err) |
| 81 | } |
| 82 | prefix := strings.TrimPrefix(Mount, "/") + "/" |
| 83 | for name, data := range ds { |
| 84 | if err := put(prefix+name, data); err != nil { |
| 85 | return nil, err |
| 86 | } |
| 87 | } |
| 88 | |
| 89 | err = fs.WalkDir(siteDir, ".", func(p string, d fs.DirEntry, err error) error { |
| 90 | if err != nil { |
| 91 | return fmt.Errorf("www: reading the site source: %w", err) |
| 92 | } |
| 93 | if p == "." { |
| 94 | return nil |
| 95 | } |
| 96 | if hidden(p) { |
| 97 | return fmt.Errorf("www: %s is hidden, and a hidden file is never published; remove it from the site source", p) |
| 98 | } |
| 99 | if d.IsDir() || (p != staticDir && !strings.HasPrefix(p, staticDir+"/")) { |
| 100 | return nil |
| 101 | } |
| 102 | if !d.Type().IsRegular() { |
| 103 | return fmt.Errorf("www: %s is not a regular file", p) |
| 104 | } |
| 105 | data, err := fs.ReadFile(siteDir, p) |
| 106 | if err != nil { |
| 107 | return fmt.Errorf("www: reading %s: %w", p, err) |
| 108 | } |
| 109 | return put(p, data) |
| 110 | }) |
| 111 | if err != nil { |
| 112 | return nil, err |
| 113 | } |
| 114 | |
| 115 | for _, lang := range Languages { |
| 116 | doc, err := landing(siteDir, lang) |
| 117 | if err != nil { |
| 118 | return nil, err |
| 119 | } |
| 120 | if err := put(pagePath(lang), doc); err != nil { |
| 121 | return nil, err |
| 122 | } |
| 123 | } |
| 124 | return out, nil |
| 125 | } |
| 126 | |
| 127 | // Rules are what the pages ask of the site's rules files, for |
| 128 | // site.Build: their redirects and their immutable addresses. pages is |
| 129 | // what Pages returned. The callers take both from here, so a site |
| 130 | // built one place cannot lose a rule another place keeps. |
| 131 | func Rules(pages map[string][]byte) site.Rules { |
| 132 | return site.Rules{Redirects: Redirects(pages), Immutable: Immutable(pages)} |
| 133 | } |
| 134 | |
| 135 | // designSystem is the part of pages under Mount, keyed by its path |
| 136 | // there, as internal/gallery rendered it. |
| 137 | func designSystem(pages map[string][]byte) map[string][]byte { |
| 138 | prefix := strings.TrimPrefix(Mount, "/") + "/" |
| 139 | ds := map[string][]byte{} |
| 140 | for name, data := range pages { |
| 141 | if rest, ok := strings.CutPrefix(name, prefix); ok { |
| 142 | ds[rest] = data |
| 143 | } |
| 144 | } |
| 145 | return ds |
| 146 | } |
| 147 | |
| 148 | // Immutable are the addresses of the design system's hashed assets |
| 149 | // (gallery.Fingerprinted): a new version of one is a new name, so the |
| 150 | // bytes at each address never change. pages is what Pages returned. |
| 151 | func Immutable(pages map[string][]byte) []string { |
| 152 | return gallery.Fingerprinted(Mount, designSystem(pages)) |
| 153 | } |
| 154 | |
| 155 | // Redirects are the _redirects rules the pages need beside the files: |
| 156 | // the design system's (gallery.Redirects), which send the addresses |
| 157 | // its pages had before they moved under a locale to the pages there |
| 158 | // now. pages is what Pages returned; site.Build writes the rules after |
| 159 | // the release rewrites. |
| 160 | func Redirects(pages map[string][]byte) []string { |
| 161 | return gallery.Redirects(Mount, designSystem(pages)) |
| 162 | } |
| 163 | |