Sign-in door: take the door's copy from estilo
The door's words now come from estilo (chrome.English, feat/door-copy): the submit reads "Sign in" (Paul, 2026-10-01; never "Email me a link"), the help line states the link's 15-minute lifetime, the sent state says "If that address can sign in here…" with "Use a different address", and Home's signed-out page is the door's "You're signed out" rather than connect's copy, which named Home (Paul, 2026-09-23: never name the hub). Tab titles are "Sign in · Memoria", "Check your email · Memoria" and "Signed out · Memoria"; the email subject is "Sign in to Memoria". Deleted memoria's own copy: the marketing lede, the heading, label and submit overrides, the sent heading/body/back link, and signinOutcome's sent sentence (the door says it). The ?reauth=1 lede stays: it is a state, not an introduction. Pinned to estilo fbd14e7 (feat/door-copy); re-pin to estilo main after that branch lands.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
9 files changed,
+48
−36
go.mod+1 −1go.sum+2 −2internal/memoria/auth.go+1 −1internal/memoria/handlers.go+10 −18internal/memoria/home_render.go+5 −7internal/memoria/home_router_test.go+9 −3internal/memoria/templates/signin.html+1 −1internal/memoriatest/signin_door_test.go+12 −2internal/memoriatest/signinoutcome_test.go+7 −1
CI passed — run details
CI log
=== vet === go vet ./... go: downloading amadan.net/oficina/estilo.git v0.0.0-20261001075355-fbd14e759027 === fmt === === test === go test ./... ? amadan.net/oficina/memoria/cmd/memoria [no test files] ok amadan.net/oficina/memoria/internal/memoria 3.442s ok amadan.net/oficina/memoria/internal/memoria/chunk 0.004s ok amadan.net/oficina/memoria/internal/memoria/fact 0.529s ? amadan.net/oficina/memoria/internal/memoria/icons [no test files] ok amadan.net/oficina/memoria/internal/memoria/index 0.034s ok amadan.net/oficina/memoria/internal/memoria/notion 0.020s ok amadan.net/oficina/memoria/internal/memoria/provider 1.127s ok amadan.net/oficina/memoria/internal/memoria/query 0.003s ok amadan.net/oficina/memoria/internal/memoria/seal 0.003s ok amadan.net/oficina/memoria/internal/memoria/sibling 3.022s ok amadan.net/oficina/memoria/internal/memoriatest 1.736s ? amadan.net/oficina/memoria/scripts/writeestilo [no test files] ? amadan.net/oficina/memoria/tools/bench [no test files] ? amadan.net/oficina/memoria/tools/drive [no test files] ? amadan.net/oficina/memoria/tools/seed [no test files] ? amadan.net/oficina/memoria/tools/vendorassets [no test files] === migration-check === go run github.com/carlosframework/rastrillo/cmd/rastrillo migration check rastrillo migration check: models and migrations agree
diff --git a/go.mod b/go.mod| index 3ec360d..7f35e10 100644 |
| --- a/go.mod |
| +++ b/go.mod |
| @@ -5,7 +5,7 @@ go 1.26 |
| toolchain go1.26.5 |
| require ( |
| - amadan.net/oficina/estilo.git v0.0.0-20260929231226-cce11010ece0 |
| + amadan.net/oficina/estilo.git v0.0.0-20261001075355-fbd14e759027 |
| amadan.net/oficina/home/connect v0.0.0-20260910000424-5b8d8410775f |
| amadan.net/oficina/home/handoff v0.0.0-20260908102137-481d2d57f489 |
| amadan.net/oficina/suite v0.0.0-20260905215529-60b01a8a0d6d |
diff --git a/go.sum b/go.sum| index a4586d6..771ea81 100644 |
| --- a/go.sum |
| +++ b/go.sum |
| @@ -1,5 +1,5 @@ |
| -amadan.net/oficina/estilo.git v0.0.0-20260929231226-cce11010ece0 h1:Qww+xGIYJaj+6TIX4Knr71ghxeYJjtgFm6WukX6NkGg= |
| -amadan.net/oficina/estilo.git v0.0.0-20260929231226-cce11010ece0/go.mod h1:4u0jNBtIdMLOsJyrScfqoEuYDJF7BBM3qM9YWbzt6aw= |
| +amadan.net/oficina/estilo.git v0.0.0-20261001075355-fbd14e759027 h1:+NFDWW5ROkLaFIsT9flDuAjNeQJJxj0Safk32UcRyn0= |
| +amadan.net/oficina/estilo.git v0.0.0-20261001075355-fbd14e759027/go.mod h1:4u0jNBtIdMLOsJyrScfqoEuYDJF7BBM3qM9YWbzt6aw= |
| amadan.net/oficina/home/connect v0.0.0-20260910000424-5b8d8410775f h1:lkw3tjzHfZxe74pNfo+tGDz4mkZLg+D6R+FTkmsym6M= |
| amadan.net/oficina/home/connect v0.0.0-20260910000424-5b8d8410775f/go.mod h1:YroavXe43Wietec+po8mtSk4a9Bir2h31Q3iAf4MWxI= |
| amadan.net/oficina/home/handoff v0.0.0-20260908102137-481d2d57f489 h1:RPKtyry/71i06o8L+gjAaUyexbmG56kO1YPs601URAM= |
diff --git a/internal/memoria/auth.go b/internal/memoria/auth.go| index 3ea2aa1..03241a8 100644 |
| --- a/internal/memoria/auth.go |
| +++ b/internal/memoria/auth.go |
| @@ -214,7 +214,7 @@ func NewAuth(sqlDB *sql.DB, env AuthEnv, logger *slog.Logger, authorize func(str |
| Authorize: authorize, |
| SigninPath: signinPath, |
| SignedInPath: landing, |
| - Subject: "Your memoria sign-in link", |
| + Subject: chrome.EmailSubject("Memoria"), |
| Logger: logger, |
| }) |
| if err != nil { |
diff --git a/internal/memoria/handlers.go b/internal/memoria/handlers.go| index 164b9d8..238ac4a 100644 |
| --- a/internal/memoria/handlers.go |
| +++ b/internal/memoria/handlers.go |
| @@ -1135,9 +1135,6 @@ type signinPage struct { |
| SignedOut *chrome.SignInNotice |
| } |
| -// signinLede is what the door says under its heading on a plain visit. |
| -const signinLede = "Memoria holds your team's work in one place. Sign in with your email and we'll send you a link." |
| - |
| func (a *app) localSignin(w http.ResponseWriter, r *http.Request) { |
| p := signinPage{page: page{Chunks: a.chunkCount(), ModelOn: a.modelOn()}} |
| p.ForceEmail = a.home != nil && r.URL.Query().Get("force") == "1" |
| @@ -1152,23 +1149,16 @@ func (a *app) localSignin(w http.ResponseWriter, r *http.Request) { |
| if p.ForceEmail { |
| back += "?force=1" |
| } |
| - // signinOutcome's sentence, in the notice's two parts. |
| - p.Sent = &chrome.SignInNotice{ |
| - Icon: icon, App: "Memoria", Sky: view, |
| - Heading: "Check your email", Body: "We've sent you a link to sign in.", |
| - Link: &chrome.Link{Href: back, Label: "Back to sign in"}, |
| - } |
| + // The door's words; back is where "Use a different address" goes. |
| + sent := chrome.SignInNotice{Icon: icon, App: "Memoria", Sky: view}.Sent(back) |
| + p.Sent = &sent |
| render(w, "signin", p) |
| return |
| } |
| - door := &chrome.SignIn{ |
| - Icon: icon, App: "Memoria", Sky: view, |
| - Heading: "Sign in", Lede: signinLede, |
| - Action: "/auth/begin", |
| - AddressLabel: "Email address", |
| - Submit: "Email me a link", |
| - } |
| + // The door supplies its own heading, label, help and submit; a plain |
| + // visit carries no lede. |
| + door := &chrome.SignIn{Icon: icon, App: "Memoria", Sky: view, Action: "/auth/begin"} |
| if p.ForceEmail { |
| door.Hidden = []chrome.Hidden{{Name: "force", Value: "1"}} |
| } |
| @@ -1176,7 +1166,8 @@ func (a *app) localSignin(w http.ResponseWriter, r *http.Request) { |
| case "": |
| case "warn": |
| // ?reauth=1. Not an error, so not the door's negative callout: |
| - // it stands where the lede does, and says why the form is back. |
| + // it stands in the lede, and says why the form is back. The one |
| + // lede this door carries: a state, never an introduction. |
| door.Lede = msg |
| default: |
| door.Error = msg |
| @@ -1199,7 +1190,8 @@ func (a *app) localSignin(w http.ResponseWriter, r *http.Request) { |
| // the owners are. |
| func signinOutcome(q url.Values) (msg, kind string) { |
| if q.Get("sent") != "" { |
| - return "Check your email. We've sent you a link to sign in.", "good" |
| + // No sentence: the door's check-your-email state says it. |
| + return "", "good" |
| } |
| switch q.Get("err") { |
| case "": |
diff --git a/internal/memoria/home_render.go b/internal/memoria/home_render.go| index c71ace2..61142a6 100644 |
| --- a/internal/memoria/home_render.go |
| +++ b/internal/memoria/home_render.go |
| @@ -18,14 +18,12 @@ func (a *app) renderHome(w http.ResponseWriter, r *http.Request, state, label st |
| pg := page{Chunks: a.chunkCount(), ModelOn: a.modelOn()} |
| data := connect.Page(state, label) |
| tree, view := pages["home"], any(homePage{page: pg, Home: data}) |
| - // Signed out of Home is the door: Home's heading, sentence and link, |
| - // on Memoria's card and sky, like the sign-in page it sits beside. |
| + // Signed out of Home is the door's signed-out page, on Memoria's card |
| + // and sky like the sign-in page it sits beside: the door's words, not |
| + // connect's (which name Home), and connect's link. |
| if state == "signedout" { |
| - tree, view = pages["signin"], signinPage{page: pg, SignedOut: &chrome.SignInNotice{ |
| - Icon: assets.Path("static/brand/icon.svg"), App: "Memoria", Sky: sky.For(r.Host), |
| - Heading: data.Title, Body: data.Body, |
| - Link: &chrome.Link{Href: data.Action, Label: data.ActionLabel}, |
| - }} |
| + out := chrome.SignInNotice{Icon: assets.Path("static/brand/icon.svg"), App: "Memoria", Sky: sky.For(r.Host)}.SignedOut(data.Action) |
| + tree, view = pages["signin"], signinPage{page: pg, SignedOut: &out} |
| } |
| var body bytes.Buffer |
| if err := tree.ExecuteTemplate(&body, "layout", view); err != nil { |
diff --git a/internal/memoria/home_router_test.go b/internal/memoria/home_router_test.go| index 6ade219..be94fe1 100644 |
| --- a/internal/memoria/home_router_test.go |
| +++ b/internal/memoria/home_router_test.go |
| @@ -378,7 +378,8 @@ func TestHomePagesStateTheModelSwitch(t *testing.T) { |
| } |
| // Where Home sign-out lands is the door, like the sign-in page beside it: |
| -// Memoria's card on the sky, with Home's words and link, and the door's |
| +// Memoria's card on the sky, with the door's own signed-out words (not |
| +// connect's, which name Home) and connect's link, and the door's |
| // stylesheets linked so the card is drawn. |
| func TestSignedOutIsTheDoor(t *testing.T) { |
| f := newHomeRouterFixture(t) |
| @@ -390,11 +391,16 @@ func TestSignedOutIsTheDoor(t *testing.T) { |
| b := html.UnescapeString(w.Body.String()) |
| for _, want := range []string{ |
| "<div estilo-door", "estilo-sky-layer", "<section estilo-signin", "<span>Memoria</span>", |
| - "/static/chrome.", "/static/sky.", "<title>" + data.Title + " · Memoria</title>", |
| - data.Title, data.Body, `href="` + data.Action + `"`, data.ActionLabel, |
| + "/static/chrome.", "/static/sky.", "<title>Signed out · Memoria</title>", |
| + "<h1>You’re signed out</h1>", "You’ve signed out of Memoria on this device.", |
| + `href="` + data.Action + `">Sign in</a>`, |
| } { |
| if !strings.Contains(b, want) { |
| t.Errorf("/signed-out lacks %q", want) |
| } |
| } |
| + // Home is never named to people (Paul, 2026-09-23). |
| + if strings.Contains(b, "Home") { |
| + t.Errorf("/signed-out names Home:\n%s", b) |
| + } |
| } |
diff --git a/internal/memoria/templates/signin.html b/internal/memoria/templates/signin.html| index 7e3a5da..0040598 100644 |
| --- a/internal/memoria/templates/signin.html |
| +++ b/internal/memoria/templates/signin.html |
| @@ -1,4 +1,4 @@ |
| -{{define "title"}}{{with .SignedOut}}{{.Heading}} · Memoria{{else}}Sign in — Memoria{{end}}{{end}} |
| +{{define "title"}}{{with .Sent}}{{.Title}}{{else with .SignedOut}}{{.Title}}{{else}}{{.Door.Title}}{{end}}{{end}} |
| {{/* The suite's door (estilo chrome): one card on the team's sky, the |
| app's mark and name at its head. chrome.css draws the card, sky.css |
| the sky; both are vendored from estilo (go run ./scripts/writeestilo). |
diff --git a/internal/memoriatest/signin_door_test.go b/internal/memoriatest/signin_door_test.go| index 35f865f..2dd2c0b 100644 |
| --- a/internal/memoriatest/signin_door_test.go |
| +++ b/internal/memoriatest/signin_door_test.go |
| @@ -40,9 +40,12 @@ func TestSigninPageIsTheDoor(t *testing.T) { |
| "estilo-door", |
| `<span>Memoria</span>`, |
| `<h1>Sign in</h1>`, |
| - "Memoria holds your team's work in one place.", |
| `action="/auth/begin"`, |
| - `>Email me a link</button>`, |
| + // The door's copy, not memoria's (estilo chrome.English; the |
| + // submit is "Sign in", Paul 2026-10-01). |
| + `<title>Sign in · Memoria</title>`, |
| + `>Sign in</button>`, |
| + "We’ll email you a link that signs you in. It works once and expires in 15 minutes.", |
| // The layout's footer, and its egress claim, stays under the door. |
| "<footer", |
| "leaves this instance", |
| @@ -52,6 +55,13 @@ func TestSigninPageIsTheDoor(t *testing.T) { |
| } |
| } |
| + // The retired copy is gone: the old submit and memoria's marketing lede. |
| + for _, gone := range []string{"Email me a link", "holds your team", "Sign in — Memoria"} { |
| + if strings.Contains(body, gone) { |
| + t.Errorf("the sign-in page still says %q", gone) |
| + } |
| + } |
| + |
| // Every sky layer the door links: the two inline placeholders (which |
| // is why CSP must allow img-src data:) and the served ones. |
| layers := regexp.MustCompile(`<img estilo-sky-layer="([a-z]+)"[^>]* src="([^"]+)"`).FindAllStringSubmatch(body, -1) |
diff --git a/internal/memoriatest/signinoutcome_test.go b/internal/memoriatest/signinoutcome_test.go| index 9b32145..4037822 100644 |
| --- a/internal/memoriatest/signinoutcome_test.go |
| +++ b/internal/memoriatest/signinoutcome_test.go |
| @@ -76,12 +76,18 @@ func TestSigninPageSaysWhatHappened(t *testing.T) { |
| h, _ := signinApp(t) |
| rr := beginSignin(t, h, address) |
| body := followToSignin(t, h, rr, "sent=1") |
| - if !strings.Contains(body, `<p estilo-signin-body role="status">We've sent you a link to sign in.</p>`) { |
| + if !strings.Contains(body, `<p estilo-signin-body role="status">If that address can sign in here, we’ve sent it a link. It works once and expires in 15 minutes.</p>`) { |
| t.Errorf("the sent page is not the door's check-your-email notice:\n%s", body) |
| } |
| if !strings.Contains(body, "<h1>Check your email</h1>") { |
| t.Error("the sent notice does not say check your email") |
| } |
| + if !strings.Contains(body, "<title>Check your email · Memoria</title>") { |
| + t.Error("the sent page's tab title is not \"Check your email · Memoria\"") |
| + } |
| + if !strings.Contains(body, `<a href="/signin">Use a different address</a>`) { |
| + t.Errorf("the sent notice does not link back to the form as \"Use a different address\":\n%s", body) |
| + } |
| if strings.Contains(body, `action="/auth/begin"`) { |
| t.Error("the sign-in form is still on the page after a link was sent") |
| } |