Name every language by its full address, and keep Japanese phrases whole
Human
Prompt
Task 5 of Fiesta part 3, after review: make the twelve hreflang links fully qualified from one origin constant, so a search engine can read them; break Japanese headings between phrases, not inside words; mark Day, Plain, Signal and The Eleven Factors translate="no", since every catalog keeps them as names; and make the menu test require aria-current="page" on the current entry and nothing on the rest, red by a mutation first.
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
www.Origin is the site's address, written once; the hreflang links are Origin plus the page's root-relative path, for all eleven languages and x-default, and the install snippet's URL is written from it too. Japanese headings get word-break: auto-phrase under :lang(ja), so Chromium breaks the headline between phrases and other browsers fall back to the usual breaks. The theme bar's Day, Plain and Signal and the footer's The Eleven Factors are translate="no". TestEveryPageLinksEveryLanguage wants the absolute hreflang URLs and aria-current="page" exactly on the current entry with no attribute on the others.
Agent notes
Red by assertion: the hreflang check against the root-relative links (index.html links hreflang="ar" to "/ar/"; want "https://fiesta.rastrillo.org/ar/"), and the aria-current check against a template mutated to say aria-current="false" on the current entry (the menu entry for en has aria-current="false"; the current entry says "page" and the rest say nothing), restored afterwards. Task 6's link resolver can tell the site's own pages by stripping www.Origin before resolving; the menu's own links stay root-relative. zh-Hans needs nothing: its headline breaks before 庆祝, a phrase boundary, and Chromium's auto-phrase is Japanese only today. The catalogs are untouched; the review's reading of sample.next.body was rejected. make ci was green in a clean worktree before this commit.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
5 files changed,
+36
−16
internal/www/landing.go+8 −7internal/www/landing_test.go+12 −4internal/www/www.go+7 −0site/landing/page.html.tmpl+5 −5site/static/site.css+4 −0
CI passed — run details
CI log
=== gofmt ===
=== no-rastrillo ===
go test -run '^(TestFiestaNeverImportsRastrillo|TestThePublicPackagesLinkNoBrowser)$' -count=1 .
ok amadan.net/rastrillo/fiesta 0.100s
=== 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.021s
=== 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.090s
ok amadan.net/rastrillo/fiesta/check 4.586s
ok amadan.net/rastrillo/fiesta/cmd/fiesta 7.963s
ok amadan.net/rastrillo/fiesta/example 0.154s
ok amadan.net/rastrillo/fiesta/internal/axe 0.134s
ok amadan.net/rastrillo/fiesta/internal/build 1.166s
ok amadan.net/rastrillo/fiesta/internal/codeview 0.010s
ok amadan.net/rastrillo/fiesta/internal/gallery 5.129s
? amadan.net/rastrillo/fiesta/internal/harness [no test files]
ok amadan.net/rastrillo/fiesta/internal/markup 0.006s
ok amadan.net/rastrillo/fiesta/internal/nodetest 0.331s
ok amadan.net/rastrillo/fiesta/internal/site 2.953s
ok amadan.net/rastrillo/fiesta/internal/tokens 0.011s
ok amadan.net/rastrillo/fiesta/internal/www 7.937s
--- 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' .
[90m8:04PM[0m [32mINF[0m [1mUnknown SCM platform. Use --platform to include links in findings.[0m [36mhost=[0mamadan.net
[90m8:04PM[0m [32mINF[0m [1m1 commits scanned.[0m
[90m8:04PM[0m [32mINF[0m [1mscanned ~5812669 bytes (5.81 MB) in 3.14s[0m
[90m8:04PM[0m [32mINF[0m [1mno leaks found[0m
--- 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 11.037s
--- 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 193.631s
--- 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 205.968s
--- 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 202.387s
--- 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 25 tests
ok amadan.net/rastrillo/fiesta/internal/gallery 75.336s
--- 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 24 of 25 tests
ok amadan.net/rastrillo/fiesta/internal/gallery 35.372s
--- 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 16 of 31 tests
ok amadan.net/rastrillo/fiesta/internal/www 31.542s
--- 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 15 of 31 tests
ok amadan.net/rastrillo/fiesta/internal/www 33.985s
Intent
diff --git a/internal/www/landing.go b/internal/www/landing.go| index e3b220d..11fe172 100644 |
| --- a/internal/www/landing.go |
| +++ b/internal/www/landing.go |
| @@ -16,14 +16,15 @@ import ( |
| const landingTemplate = "landing/page.html.tmpl" |
| // page is what the template sees: the language and its direction for |
| -// the html element, the version the footer shows, the still bunting, |
| -// every language the page is built in for the hreflang links and the |
| +// the html element, the site's Origin for a URL that has to be fully |
| +// qualified, the version the footer shows, the still bunting, every |
| +// language the page is built in for the hreflang links and the |
| // language menu, and the catalog behind T. |
| type page struct { |
| - Lang, Dir, Version string |
| - Bunting template.HTML |
| - Languages []menuLanguage |
| - strings map[string]string |
| + Lang, Dir, Origin, Version string |
| + Bunting template.HTML |
| + Languages []menuLanguage |
| + strings map[string]string |
| } |
| // menuLanguage is one entry of the language menu and one hreflang |
| @@ -350,7 +351,7 @@ func landing(siteDir fs.FS, lang string) ([]byte, error) { |
| return nil, fmt.Errorf("www: %w", err) |
| } |
| var buf bytes.Buffer |
| - if err := tmpl.Execute(&buf, page{Lang: lang, Dir: Dir(lang), Version: fiesta.Version, Bunting: stillBunting(buntingShape), Languages: languages(lang), strings: strings}); err != nil { |
| + if err := tmpl.Execute(&buf, page{Lang: lang, Dir: Dir(lang), Origin: Origin, Version: fiesta.Version, Bunting: stillBunting(buntingShape), Languages: languages(lang), strings: strings}); err != nil { |
| return nil, fmt.Errorf("www: rendering the landing page in %s: %w", lang, err) |
| } |
| return buf.Bytes(), nil |
diff --git a/internal/www/landing_test.go b/internal/www/landing_test.go| index 4e4745a..75c0455 100644 |
| --- a/internal/www/landing_test.go |
| +++ b/internal/www/landing_test.go |
| @@ -562,13 +562,15 @@ func TestEveryPageLinksEveryLanguage(t *testing.T) { |
| } |
| } |
| }) |
| + // hreflang wants a fully qualified URL, so every one is the |
| + // site's origin and the page's path. |
| for _, l := range www.Languages { |
| - if got, want := alternates[l], pageURL(l); got != want { |
| + if got, want := alternates[l], www.Origin+pageURL(l); got != want { |
| t.Errorf("%s links hreflang=%q to %q; want %q", pagePath(lang), l, got, want) |
| } |
| } |
| - if got := alternates["x-default"]; got != "/" { |
| - t.Errorf("%s links hreflang=x-default to %q; want /", pagePath(lang), got) |
| + if got, want := alternates["x-default"], www.Origin+"/"; got != want { |
| + t.Errorf("%s links hreflang=x-default to %q; want %q", pagePath(lang), got, want) |
| } |
| if len(alternates) != len(www.Languages)+1 { |
| t.Errorf("%s has %d hreflang links; want %d, the eleven languages and x-default", pagePath(lang), len(alternates), len(www.Languages)+1) |
| @@ -588,8 +590,14 @@ func TestEveryPageLinksEveryLanguage(t *testing.T) { |
| if got, want := text(a), names[code]; got != want { |
| t.Errorf("%s's menu names %s %q; the locale-menu example says %q", pagePath(lang), code, got, want) |
| } |
| - if attr(a, "aria-current") != "" { |
| + // The current page's entry says aria-current="page", and |
| + // no other entry carries the attribute at all: a screen |
| + // reader announces any value, "false" included. |
| + switch cur := attr(a, "aria-current"); { |
| + case cur == "page": |
| current = append(current, code) |
| + case cur != "": |
| + t.Errorf("%s's menu entry for %s has aria-current=%q; the current entry says \"page\" and the rest say nothing", pagePath(lang), code, cur) |
| } |
| noTranslate := false |
| for p := a; p != nil; p = p.Parent { |
diff --git a/internal/www/www.go b/internal/www/www.go| index 1590b7c..21579b3 100644 |
| --- a/internal/www/www.go |
| +++ b/internal/www/www.go |
| @@ -29,6 +29,13 @@ import ( |
| // 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" |
diff --git a/site/landing/page.html.tmpl b/site/landing/page.html.tmpl| index 3b45a08..974312f 100644 |
| --- a/site/landing/page.html.tmpl |
| +++ b/site/landing/page.html.tmpl |
| @@ -5,8 +5,8 @@ |
| <meta name="viewport" content="width=device-width, initial-scale=1"> |
| <title>{{.T "title"}}</title> |
| <meta name="description" content="{{.T "description"}}"> |
| -{{range .Languages}}<link rel="alternate" hreflang="{{.Code}}" href="{{.URL}}"> |
| -{{end}}<link rel="alternate" hreflang="x-default" href="/"> |
| +{{range .Languages}}<link rel="alternate" hreflang="{{.Code}}" href="{{$.Origin}}{{.URL}}"> |
| +{{end}}<link rel="alternate" hreflang="x-default" href="{{.Origin}}/"> |
| <!-- The visitor's theme, before first paint: day, plain or signal from |
| localStorage, day for anything else or a storage that throws. One |
| parser-inserted stylesheet, which blocks rendering until it |
| @@ -58,7 +58,7 @@ |
| <p class="site-lede">{{.T "lede"}}</p> |
| <div class="site-install" id="install"> |
| <p class="site-inote">{{.T "install"}}</p> |
| - <div class="site-snippet"><code dir="ltr" translate="no"><link rel="stylesheet" href="https://fiesta.rastrillo.org/1/fiesta.css"></code><button rst-btn="sm" type="button" class="site-copy" hidden data-site-copied="{{.T "copied"}}" data-site-failed="{{.T "copy.failed"}}">{{.T "copy"}}</button></div> |
| + <div class="site-snippet"><code dir="ltr" translate="no"><link rel="stylesheet" href="{{.Origin}}/1/fiesta.css"></code><button rst-btn="sm" type="button" class="site-copy" hidden data-site-copied="{{.T "copied"}}" data-site-failed="{{.T "copy.failed"}}">{{.T "copy"}}</button></div> |
| <p class="site-live" role="status"></p> |
| </div> |
| <div class="site-cta" id="actions"><a rst-btn="primary" href="/design-system/">{{.T "cta.see"}}</a><a rst-btn href="#themes">{{.T "cta.themes"}}</a></div> |
| @@ -67,7 +67,7 @@ |
| <div class="site-stage" id="themes"> |
| <div class="site-themebar" hidden> |
| <span class="site-themelabel" id="site-theme-label">{{.T "theme.label"}}</span> |
| - <nav rst-seg-tabs aria-labelledby="site-theme-label" class="site-themetabs"><a href="#themes" data-site-theme="day" aria-current="page">{{.T "theme.day"}}</a><a href="#themes" data-site-theme="plain">{{.T "theme.plain"}}</a><a href="#themes" data-site-theme="signal">{{.T "theme.signal"}}</a></nav> |
| + <nav rst-seg-tabs aria-labelledby="site-theme-label" class="site-themetabs"><a href="#themes" data-site-theme="day" aria-current="page" translate="no">{{.T "theme.day"}}</a><a href="#themes" data-site-theme="plain" translate="no">{{.T "theme.plain"}}</a><a href="#themes" data-site-theme="signal" translate="no">{{.T "theme.signal"}}</a></nav> |
| <details rst-dropdown rst-scheme name="rst-menus"> |
| <summary>{{.T "scheme.label"}}<span rst-caret aria-hidden="true"><svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m6 9 6 6 6-6"/></svg></span></summary> |
| <div rst-dropdown-menu role="group" aria-label="{{.T "scheme.label"}}"> |
| @@ -185,7 +185,7 @@ |
| <li><a href="https://carloku.com" translate="no">Carloku</a></li> |
| </ul></nav> |
| <nav aria-labelledby="site-fh-principles"><h2 id="site-fh-principles">{{.T "foot.principles"}}</h2><ul> |
| - <li><a href="https://11factor.org">{{.T "foot.eleven"}}</a></li> |
| + <li><a href="https://11factor.org" translate="no">{{.T "foot.eleven"}}</a></li> |
| <li><a href="https://carlosframework.com/letters/">{{.T "foot.letters"}}</a></li> |
| <li><a href="https://carlosframework.com/trust/">{{.T "foot.trust"}}</a></li> |
| </ul></nav> |
diff --git a/site/static/site.css b/site/static/site.css| index bbb6f05..0e5519e 100644 |
| --- a/site/static/site.css |
| +++ b/site/static/site.css |
| @@ -29,6 +29,10 @@ |
| /* The hero: headline, lede, the install line and the two buttons. */ |
| .site-hero { text-align: center; padding: 34px 28px 0; } |
| +/* Japanese has no spaces, so a heading would otherwise break anywhere, |
| + mid-word; auto-phrase breaks between phrases where the browser can |
| + (Chromium), and the rest fall back to the usual breaks. */ |
| +:lang(ja) :is(h1, h2, h3) { word-break: auto-phrase; } |
| .site-hero h1 { font-size: 2.6rem; line-height: 1.08; letter-spacing: -0.025em; margin: 0 0 14px; } |
| .site-lede { font-size: 1.15rem; color: var(--rst-text-muted); margin: 0 auto 22px; max-width: 64ch; text-wrap: pretty; } |
| .site-install { padding: 6px 0 0; } |