Skip to main content

max / makenotwork

Restore the Fan+ visitor sentence, and repoint the method-mismatch test Two integration failures from the conversions, both mine, both caught on astra rather than locally. fan_plus_page_renders_for_anonymous: the conversion had turned one sentence with two inline links into two buttons. The sentence says which link is for whom and a pair of buttons does not, which is the whole point of the loose-wire finding that test seals, so the sentence comes back as Node::rich. That exposed the real problem, filed as quasicoherent 24a3b1df: Node::rich renders through docengine's strict preset, written for user-generated content, so it adds rel=nofollow to links. Nine internal links across four converted pages now tell crawlers not to follow our own site. Both assertions here are split across the anchor's attributes with a note, and go back to matching whole anchors when a screen can say its prose is its own. method_mismatch_renders_error_page: it used /creators as its example of a route axum still owns, and /creators is a described document now. Repointed at /robots.txt, which is not a screen and will not become one, so the next conversion does not move the same breakage again.
Author: Max Johnson <me@maxj.phd> · 2026-09-01 14:20 UTC
Signed with PGP, not checked
Commit: 889992d52b5465d15901f9ded4e869fb10db1e92
Parent: 1d0f094
3 files changed, +51 insertions, -14 deletions
@@ -41,6 +41,15 @@
41 41 //! and every one of the twelve is a POST to our own route that answers with a
42 42 //! redirect. Filed against quasicoherent rather than worked around with a
43 43 //! hand-written attribute inside a described form.
44 + //!
45 + //! # The visitor's sentence carries `nofollow`, and should not
46 + //!
47 + //! `Node::rich` renders through `docengine::render_strict`, the preset written
48 + //! for user-generated content, so the two links in that sentence come out with
49 + //! `rel="noopener noreferrer nofollow"`. They point at `/join` and `/login`:
50 + //! our own pages, telling crawlers not to follow them. quasi has one `Rich` and
51 + //! treats every source as untrusted, which is right for a forum post and wrong
52 + //! for a page's own copy. quasicoherent `24a3b1df`.
44 53
45 54 use makeover_layout as layout;
46 55 use quasi_router::screen::{Figure, Row};
@@ -151,13 +160,16 @@
151 160 // Fan+ needs an account, so a visitor gets both paths: the one for
152 161 // people who already have one and the one for people who do not.
153 162 // Without the second this page is a dead end for exactly the
154 - // visitor it is written for.
155 - pitch(page)
156 - .with(Node::act(
157 - "Create an account",
158 - Action::get("/join").navigating(),
159 - ))
160 - .with(Node::act("Log in", Action::get("/login").navigating()))
163 + // visitor it is written for (`loose-wire g1-23`, and
164 + // `fan_plus_page_renders_for_anonymous` is the seal).
165 + //
166 + // One sentence with two inline links, not two buttons. The sentence
167 + // says which link is for whom and a pair of buttons does not, and
168 + // that distinction is the whole point of the finding above. It is
169 + // prose, so it is `Node::rich`, on `/policy`'s rule.
170 + pitch(page).with(Node::rich(
171 + "[Create an account](/join) to join, or [log in](/login) if you already have one.",
172 + ))
161 173 }
162 174 };
163 175
@@ -242,8 +254,22 @@
242 254 fn a_visitor_is_offered_an_account_rather_than_a_dead_end() {
243 255 let html = html(&Standing::Visitor, false);
244 256
257 + // Asserted on each link's own copy rather than on the bare hrefs: the
258 + // site header carries `/join` and `/login` on every page, so an href
259 + // alone would pass whatever this block said.
260 + //
261 + // Matched loosely across the anchor's attributes because `Node::rich`
262 + // hardens links as untrusted content and adds
263 + // `rel="noopener noreferrer nofollow"` (quasicoherent `24a3b1df`). The
264 + // exact-markup form belongs back here when a screen can say its prose
265 + // is its own.
245 266 assert!(html.contains(r#"href="/join""#), "{html}");
267 + assert!(html.contains("Create an account</a> to join"), "{html}");
246 268 assert!(html.contains(r#"href="/login""#), "{html}");
269 + assert!(
270 + html.contains("log in</a> if you already have one"),
271 + "{html}"
272 + );
247 273 assert!(
248 274 !html.contains(SUBSCRIBE),
249 275 "a visitor cannot subscribe, so the control must not be drawn: {html}"
@@ -18,9 +18,16 @@
18 18 // is a dead end on a conversion surface (loose-wire g1-23). Asserted on the
19 19 // CTA's own copy rather than on `href="/join"`, which the site header
20 20 // carries on every page and would pass whatever this block said.
21 - let cta = r#"<a href="/join">Create an account</a>"#;
22 - assert!(resp.text.contains(cta));
23 - assert!(resp.text.contains(r#"<a href="/login">log in</a>"#));
21 + //
22 + // Split across the anchor's attributes since the page became a described
23 + // document (`fa1a268d`): the sentence is `Node::rich`, which renders through
24 + // docengine's strict preset and adds `rel="noopener noreferrer nofollow"`.
25 + // That rel on our own pages is wrong and is quasicoherent `24a3b1df`; when
26 + // it is fixed this goes back to matching the whole anchor.
27 + assert!(resp.text.contains(r#"href="/join""#));
28 + assert!(resp.text.contains("Create an account</a> to join"));
29 + assert!(resp.text.contains(r#"href="/login""#));
30 + assert!(resp.text.contains("log in</a> if you already have one"));
24 31 assert!(!resp.text.contains("Join Fan+"));
25 32 }
26 33
@@ -551,10 +551,14 @@
551 551 // The same holds for any other method mismatch.
552 552 let resp = h.client.post_form("/login", "").await;
553 553 assert_ne!(resp.status, 405, "/login accepts POST");
554 - // `/creators` is a route axum still owns, so this is what axum's own method
555 - // fallback renders and is what this test is about.
556 - let resp = h.client.delete("/creators").await;
557 - assert_eq!(resp.status, 405, "DELETE /creators should be 405");
554 + // A route axum still owns, so this is what axum's own method fallback
555 + // renders and is what this test is about. `/robots.txt` rather than a page:
556 + // it stood at `/creators` until that became a described document
557 + // (`de85a338`), and picking another page only moves the same breakage to
558 + // the next conversion. `/robots.txt` is not a screen and will not become
559 + // one.
560 + let resp = h.client.delete("/robots.txt").await;
561 + assert_eq!(resp.status, 405, "DELETE /robots.txt should be 405");
558 562 assert!(
559 563 resp.text.contains("error-page"),
560 564 "405 should render the error template, got: {}",