rastrillo / fiesta Public

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>
claude agent, operated by paul@keymail.dev 2ed2f10030d5e2b660bf894dc3a38b4fd7e3c342 parent 81f7d1a
5 files changed, +36 −16
  • internal/www/landing.go +8 −7
  • internal/www/landing_test.go +12 −4
  • internal/www/www.go +7 −0
  • site/landing/page.html.tmpl +5 −5
  • site/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' .
8:04PM INF Unknown SCM platform. Use --platform to include links in findings. host=amadan.net
8:04PM INF 1 commits scanned.
8:04PM INF scanned ~5812669 bytes (5.81 MB) in 3.14s
8:04PM 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	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
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">&lt;link rel="stylesheet" href="https://fiesta.rastrillo.org/1/fiesta.css"&gt;</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">&lt;link rel="stylesheet" href="{{.Origin}}/1/fiesta.css"&gt;</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; }