Skip to main content

max / makenotwork

Serve /fan-plus from a residual that branches The first screen on the seam whose residual carries branches. /policy and /team fold to one literal and /use-cases is literals and holes; this page asks four questions about the reader and holds the markup of every answer, so all three readers get a compiled page and nothing is rendered per request for the two answers this reader did not give. Nine literals, six branches, one hole. The hole is the period end a member's subscription carries. Its benefits table and the visitor's sign-up sentence moved to content/fan-plus.toml. `pitch()` is #[constant] and `membership()` is #[staged], which is the rule the screen taught: a staged shape's include forces a flag onto the callee, and the arguments pick which. A request value means the callee needs a filler, so #[staged]; nothing means #[constant]. A zero-argument shape has no choice, because an all-literal include folds to the constant shim before the staged path is reached. The mount now carries what the address said. A screen on the seam answers no Screen, so it never reaches the adapter and there is no Request to read; /fan-plus shows its welcome banner only on the way back from checkout, which is `?subscribed=true` and is carried nowhere else. `Carried` is that much of a request and no more. A failed membership read answers Visitor rather than 500. The page exists to explain what Fan+ is, and it reads correctly for somebody whose membership is unknown. The residual is checked by filling it under every standing and both banner states against the tree the screen builds, and by asserting every reader's words are in the compiled markup: an equality alone would pass if both paths lost a branch together. Dead makeover vocabulary falls 52 to 49, which is the ratchet doing what its own note says it should as phase 3 converts screens.
Co-Authored-By
Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session
https://claude.ai/code/session_01P8ostB2UmZJGj5WjSHRSot
Author: Max Johnson <me@maxj.phd> · 2026-09-07 22:53 UTC
Signed with PGP, not checked
Commit: 26518237d33d7592edae2c4e35f25ab18dd52618
Parent: 3bb3f27
6 files changed, +301 insertions, -76 deletions
M server/build.rs +10 -1
@@ -396,6 +396,15 @@
396 396 /// emitted by `quasi-webview` at request time, which no scan of this repo can
397 397 /// see. A class the description writes is dead here by construction.
398 398 ///
399 + /// # 49 since 2026-09-07, lowered by converting screens
400 + ///
401 + /// The ratchet doing what the paragraph below says it should. `/team`,
402 + /// `/use-cases` and `/fan-plus` moved onto the build-time residual seam
403 + /// (quasicoherent `14d42beb`), their markup is now derived from the described
404 + /// vocabulary rather than written by a template, and the dead count fell 52 to
405 + /// 49 without anything being deleted by hand. Lower it again whenever a
406 + /// conversion moves it; raising it is the thing that wants an argument.
407 + ///
399 408 /// # 52 since 2026-09-01, and the three were already loose
400 409 ///
401 410 /// Re-sealed after measuring, not after a change: the count was 52 at the
@@ -413,7 +422,7 @@
413 422 /// is mid-flight, and this number should FALL as phase 3 converts screens
414 423 /// rather than being raised again. If it needs raising a second time, read the
415 424 /// list before doing it.
416 - const DEAD_VOCABULARY_HIGH_WATER: usize = 52;
425 + const DEAD_VOCABULARY_HIGH_WATER: usize = 49;
417 426
418 427 /// The scripts this build script generates into `static/`.
419 428 ///
@@ -75,58 +75,18 @@
75 75 /// What the membership costs, in whole dollars per month.
76 76 const PRICE: &str = "$8";
77 77
78 - /// One thing a membership buys.
79 - ///
80 - /// Named members rather than a tuple, for `policy`'s reason: a description
81 - /// names what it draws, and `.1` is not a name.
82 - struct Benefit {
83 - name: &'static str,
84 - detail: &'static str,
85 - }
86 -
87 - /// What a member gets.
88 - ///
89 - /// **Keep this list and the Fan+ card on the landing page identical, and keep
90 - /// both to what the code grants.** Audited 2026-08-05, carried over from the
91 - /// template's comment because it is a rule rather than a note: a benefit listed
92 - /// here and not granted by the code is a promise nobody implemented.
93 - const BENEFITS: &[Benefit] = &[
94 - Benefit {
95 - name: "$5 monthly credit",
96 - detail: "A promo code delivered by email each billing cycle, usable toward any purchase on the platform",
97 - },
98 - Benefit {
99 - name: "+ badge",
100 - detail: "Displayed next to your name on your forum posts",
101 - },
102 - Benefit {
103 - name: "Forum signatures",
104 - detail: "A signature block rendered under everything you post",
105 - },
106 - Benefit {
107 - name: "Image embeds",
108 - detail: "Post images in forum threads",
109 - },
110 - ];
111 -
112 78 /// The region a member's own state is drawn in.
113 79 const MEMBERSHIP: &str = "fan-plus-membership";
114 80
115 81 /// The region the pitch is drawn in.
116 82 const PITCH: &str = "fan-plus-pitch";
117 83
118 - /// What a visitor is offered instead of a control they cannot use.
119 - ///
120 - /// One sentence with two inline links, not two buttons. The sentence says which
121 - /// link is for whom and a pair of buttons does not, and that distinction is the
122 - /// whole point of `loose-wire g1-23`: Fan+ needs an account, so without the
123 - /// second link this page is a dead end for exactly the visitor it is written
124 - /// for. It is prose, so it is `own_prose`, on `/policy`'s rule.
125 - const VISITOR: &str =
126 - "[Create an account](/join) to join, or [log in](/login) if you already have one.";
127 -
128 84 /// What this request knows about the reader's membership.
129 - enum Standing {
85 + ///
86 + /// `pub(crate)` so `quasi::residuals` can name it: the residual checks live
87 + /// beside the roster rather than in each screen, and a screen that branches has
88 + /// to be filled under every branch to be checked at all.
89 + pub(crate) enum Standing {
130 90 /// Nobody is signed in. The page is a pitch plus a way to get an account.
131 91 Visitor,
132 92 /// Signed in, not a member.
@@ -171,6 +131,12 @@
171 131 }
172 132
173 133 /// The page.
134 + ///
135 + /// Kept beside the residual mount rather than replaced by it, and it is what the
136 + /// residual is checked against: `quasi::residuals` asserts that filling the
137 + /// compiled one gives back exactly what building and rendering this gives, on
138 + /// every branch. A screen with no second way to produce its markup has nothing
139 + /// to check the first one with.
174 140 pub fn screen(viewer: &super::Viewer, request: Request) -> Result<Response, RouteError> {
175 141 // Moved out of the request rather than borrowed: the signature is quasi's,
176 142 // so the request arrives owned.
@@ -184,6 +150,22 @@
184 150 Ok(page_screen(&standing, just_subscribed).into())
185 151 }
186 152
153 + /// The two things this page reads, for the mount that serves it from a residual.
154 + ///
155 + /// Its own function because both halves of the mount want them: the document, to
156 + /// state the screen, and the body, to fill the branches. A failure to read the
157 + /// membership answers `Visitor`, which is what a reader with no account sees; the
158 + /// alternative is a 500 on a marketing page because Stripe's mirror of a
159 + /// subscription could not be loaded, and the page that says "here is what Fan+
160 + /// is" reads correctly for somebody whose membership is unknown.
161 + #[must_use]
162 + pub(crate) fn reading(viewer: &super::Viewer, carried: &super::Carried) -> (Standing, bool) {
163 + (
164 + standing(viewer).unwrap_or(Standing::Visitor),
165 + carried.says("subscribed", "true"),
166 + )
167 + }
168 +
187 169 /// Read the reader's membership, if there is a reader.
188 170 fn standing(viewer: &super::Viewer) -> Result<Standing, RouteError> {
189 171 let Some(user) = viewer.user.as_ref() else {
@@ -211,7 +193,7 @@
211 193 /// a visitor gets the pitch and both ways to get an account, a signed-in
212 194 /// reader gets the pitch and the control that subscribes, and a member gets
213 195 /// their membership's state and nothing to buy.
214 - shape page_screen(standing: &Standing, just_subscribed: bool) -> Screen;
196 + pub(crate) shape page_screen(standing: &Standing, just_subscribed: bool) -> Screen;
215 197
216 198 screen single "Fan+ - Makenotwork" {
217 199 measured MEASURE;
@@ -219,21 +201,46 @@
219 201 summarised "Support the platform and get $5 of credit back every month, plus forum badges, \
220 202 signatures and image embeds.";
221 203
222 - region PAGE_REGION as Pane {
223 - page "Fan+";
224 - banner layout::Tone::Success "You're now a Fan+ member. Welcome." when just_subscribed;
204 + include page_region(standing, just_subscribed);
205 + }
206 + }
225 207
226 - include membership(standing) when standing.is_member();
227 - include pitch() unless standing.is_member();
208 + declare! {
209 + /// The page's one region, split out so it can be staged.
210 + ///
211 + /// **The first screen on the seam whose residual branches.** `/policy` and
212 + /// `/team` fold to one literal and `/use-cases` is literals and holes; this
213 + /// page asks four questions about the reader, so its residual carries the
214 + /// markup of every answer and the filler walks the arms the request picks.
215 + /// That is the point rather than a cost: the three readers see three
216 + /// documents, and all three are compiled.
217 + #[staged]
218 + pub(crate) shape page_region(standing: &Standing, just_subscribed: bool) -> Slot;
228 219
229 - act "Join Fan+" to post SUBSCRIBE awaiting when standing.is_unsubscribed();
230 - include super::own_prose(VISITOR) when standing.is_visitor();
220 + region PAGE_REGION as Pane {
221 + page "Fan+";
222 + banner layout::Tone::Success "You're now a Fan+ member. Welcome." when just_subscribed;
223 +
224 + include membership(standing) when standing.is_member();
225 + include pitch() unless standing.is_member();
226 +
227 + act "Join Fan+" to post SUBSCRIBE awaiting when standing.is_unsubscribed();
228 + for visitor in copy "content/fan-plus.toml" as visitor {
229 + include super::own_prose(visitor.body) when standing.is_visitor();
231 230 }
232 231 }
233 232 }
234 233
235 234 declare! {
236 235 /// What a member is shown: the state of the thing they are paying for.
236 + ///
237 + /// `#[staged]` because [`page_region`] includes it and hands it the
238 + /// standing, which is a value the request brings. A staged `include` whose
239 + /// arguments are not all literal opens a scope and calls the callee's
240 + /// filler, so the callee has to have one; without the flag the call does
241 + /// not resolve and rustc names `membership_fill`. The other direction is
242 + /// [`pitch`], which takes nothing and is `#[constant]` instead.
243 + #[staged]
237 244 shape membership(standing: &Standing) -> Slot;
238 245
239 246 region MEMBERSHIP as Group {
@@ -248,13 +255,19 @@
248 255 /// What somebody who is not a member is shown, whether or not they have an
249 256 /// account. The two branches differ only in what they are offered
250 257 /// afterwards.
258 + ///
259 + /// `#[constant]`: it reads nothing the request brings. `PRICE` is a `const`
260 + /// and a staged path is kept rather than made a hole, so the figure is
261 + /// evaluated once while the residual is derived. The benefits are copy read
262 + /// at macro time, so the loop is unrolled before any AST exists.
263 + #[constant]
251 264 shape pitch() -> Slot;
252 265
253 266 region PITCH as Group {
254 267 text "Support the platform and get something back every month.";
255 268 section "What you get";
256 269 list {
257 - for benefit in BENEFITS {
270 + for benefit in copy "content/fan-plus.toml" as benefits {
258 271 row benefit.name {
259 272 secondary benefit.detail;
260 273 }
@@ -394,13 +407,24 @@
394 407
395 408 /// The benefits are what the code grants, and the landing page's Fan+ card
396 409 /// says the same four. Audited 2026-08-05; this keeps the count honest.
410 + ///
411 + /// Reads `content/fan-plus.toml` the way the macro reads it, which is
412 + /// `policy`'s rule: the file is asked what the page should say rather than
413 + /// a second copy of it being held here. The count stays asserted, because
414 + /// the audit is about the number as much as the names.
397 415 #[test]
398 416 fn the_four_benefits_are_all_stated() {
399 417 let html = html(&Standing::Unsubscribed, false);
400 418
401 - assert_eq!(BENEFITS.len(), 4);
402 - for benefit in BENEFITS {
403 - assert!(html.contains(benefit.name), "{} missing", benefit.name);
419 + let copy: toml::Table = include_str!("../../content/fan-plus.toml")
420 + .parse()
421 + .expect("the fan-plus copy is TOML");
422 + let benefits = copy["benefits"].as_array().expect("a list of benefits");
423 + assert_eq!(benefits.len(), 4);
424 + for benefit in benefits {
425 + let name = benefit["name"].as_str().expect("a name");
426 + let escaped = crate::helpers::escape_html(name);
427 + assert!(html.contains(&escaped), "{name} missing");
404 428 }
405 429 }
406 430
@@ -669,9 +669,9 @@
669 669 served_document_mount(
670 670 app,
671 671 team::PATH,
672 - |_| team::page_screen(),
672 + |_, _| team::page_screen(),
673 673 team::renderer,
674 - |_| residuals::settled(&residuals::TEAM),
674 + |_, _| residuals::settled(&residuals::TEAM),
675 675 ),
676 676 ),
677 677 (
@@ -679,9 +679,9 @@
679 679 served_document_mount(
680 680 app,
681 681 use_cases::PATH,
682 - |viewer| use_cases::page_screen(&use_cases::prices(viewer)),
682 + |viewer, _| use_cases::page_screen(&use_cases::prices(viewer)),
683 683 use_cases::renderer,
684 - |viewer| {
684 + |viewer, _| {
685 685 use_cases::page_region_serve(&residuals::USE_CASES, &use_cases::prices(viewer))
686 686 .into()
687 687 },
@@ -692,14 +692,27 @@
692 692 served_document_mount(
693 693 app,
694 694 policy::PATH,
695 - |_| policy::page_screen(),
695 + |_, _| policy::page_screen(),
696 696 policy::renderer,
697 - |_| residuals::settled(&residuals::POLICY),
697 + |_, _| residuals::settled(&residuals::POLICY),
698 698 ),
699 699 ),
700 700 (
701 701 fan_plus::PATH,
702 - public_document_mount(app, fan_plus::PATH, fan_plus::screen, fan_plus::renderer),
702 + served_document_mount(
703 + app,
704 + fan_plus::PATH,
705 + |viewer, carried| {
706 + let (standing, just_subscribed) = fan_plus::reading(viewer, carried);
707 + fan_plus::page_screen(&standing, just_subscribed)
708 + },
709 + fan_plus::renderer,
710 + |viewer, carried| {
711 + let (standing, just_subscribed) = fan_plus::reading(viewer, carried);
712 + fan_plus::page_region_serve(&residuals::FAN_PLUS, &standing, just_subscribed)
713 + .into()
714 + },
715 + ),
703 716 ),
704 717 (
705 718 creators::PATH,
@@ -810,9 +823,12 @@
810 823 /// builds a `Node`, which is the property the seam exists for; a `Cow` is what
811 824 /// lets the first path stay a borrow while the second returns a `String`.
812 825 ///
813 - /// `screen` takes the viewer because a document is not derivable: the shell
814 - /// carries whoever is looking, and a screen with parameters needs them to state
815 - /// its own title and measure.
826 + /// `screen` and `body` take the viewer because a document is not derivable: the
827 + /// shell carries whoever is looking, and a screen with parameters needs them to
828 + /// state its own title and measure. They take the request's parts as well,
829 + /// because a screen may read the address it was reached at: `/fan-plus` shows a
830 + /// welcome banner only on the way back from checkout, which is `?subscribed=true`
831 + /// and is carried nowhere else.
816 832 ///
817 833 /// Pairing a body with the wrong screen serves one page's markup inside
818 834 /// another's document, which nothing here can catch; what catches it is
@@ -821,9 +837,9 @@
821 837 fn served_document_mount(
822 838 app: &AppState,
823 839 path: &'static str,
824 - screen: fn(&Viewer) -> quasi_router::Screen,
840 + screen: fn(&Viewer, &Carried) -> quasi_router::Screen,
825 841 renderer: fn(&Viewer) -> quasi_webview::Webview,
826 - body: fn(&Viewer) -> std::borrow::Cow<'static, str>,
842 + body: fn(&Viewer, &Carried) -> std::borrow::Cow<'static, str>,
827 843 ) -> axum::Router {
828 844 let app = app.clone();
829 845 axum::Router::new().route(
@@ -842,8 +858,10 @@
842 858 return axum::http::StatusCode::INTERNAL_SERVER_ERROR.into_response();
843 859 };
844 860
845 - let markup = body(&viewer);
846 - axum::response::Html(served(&viewer, screen, renderer, &markup)).into_response()
861 + let carried = Carried::of(&parts);
862 + let markup = body(&viewer, &carried);
863 + axum::response::Html(served(&viewer, &carried, screen, renderer, &markup))
864 + .into_response()
847 865 }
848 866 }),
849 867 )
@@ -857,11 +875,44 @@
857 875 /// `residuals::tests` asserts and nothing at this level can.
858 876 fn served(
859 877 viewer: &Viewer,
860 - screen: fn(&Viewer) -> quasi_router::Screen,
878 + carried: &Carried,
879 + screen: fn(&Viewer, &Carried) -> quasi_router::Screen,
861 880 renderer: fn(&Viewer) -> quasi_webview::Webview,
862 881 markup: &str,
863 882 ) -> String {
864 - renderer(viewer).served(&screen(viewer), markup)
883 + renderer(viewer).served(&screen(viewer, carried), markup)
884 + }
885 +
886 + /// What the address carried, for a screen on the seam that reads it.
887 + ///
888 + /// The adapter builds a `quasi_router::Request` and hands every other screen
889 + /// one; a screen here answers no `Screen`, so it never reaches the adapter and
890 + /// there is no `Request` to read. This is that much of one: the query, parsed
891 + /// the same way, and nothing else. Kept to what a served document actually
892 + /// needs rather than growing into a second `Request` nobody asked for.
893 + pub struct Carried(std::collections::HashMap<String, String>);
894 +
895 + impl Carried {
896 + /// The query the reader arrived with.
897 + fn of(parts: &axum::http::request::Parts) -> Self {
898 + Self(
899 + parts
900 + .uri
901 + .query()
902 + .map(|query| {
903 + url::form_urlencoded::parse(query.as_bytes())
904 + .map(|(key, value)| (key.into_owned(), value.into_owned()))
905 + .collect()
906 + })
907 + .unwrap_or_default(),
908 + )
909 + }
910 +
911 + /// Whether the address carried `key` set to exactly `value`.
912 + #[must_use]
913 + pub fn says(&self, key: &str, value: &str) -> bool {
914 + self.0.get(key).is_some_and(|held| held.trim() == value)
915 + }
865 916 }
866 917
867 918 /// Every described screen a reader with no session can reach, mounted.
@@ -69,6 +69,9 @@
69 69 ("USE_CASES", |plan| {
70 70 Node::Region(super::use_cases::page_region_staged(plan))
71 71 }),
72 + ("FAN_PLUS", |plan| {
73 + Node::Region(super::fan_plus::page_region_staged(plan))
74 + }),
72 75 ]
73 76 }
74 77
@@ -317,6 +320,66 @@
317 320 }
318 321 }
319 322
323 + /// A branching screen's residual fills to what the renderer builds, on
324 + /// every branch.
325 + ///
326 + /// `/fan-plus` is the first residual on the seam that carries branches: it
327 + /// asks four questions about the reader and holds the markup of every
328 + /// answer, so one filled render proves nothing. What is checked is every
329 + /// combination of the three standings and both banner states, each against
330 + /// the tree the screen has always built.
331 + ///
332 + /// This is the test that would have caught the span overlap the derivation
333 + /// panicked on (quasi-webview `stage::placed`): two guarded siblings whose
334 + /// markup shares a boundary can be located on top of each other, and a
335 + /// residual built on that serves one branch's markup inside another's.
336 + #[test]
337 + fn the_fan_plus_residual_fills_to_what_the_renderer_builds_on_every_branch() {
338 + use crate::quasi::fan_plus::{Standing, page_region, page_region_serve};
339 + use quasi_axum::Serves as _;
340 +
341 + let standings = [
342 + Standing::Visitor,
343 + Standing::Unsubscribed,
344 + Standing::Member { period_end: None },
345 + Standing::Member {
346 + period_end: Some("March 3, 2027".to_owned()),
347 + },
348 + ];
349 +
350 + for standing in &standings {
351 + for just_subscribed in [false, true] {
352 + let filled = page_region_serve(&FAN_PLUS, standing, just_subscribed);
353 + let rendered =
354 + Webview::new().fragment(&Node::Region(page_region(standing, just_subscribed)));
355 +
356 + assert_eq!(filled, rendered, "just_subscribed={just_subscribed}");
357 + }
358 + }
359 + }
360 +
361 + /// The branches a reader never sees are still in the compiled markup.
362 + ///
363 + /// The property that makes the seam worth having on a screen like this: all
364 + /// three readers get a compiled page, so nothing is rendered per request for
365 + /// the sake of the two answers this reader did not give. A literal going
366 + /// missing here means a branch stopped being derived, which the equality
367 + /// above would still pass if both paths lost it together.
368 + #[test]
369 + fn the_fan_plus_residual_holds_every_reader_s_markup() {
370 + let compiled = format!("{:?}", FAN_PLUS.ops());
371 +
372 + for words in [
373 + "Fan+ membership is active",
374 + "Support the platform",
375 + "Join Fan+",
376 + "Create an account",
377 + "now a Fan+ member",
378 + ] {
379 + assert!(compiled.contains(words), "{words} is not in the residual");
380 + }
381 + }
382 +
320 383 /// Every screen the roster names is checked above.
321 384 ///
322 385 /// The gap this closes is the one a table opens: a screen added to
@@ -330,8 +393,8 @@
330 393 /// the two counts have to be kept by hand or not at all.
331 394 #[test]
332 395 fn every_screen_on_the_seam_is_checked() {
333 - /// Screens with their own filling test: `/use-cases`.
334 - const HOLED: usize = 1;
396 + /// Screens with their own filling test: `/use-cases`, `/fan-plus`.
397 + const HOLED: usize = 2;
335 398
336 399 assert_eq!(
337 400 roster().len(),
@@ -60,3 +60,48 @@
60 60 )),
61 61 ],
62 62 );
63 +
64 + pub static FAN_PLUS: ::quasi_router::stage::Residual = ::quasi_router::stage::Residual::compiled(
65 + &[
66 + ::quasi_router::stage::Op::Lit(::std::borrow::Cow::Borrowed(
67 + "<div id=\"fan-plus\" class=\"region pane\"><h1 class=\"heading\">Fan+</h1",
68 + )),
69 + ::quasi_router::stage::Op::Branch(::std::borrow::Cow::Borrowed(&[
70 + ::quasi_router::stage::Op::Lit(::std::borrow::Cow::Borrowed(
71 + "><div class=\"banner\" data-tone=\"success\" role=\"status\" aria-live=\"polite\">You&#39;re now a Fan+ member. Welcome.</div",
72 + )),
73 + ])),
74 + ::quasi_router::stage::Op::Branch(::std::borrow::Cow::Borrowed(&[
75 + ::quasi_router::stage::Op::Lit(::std::borrow::Cow::Borrowed(
76 + "><section id=\"fan-plus-membership\" class=\"region group\"><p class=\"text\">Your Fan+ membership is active.",
77 + )),
78 + ::quasi_router::stage::Op::Branch(::std::borrow::Cow::Borrowed(&[
79 + ::quasi_router::stage::Op::Lit(::std::borrow::Cow::Borrowed(
80 + "</p><p class=\"text\">Current period ends: ",
81 + )),
82 + ::quasi_router::stage::Op::Hole { scope: 1, id: 0 },
83 + ])),
84 + ::quasi_router::stage::Op::Lit(::std::borrow::Cow::Borrowed(
85 + "</p><p class=\"text\">You&#39;ll receive a $5 credit code each billing cycle via email.</p><div class=\"anchored\" id=\"fan-plus-membership-anchored\" data-menu=\"anchored\" hidden></div></section",
86 + )),
87 + ])),
88 + ::quasi_router::stage::Op::Branch(::std::borrow::Cow::Borrowed(&[
89 + ::quasi_router::stage::Op::Lit(::std::borrow::Cow::Borrowed(
90 + "><section id=\"fan-plus-pitch\" class=\"region group\"><p class=\"text\">Support the platform and get something back every month.</p><h2 class=\"heading\">What you get</h2><ul class=\"list\"><li class=\"row\" data-row><span class=\"row-primary\">$5 monthly credit</span><span class=\"row-secondary\">A promo code delivered by email each billing cycle, usable toward any purchase on the platform</span></li><li class=\"row\" data-row><span class=\"row-primary\">+ badge</span><span class=\"row-secondary\">Displayed next to your name on your forum posts</span></li><li class=\"row\" data-row><span class=\"row-primary\">Forum signatures</span><span class=\"row-secondary\">A signature block rendered under everything you post</span></li><li class=\"row\" data-row><span class=\"row-primary\">Image embeds</span><span class=\"row-secondary\">Post images in forum threads</span></li></ul><div class=\"figures\"><div class=\"figure\" aria-label=\"per month: $8\"><span class=\"figure-value\" aria-hidden=\"true\">$8</span><span class=\"figure-caption\" aria-hidden=\"true\">per month</span></div></div><p class=\"text\">Makenotwork is built on 0% platform fees. Fan+ is how you directly support the platform&#39;s development and operations, while getting real value back each month.</p><div class=\"anchored\" id=\"fan-plus-pitch-anchored\" data-menu=\"anchored\" hidden></div></section",
91 + )),
92 + ])),
93 + ::quasi_router::stage::Op::Branch(::std::borrow::Cow::Borrowed(&[
94 + ::quasi_router::stage::Op::Lit(::std::borrow::Cow::Borrowed(
95 + "><button type=\"button\" class=\"button\" data-act hx-post=\"/stripe/fan-plus\" hx-swap=\"outerMorph\" hx-disable=\"this\" data-awaiting=\"indeterminate\">Join Fan+</button",
96 + )),
97 + ])),
98 + ::quasi_router::stage::Op::Branch(::std::borrow::Cow::Borrowed(&[
99 + ::quasi_router::stage::Op::Lit(::std::borrow::Cow::Borrowed(
100 + "><div class=\"rich\" data-disable-scripting><p><a href=\"/join\" rel=\"noopener noreferrer\">Create an account</a> to join, or <a href=\"/login\" rel=\"noopener noreferrer\">log in</a> if you already have one.</p>\n</div",
101 + )),
102 + ])),
103 + ::quasi_router::stage::Op::Lit(::std::borrow::Cow::Borrowed(
104 + "><div class=\"anchored\" id=\"fan-plus-anchored\" data-menu=\"anchored\" hidden></div></div>",
105 + )),
106 + ],
107 + );
@@ -1,0 +1,33 @@
1 + # The copy at `/fan-plus`, which is words and nothing else.
2 + #
3 + # Read at build time by the `declare!` in `src/quasi/fan_plus.rs`. Editing this
4 + # file is editing the page. See `content/policy.toml` for the split this follows.
5 + #
6 + # KEEP `benefits` AND THE FAN+ CARD ON THE LANDING PAGE IDENTICAL, AND KEEP BOTH
7 + # TO WHAT THE CODE GRANTS. Audited 2026-08-05, carried from the template because
8 + # it is a rule rather than a note: a benefit listed here and not granted by the
9 + # code is a promise nobody implemented.
10 +
11 + [[benefits]]
12 + name = "$5 monthly credit"
13 + detail = "A promo code delivered by email each billing cycle, usable toward any purchase on the platform"
14 +
15 + [[benefits]]
16 + name = "+ badge"
17 + detail = "Displayed next to your name on your forum posts"
18 +
19 + [[benefits]]
20 + name = "Forum signatures"
21 + detail = "A signature block rendered under everything you post"
22 +
23 + [[benefits]]
24 + name = "Image embeds"
25 + detail = "Post images in forum threads"
26 +
27 + # What a reader with no account is told, markdown through docengine. In the file
28 + # rather than a Rust `const` so the sentence folds into the residual: a `const`
29 + # is a path and not a literal, so an `include` of one opens a staged scope and
30 + # the sentence is built and rendered on every request.
31 +
32 + [[visitor]]
33 + body = "[Create an account](/join) to join, or [log in](/login) if you already have one."