rastrillo / fiesta Public

Clone
git clone https://amadan.net/rastrillo/fiesta

Plain git — no account needed to clone.

Download

Download this file

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
17package www
18
19import (
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.
31const 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.
38const 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.
42const 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.
46func 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.
68func 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.
131func 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.
137func 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.
151func 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.
160func Redirects(pages map[string][]byte) []string {
161 return gallery.Redirects(Mount, designSystem(pages))
162}
163