rastrillo / fiesta Public

Assemble the site's pages in one place, with the design system at /design-system/

Human

Prompt

Task 1 of Fiesta part 3: internal/www with the design system mounted at /design-system/, the -site flag, the gate change and the stand-in landing page.

From this commit's own message. The prompt ledger has no entries for it — which is what a squashed, rebased or amended commit looks like, and what every commit looks like in a repo without a ledger.

Summary

internal/www.Pages renders the gallery for /design-system/, copies site/static/ and writes the landing page; fiesta site takes -site instead of -pages and builds through www.Pages and www.FS; site/index.html is deleted and the placeholder's words move into www as the stand-in landing page; internal/www@2 joins BROWSER_PKGS and the suite step becomes 50-suite@12; site/FOLLOW-UPS.md lists what Fiesta 1.1 should take in from site/static/.
Agent notes
The www browser slices run the package's untagged tests under -tags browser until Task 4 adds browser tests; hack/gotest.sh lists untagged tests under the tag, so neither slice is empty. internal/site/site.go still says -pages in one comment; it is outside this task's files.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev 9ae52062938738c7b069d0666691a7b5ea715641 parent cdb822d
9 files changed, +386 −50
  • .amadan/ci.d/{50-suite@10 => 50-suite@12} +0 −0
  • Makefile +10 −8
  • cmd/fiesta/main.go +24 −14
  • cmd/fiesta/release_test.go +16 −12
  • internal/www/fs.go +18 −0
  • internal/www/www.go +128 −0
  • internal/www/www_test.go +180 −0
  • site/FOLLOW-UPS.md +10 −0
  • site/index.html +0 −16

CI passed — run details

CI log
=== gofmt ===
=== no-rastrillo ===
go test -run '^(TestFiestaNeverImportsRastrillo|TestThePublicPackagesLinkNoBrowser)$' -count=1 .
ok  	amadan.net/rastrillo/fiesta	0.108s
=== size ===
go test ./internal/build -run '^TestTheOneLinkIsUnder25KiBGzipped$' -count=1 -v
=== RUN   TestTheOneLinkIsUnder25KiBGzipped
    build_test.go:138: fiesta-plain.css: 152856 bytes, 22303 bytes gzipped (21.8 KiB of the 25.0 KiB limit)
    build_test.go:138: fiesta-signal.css: 152951 bytes, 22414 bytes gzipped (21.9 KiB of the 25.0 KiB limit)
    build_test.go:138: fiesta.css: 155770 bytes, 23018 bytes gzipped (22.5 KiB of the 25.0 KiB limit)
--- PASS: TestTheOneLinkIsUnder25KiBGzipped (0.02s)
PASS
ok  	amadan.net/rastrillo/fiesta/internal/build	0.022s
=== suite ===
--- shard 0 of 12 ---
==> shard 0 of 12: make root
go build ./...
go vet ./...
FIESTA_TEST_REQUIRE_NODE=1 FIESTA_TEST_REQUIRE_GIT=1 go test -count=1 ./...
ok  	amadan.net/rastrillo/fiesta	1.495s
ok  	amadan.net/rastrillo/fiesta/check	4.398s
ok  	amadan.net/rastrillo/fiesta/cmd/fiesta	5.523s
ok  	amadan.net/rastrillo/fiesta/example	0.302s
ok  	amadan.net/rastrillo/fiesta/internal/axe	0.134s
ok  	amadan.net/rastrillo/fiesta/internal/build	1.472s
ok  	amadan.net/rastrillo/fiesta/internal/codeview	0.023s
ok  	amadan.net/rastrillo/fiesta/internal/gallery	2.277s
?   	amadan.net/rastrillo/fiesta/internal/harness	[no test files]
ok  	amadan.net/rastrillo/fiesta/internal/markup	0.017s
ok  	amadan.net/rastrillo/fiesta/internal/nodetest	0.383s
ok  	amadan.net/rastrillo/fiesta/internal/site	3.158s
ok  	amadan.net/rastrillo/fiesta/internal/tokens	0.016s
ok  	amadan.net/rastrillo/fiesta/internal/www	2.541s
--- shard 1 of 12 ---
==> shard 1 of 12: make staticcheck
go run honnef.co/go/tools/cmd/staticcheck@v0.8.2 -tags browser ./...
--- shard 2 of 12 ---
==> shard 2 of 12: make govulncheck
go run golang.org/x/vuln/cmd/govulncheck@v1.8.0 ./...
=== Symbol Results ===

No vulnerabilities found.

Your code is affected by 0 vulnerabilities.
This scan also found 2 vulnerabilities in packages you import and 5
vulnerabilities in modules you require, but your code doesn't appear to call
these vulnerabilities.
Use '-show verbose' for more details.
--- shard 3 of 12 ---
==> shard 3 of 12: make gitleaks
go run github.com/zricethezav/gitleaks/v8@v8.30.1 git --no-banner --redact --log-opts='--full-history --diff-filter=tuxdb HEAD' .
10:05AM INF Unknown SCM platform. Use --platform to include links in findings. host=amadan.net
10:05AM INF 1 commits scanned.
10:05AM INF scanned ~5484677 bytes (5.48 MB) in 3.52s
10:05AM INF no leaks found
--- shard 4 of 12 ---
==> shard 4 of 12: make browser.internal.harness
./hack/gotest.sh browser 'internal.harness'
gotest: on DOCS-T1, driving the harness's own choice (/usr/bin/chromium)
ok  	amadan.net/rastrillo/fiesta/internal/harness	15.652s
--- shard 5 of 12 ---
==> shard 5 of 12: make browser.check~1of3
./hack/gotest.sh browser 'check~1of3'
gotest: on DOCS-T1, driving the harness's own choice (/usr/bin/chromium)
gotest: slice 1 of 3 of ./check/ runs 95 of 286 tests
ok  	amadan.net/rastrillo/fiesta/check	219.786s
--- shard 6 of 12 ---
==> shard 6 of 12: make browser.check~2of3
./hack/gotest.sh browser 'check~2of3'
gotest: on DOCS-T1, driving the harness's own choice (/usr/bin/chromium)
gotest: slice 2 of 3 of ./check/ runs 95 of 286 tests
ok  	amadan.net/rastrillo/fiesta/check	244.866s
--- shard 7 of 12 ---
==> shard 7 of 12: make browser.check~3of3
./hack/gotest.sh browser 'check~3of3'
gotest: on DOCS-T1, driving the harness's own choice (/usr/bin/chromium)
gotest: slice 3 of 3 of ./check/ runs 96 of 286 tests
ok  	amadan.net/rastrillo/fiesta/check	238.964s
--- shard 8 of 12 ---
==> shard 8 of 12: make browser.internal.gallery~1of2
./hack/gotest.sh browser 'internal.gallery~1of2'
gotest: on DOCS-T1, driving the harness's own choice (/usr/bin/chromium)
gotest: slice 1 of 2 of ./internal/gallery/ runs 1 of 13 tests
ok  	amadan.net/rastrillo/fiesta/internal/gallery	74.524s
--- shard 9 of 12 ---
==> shard 9 of 12: make browser.internal.gallery~2of2
./hack/gotest.sh browser 'internal.gallery~2of2'
gotest: on DOCS-T1, driving the harness's own choice (/usr/bin/chromium)
gotest: slice 2 of 2 of ./internal/gallery/ runs 12 of 13 tests
ok  	amadan.net/rastrillo/fiesta/internal/gallery	23.662s
--- shard 10 of 12 ---
==> shard 10 of 12: make browser.internal.www~1of2
./hack/gotest.sh browser 'internal.www~1of2'
gotest: on DOCS-T1, driving the harness's own choice (/usr/bin/chromium)
gotest: slice 1 of 2 of ./internal/www/ runs 3 of 5 tests
ok  	amadan.net/rastrillo/fiesta/internal/www	2.561s
--- shard 11 of 12 ---
==> shard 11 of 12: make browser.internal.www~2of2
./hack/gotest.sh browser 'internal.www~2of2'
gotest: on DOCS-T1, driving the harness's own choice (/usr/bin/chromium)
gotest: slice 2 of 2 of ./internal/www/ runs 2 of 5 tests
ok  	amadan.net/rastrillo/fiesta/internal/www	0.946s
diff --git a/.amadan/ci.d/50-suite@10 b/.amadan/ci.d/50-suite@12
similarity index 100%
rename from .amadan/ci.d/50-suite@10
rename to .amadan/ci.d/50-suite@12
diff --git a/Makefile b/Makefile
index 935dd8d..57b1c8f 100644
--- a/Makefile
+++ b/Makefile
@@ -54,7 +54,7 @@ ci: gofmt no-rastrillo size suite
# check/drives@2 would become browser.check.drives~1of2 and ~2of2.
# The tasks that add a browser package append here and rename
# 50-suite@N to match.
-BROWSER_PKGS := internal/harness check@3 internal/gallery@2
+BROWSER_PKGS := internal/harness check@3 internal/gallery@2 internal/www@2
go-roles = $(foreach p,$(2),$(if $(findstring @,$(p)),$(call go-slices,$(1),$(subst /,.,$(word 1,$(subst @, ,$(p)))),$(word 2,$(subst @, ,$(p)))),$(1).$(subst /,.,$(p))))
go-slices = $(foreach k,$(shell seq 1 $(3)),$(1).$(2)~$(k)of$(3))
BROWSER_ROLES := $(call go-roles,browser,$(BROWSER_PKGS))
@@ -188,14 +188,16 @@ browser.%: | $(BIN)/tmp
./hack/gotest.sh browser '$*'
# site assembles fiesta.rastrillo.org into .build/site/public from the
-# committed releases and the pages: each release under its exact
-# version, _redirects sending /M/* to the newest M.y.z, and site/ at
-# the root. .build/site/public is the tree to ship; the command's own
-# .fiesta stamp sits above it in .build/site, so nothing hidden is in
-# the shipped tree. Nothing in ci depends on this target, since the
-# releases are committed and checked there.
+# committed releases and the site source: each release under its exact
+# version, _redirects sending /M/* to the newest M.y.z, and at the root
+# the pages internal/www assembles from site/, the landing page, the
+# design system under /design-system/ and site/static/. The tree is
+# .build/site/public; the command's own .fiesta stamp sits above it in
+# .build/site, so nothing hidden is in the shipped tree. Nothing in ci
+# depends on this target, since the releases are committed and checked
+# there.
site: | $(BIN)/tmp
- go run ./cmd/fiesta site -releases releases -pages site -out .build/site
+ go run ./cmd/fiesta site -releases releases -site site -out .build/site
root no-rastrillo size staticcheck govulncheck gitleaks: | $(BIN)/tmp
diff --git a/cmd/fiesta/main.go b/cmd/fiesta/main.go
index 053a511..1c50a1f 100644
--- a/cmd/fiesta/main.go
+++ b/cmd/fiesta/main.go
@@ -29,13 +29,16 @@
// an unpublished candidate may be rebuilt until it is tagged. See
// release.
//
-// fiesta site -releases DIR -pages DIR -out DIR
+// fiesta site -releases DIR -site DIR -out DIR
//
-// assembles the site from the releases and the pages, exactly what
-// internal/site.Build returns, into DIR/public/: each release under
-// its exact version, _redirects sending /M/* to the newest M.y.z, and
-// the pages at the root. DIR/public/ is the tree to ship, and the
-// stamp sits above it in DIR, so nothing hidden is ever deployed.
+// assembles the site from the releases and the site source, exactly
+// what internal/site.Build returns, into DIR/public/: each release
+// under its exact version, _redirects sending /M/* to the newest
+// M.y.z, and the pages at the root. The pages are what internal/www
+// assembles from the site source: the landing page, the design system
+// under /design-system/, and the source's static/ files. DIR/public/
+// is the tree to ship, and the stamp sits above it in DIR, so nothing
+// hidden is ever deployed.
//
// Each command owns the directory it writes. It empties it before it
// writes, so what is there afterwards is the output and only the
@@ -61,10 +64,11 @@ import (
"amadan.net/rastrillo/fiesta/internal/build"
"amadan.net/rastrillo/fiesta/internal/gallery"
"amadan.net/rastrillo/fiesta/internal/site"
+ "amadan.net/rastrillo/fiesta/internal/www"
)
const usage = "usage: fiesta build -out DIR | fiesta gallery -out DIR [-mount /path] | " +
- "fiesta release -version X.Y.Z [-releases DIR] [-candidate] | fiesta site -releases DIR -pages DIR -out DIR"
+ "fiesta release -version X.Y.Z [-releases DIR] [-candidate] | fiesta site -releases DIR -site DIR -out DIR"
func main() {
if err := run(os.Args[1:], os.Stderr); err != nil {
@@ -159,17 +163,17 @@ func releaseCmd(args []string, stderr io.Writer) error {
func siteCmd(args []string, stderr io.Writer) error {
flags := flag.NewFlagSet("site", flag.ContinueOnError)
releases := flags.String("releases", "", "the directory of committed releases, one X.Y.Z/ each")
- pages := flags.String("pages", "", "the directory of pages for the site's root")
+ siteDir := flags.String("site", "", "the site source: its static/ is copied to the site's root")
out := flags.String("out", "", "directory to write the site into")
if ok, err := parse(flags, args, stderr); !ok {
return err
}
- for _, f := range []struct{ name, value string }{{"-releases", *releases}, {"-pages", *pages}, {"-out", *out}} {
+ for _, f := range []struct{ name, value string }{{"-releases", *releases}, {"-site", *siteDir}, {"-out", *out}} {
if f.value == "" {
return errors.New(f.name + " is required\n" + usage)
}
}
- return writeSite(*releases, *pages, *out)
+ return writeSite(*releases, *siteDir, *out)
}
// write builds the release and puts it under out/<version>/, a
@@ -200,10 +204,16 @@ func writeGallery(out, mount string) error {
// keeps the command's own mark out too.
const shipDir = "public"
-// writeSite assembles the site from releases and pages and puts it in
-// out/public/, after claiming out.
-func writeSite(releases, pages, out string) error {
- files, err := site.Build(os.DirFS(releases), os.DirFS(pages))
+// writeSite assembles the pages from the site source, then the site
+// from the releases and those pages, and puts it in out/public/, after
+// claiming out. A fault in the site source is reported on its own,
+// before the releases are read.
+func writeSite(releases, siteDir, out string) error {
+ pages, err := www.Pages(fiesta.FS(), os.DirFS(siteDir))
+ if err != nil {
+ return err
+ }
+ files, err := site.Build(os.DirFS(releases), www.FS(pages))
if err != nil {
return err
}
diff --git a/cmd/fiesta/release_test.go b/cmd/fiesta/release_test.go
index da9fa3a..9bef951 100644
--- a/cmd/fiesta/release_test.go
+++ b/cmd/fiesta/release_test.go
@@ -13,6 +13,7 @@ import (
"amadan.net/rastrillo/fiesta"
"amadan.net/rastrillo/fiesta/internal/build"
"amadan.net/rastrillo/fiesta/internal/site"
+ "amadan.net/rastrillo/fiesta/internal/www"
)
// requireGitEnv names the environment variable that turns a missing
@@ -355,30 +356,33 @@ func TestAnUnreachableOriginRefusesACandidate(t *testing.T) {
untouched(t, dir, "an unreachable origin")
}
-// TestSiteWritesWhatBuildReturns: `fiesta site -releases R -pages P
-// -out DIR` writes site.Build's tree into DIR/public, file for file,
-// stamps DIR itself as its own, and leaves nothing hidden in the tree
-// to ship.
+// TestSiteWritesWhatBuildReturns is a preservation test: `fiesta site
+// -releases R -site S -out DIR` writes site.Build's tree into
+// DIR/public, file for file, stamps DIR itself as its own, and leaves
+// nothing hidden in the tree to ship. The pages are what www.Pages
+// assembles from S, so the expected tree is computed the same way.
func TestSiteWritesWhatBuildReturns(t *testing.T) {
root := t.TempDir()
releases := filepath.Join(root, "releases")
if err := fiestaRelease(t, releases, fiesta.Version, false); err != nil {
t.Fatal(err)
}
- pages := filepath.Join(root, "pages")
- if err := os.MkdirAll(filepath.Join(pages, "themes"), 0o755); err != nil {
+ siteDir := filepath.Join(root, "site")
+ if err := os.MkdirAll(filepath.Join(siteDir, "static"), 0o755); err != nil {
t.Fatal(err)
}
- for name, text := range map[string]string{"index.html": "<h1>Fiesta</h1>\n", "themes/index.html": "<h1>Themes</h1>\n"} {
- if err := os.WriteFile(filepath.Join(pages, filepath.FromSlash(name)), []byte(text), 0o644); err != nil {
- t.Fatal(err)
- }
+ if err := os.WriteFile(filepath.Join(siteDir, "static", "x.css"), []byte("body{margin:0}\n"), 0o644); err != nil {
+ t.Fatal(err)
}
out := filepath.Join(root, "out", "site")
- if err := run([]string{"site", "-releases", releases, "-pages", pages, "-out", out}, io.Discard); err != nil {
+ if err := run([]string{"site", "-releases", releases, "-site", siteDir, "-out", out}, io.Discard); err != nil {
t.Fatalf("fiesta site: %v", err)
}
- want, err := site.Build(os.DirFS(releases), os.DirFS(pages))
+ pages, err := www.Pages(fiesta.FS(), os.DirFS(siteDir))
+ if err != nil {
+ t.Fatal(err)
+ }
+ want, err := site.Build(os.DirFS(releases), www.FS(pages))
if err != nil {
t.Fatal(err)
}
diff --git a/internal/www/fs.go b/internal/www/fs.go
new file mode 100644
index 0000000..8dae1fe
--- /dev/null
+++ b/internal/www/fs.go
@@ -0,0 +1,18 @@
+package www
+
+import (
+ "io/fs"
+ "testing/fstest"
+)
+
+// FS presents a tree Pages returned as a file system, so site.Build
+// can take it where it takes a directory. Every entry is a regular
+// file with mode 0644, the mode cmd/fiesta writes files with, and
+// the directories between are implied by the paths.
+func FS(m map[string][]byte) fs.FS {
+ out := make(fstest.MapFS, len(m))
+ for name, data := range m {
+ out[name] = &fstest.MapFile{Data: data, Mode: 0o644}
+ }
+ return out
+}
diff --git a/internal/www/www.go b/internal/www/www.go
new file mode 100644
index 0000000..d06f924
--- /dev/null
+++ b/internal/www/www.go
@@ -0,0 +1,128 @@
+// 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
+// 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"
+)
+
+// 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"
+
+// staticDir is the directory under the site source whose files are
+// copied to the site at the same path.
+const staticDir = "static"
+
+// landing is the root page until the real one is built: the
+// placeholder the site went live with, in the same words.
+const landing = `<!doctype html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width, initial-scale=1">
+<title>Fiesta</title>
+<link rel="stylesheet" href="/1/fiesta.css">
+</head>
+<body>
+<main rst-page>
+<h1>Celebrate your new design system</h1>
+<p>One stylesheet and done. JS optional. &lt;25KiB gzipped.</p>
+<pre style="white-space: pre-wrap; overflow-wrap: anywhere"><code>&lt;link rel="stylesheet" href="https://fiesta.rastrillo.org/1/fiesta.css"&gt;</code></pre>
+</main>
+</body>
+</html>
+`
+
+// 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, 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
+ }
+
+ if err := put("index.html", []byte(landing)); err != nil {
+ return nil, err
+ }
+ return out, nil
+}
diff --git a/internal/www/www_test.go b/internal/www/www_test.go
new file mode 100644
index 0000000..44d4940
--- /dev/null
+++ b/internal/www/www_test.go
@@ -0,0 +1,180 @@
+package www_test
+
+import (
+ "bytes"
+ "crypto/sha256"
+ "encoding/hex"
+ "sort"
+ "strings"
+ "testing"
+ "testing/fstest"
+
+ xhtml "golang.org/x/net/html"
+
+ "amadan.net/rastrillo/fiesta"
+ "amadan.net/rastrillo/fiesta/internal/site"
+ "amadan.net/rastrillo/fiesta/internal/www"
+)
+
+// siteOf is a site source tree from path to content.
+func siteOf(files map[string]string) fstest.MapFS {
+ m := fstest.MapFS{}
+ for name, text := range files {
+ m[name] = &fstest.MapFile{Data: []byte(text)}
+ }
+ return m
+}
+
+// pages runs Pages over Fiesta's own tree and siteDir, failed loudly.
+func pages(t *testing.T, siteDir fstest.MapFS) map[string][]byte {
+ t.Helper()
+ out, err := www.Pages(fiesta.FS(), siteDir)
+ if err != nil {
+ t.Fatalf("Pages: %v", err)
+ }
+ return out
+}
+
+// urlsOf reads every href and src on doc the way a browser would,
+// whatever the element, so a URL the gallery writes under its mount
+// and one a sample wrote are both seen.
+func urlsOf(t *testing.T, doc []byte) []string {
+ t.Helper()
+ var out []string
+ z := xhtml.NewTokenizer(bytes.NewReader(doc))
+ for {
+ switch z.Next() {
+ case xhtml.ErrorToken:
+ return out
+ case xhtml.StartTagToken, xhtml.SelfClosingTagToken:
+ _, more := z.TagName()
+ for more {
+ var k, v []byte
+ k, v, more = z.TagAttr()
+ if key := string(k); key == "href" || key == "src" {
+ out = append(out, string(v))
+ }
+ }
+ }
+ }
+}
+
+// TestPagesHasARootAndTheDesignSystem: the tree has a landing page at
+// the root and the design system's index under design-system/, and
+// neither is empty.
+func TestPagesHasARootAndTheDesignSystem(t *testing.T) {
+ out := pages(t, siteOf(nil))
+ for _, name := range []string{"index.html", "design-system/index.html"} {
+ if len(out[name]) == 0 {
+ t.Errorf("Pages wrote no %s", name)
+ }
+ }
+}
+
+// TestTheDesignSystemIsMounted: on the design system's index and on
+// one example page, every root-relative href and src starts with
+// /design-system/, so the gallery served there asks for nothing from
+// the site's root.
+func TestTheDesignSystemIsMounted(t *testing.T) {
+ out := pages(t, siteOf(nil))
+ for _, name := range []string{"design-system/index.html", "design-system/day/button.html"} {
+ doc, ok := out[name]
+ if !ok {
+ t.Errorf("Pages wrote no %s", name)
+ continue
+ }
+ rooted := 0
+ for _, u := range urlsOf(t, doc) {
+ if !strings.HasPrefix(u, "/") {
+ continue
+ }
+ rooted++
+ if !strings.HasPrefix(u, www.Mount+"/") {
+ t.Errorf("%s links %s, which is not under %s/", name, u, www.Mount)
+ }
+ }
+ if rooted == 0 {
+ t.Errorf("%s has no root-relative URL at all; the check above read nothing", name)
+ }
+ }
+}
+
+// TestAHiddenSiteFileIsRefused: a hidden file under the site source
+// fails the build, and the error names it, rather than being skipped
+// or published.
+func TestAHiddenSiteFileIsRefused(t *testing.T) {
+ _, err := www.Pages(fiesta.FS(), siteOf(map[string]string{
+ "static/x.css": "body{}\n",
+ "static/.DS_Store": "\x00",
+ }))
+ if err == nil {
+ t.Fatal("Pages accepted static/.DS_Store")
+ }
+ if !strings.Contains(err.Error(), "static/.DS_Store") {
+ t.Errorf("the error does not name the hidden file: %v", err)
+ }
+}
+
+// TestStaticFilesAreCopied: a file under static/ in the site source is
+// in the tree at the same path, byte for byte.
+func TestStaticFilesAreCopied(t *testing.T) {
+ want := "/* the site's own */\nbody{margin:0}\n"
+ out := pages(t, siteOf(map[string]string{"static/x.css": want}))
+ if got := string(out["static/x.css"]); got != want {
+ t.Errorf("static/x.css is %q, want %q", got, want)
+ }
+}
+
+// sumsOf is SHA256SUMS as build.Dist writes it: one `<hex> <path>`
+// line per file, sorted by path, the sums file itself left out.
+func sumsOf(files map[string][]byte) []byte {
+ names := make([]string, 0, len(files))
+ for name := range files {
+ if name != "SHA256SUMS" {
+ names = append(names, name)
+ }
+ }
+ sort.Strings(names)
+ var b bytes.Buffer
+ for _, name := range names {
+ sum := sha256.Sum256(files[name])
+ b.WriteString(hex.EncodeToString(sum[:]) + " " + name + "\n")
+ }
+ return b.Bytes()
+}
+
+// releases is a releases tree holding one small, checksummed 1.0.0.
+func releases() fstest.MapFS {
+ files := map[string][]byte{
+ "fiesta.css": []byte("/*! Fiesta 1.0.0 */\nbody{margin:0}\n"),
+ "LICENSE": []byte("MPL-2.0\n"),
+ }
+ files["SHA256SUMS"] = sumsOf(files)
+ m := fstest.MapFS{}
+ for name, data := range files {
+ m["1.0.0/"+name] = &fstest.MapFile{Data: data}
+ }
+ return m
+}
+
+// TestSiteBuildAcceptsThePages is a preservation test: site.Build
+// takes FS(Pages(...)) as its pages beside a checksummed 1.0.0/, and
+// what Pages wrote is in the site at the same paths beside the release.
+func TestSiteBuildAcceptsThePages(t *testing.T) {
+ out := pages(t, siteOf(map[string]string{"static/x.css": "body{}\n"}))
+ built, err := site.Build(releases(), www.FS(out))
+ if err != nil {
+ t.Fatalf("site.Build refused the pages: %v", err)
+ }
+ for name, data := range out {
+ if !bytes.Equal(built[name], data) {
+ t.Errorf("%s in the site differs from Pages, or is missing", name)
+ }
+ }
+ if _, ok := built["1.0.0/fiesta.css"]; !ok {
+ t.Error("the site lost the release")
+ }
+ if got := string(built["_redirects"]); got != "/1/* /1.0.0/:splat 200\n" {
+ t.Errorf("_redirects is %q", got)
+ }
+}
diff --git a/site/FOLLOW-UPS.md b/site/FOLLOW-UPS.md
new file mode 100644
index 0000000..fea88a6
--- /dev/null
+++ b/site/FOLLOW-UPS.md
@@ -0,0 +1,10 @@
+# Follow-ups for Fiesta 1.1
+
+Fiesta is frozen while the site is built. Anything the site needs that
+Fiesta lacks lives in `site/static/`, with a comment saying so, and is
+listed here for Fiesta 1.1 to take in.
+
+- A copyable code box.
+- An embeddable sidebar.
+- Clipboard behaviour.
+- `rst-seg-tabs` for buttons.
diff --git a/site/index.html b/site/index.html
deleted file mode 100644
index adb3391..0000000
--- a/site/index.html
+++ /dev/null
@@ -1,16 +0,0 @@
-<!doctype html>
-<html lang="en">
-<head>
-<meta charset="utf-8">
-<meta name="viewport" content="width=device-width, initial-scale=1">
-<title>Fiesta</title>
-<link rel="stylesheet" href="/1/fiesta.css">
-</head>
-<body>
-<main rst-page>
-<h1>Celebrate your new design system</h1>
-<p>One stylesheet and done. JS optional. &lt;25KiB gzipped.</p>
-<pre style="white-space: pre-wrap; overflow-wrap: anywhere"><code>&lt;link rel="stylesheet" href="https://fiesta.rastrillo.org/1/fiesta.css"&gt;</code></pre>
-</main>
-</body>
-</html>