Skip to main content

max / makenotwork

Serve /team and /use-cases from residuals too Two more screens onto the build-time seam, and the second is the first one that does not fold to a literal. /team reads nothing, so its residual is one Op::Lit like /policy's. Its bio prose moved to content/team.toml, a const being a path rather than a literal and so an include that opens a scope for one sentence. /use-cases is 10 literals and 9 holes: the nine cards are compiled markup and what varies is each card's tier line, built from the TierPrices the server holds in memory. Its nine profiles, four promises and three onward links were const arrays and are now content/use-cases.toml, so all three loops are unrolled before an AST exists. Serving the page writes nine short strings into gaps rather than building some two hundred nodes and rendering them. The copy file names the shape of a tier line rather than a price, a formatted price in content being a fourth place a price can go stale. That costs the Priced enum's exhaustiveness, since a proc macro cannot evaluate a path; a test holds the file to the four names tier_line answers to, which buys it back. One mount replaces policy's bespoke one. served_document_mount takes a body of Cow<'static, str>: a settled screen borrows its compiled literal and a holed one calls the generated filler, and neither builds a Node. Restores the nine card titles, missing since 84f0886b. The card wrote `label profile.title`, and a region's label is rendered only by a container that discloses or steps through its members; PROFILES is a plain group, so it reached no markup. The template drew each as `<div class="use-case-title">`. The card's own test asserted the anchor, the who and the features, and never the title. The residual checks are a table now, so a screen joins them with a row rather than by somebody copying two tests. every_screen_on_the_seam_is_checked fails when a screen is added to the roster and to neither check. `list [founder()]` is gone rather than folded. A bracket is the one place the grammar calls a shape without an include, and #[constant] does not reach it: a bracket also holds plain constructors, so folding every all-literal call in one would demand a Figure::new_constant that cannot exist. Incidence was one, tree-wide.
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 21:20 UTC
Signed with PGP, not checked
Commit: b4a83103474363c615ac6f7c01f46cc60b05152a
Parent: 3f708b5
8 files changed, +766 insertions, -382 deletions
@@ -666,13 +666,37 @@
666 666 vec![
667 667 (
668 668 team::PATH,
669 - public_document_mount(app, team::PATH, team::screen, team::renderer),
669 + served_document_mount(
670 + app,
671 + team::PATH,
672 + |_| team::page_screen(),
673 + team::renderer,
674 + |_| residuals::settled(&residuals::TEAM),
675 + ),
670 676 ),
671 677 (
672 678 use_cases::PATH,
673 - public_document_mount(app, use_cases::PATH, use_cases::screen, use_cases::renderer),
679 + served_document_mount(
680 + app,
681 + use_cases::PATH,
682 + |viewer| use_cases::page_screen(&use_cases::prices(viewer)),
683 + use_cases::renderer,
684 + |viewer| {
685 + use_cases::page_region_serve(&residuals::USE_CASES, &use_cases::prices(viewer))
686 + .into()
687 + },
688 + ),
689 + ),
690 + (
691 + policy::PATH,
692 + served_document_mount(
693 + app,
694 + policy::PATH,
695 + |_| policy::page_screen(),
696 + policy::renderer,
697 + |_| residuals::settled(&residuals::POLICY),
698 + ),
674 699 ),
675 - (policy::PATH, policy::mount(app)),
676 700 (
677 701 fan_plus::PATH,
678 702 public_document_mount(app, fan_plus::PATH, fan_plus::screen, fan_plus::renderer),
@@ -769,6 +793,77 @@
769 793 .into_router()
770 794 }
771 795
796 + /// A public document whose regions are already written, mounted.
797 + ///
798 + /// The seam's mount, shared by every screen on it. It is not
799 + /// [`public_document_mount`] and cannot be: that one builds a
800 + /// `quasi_axum::Adapter`, whose whole job is to call a handler that answers a
801 + /// `Screen` and render it, and there is no `Screen` here to answer with. The
802 + /// markup was derived on a build machine, so answering is writing the document
803 + /// around it (quasicoherent `793d99dd`).
804 + ///
805 + /// `body` is where the two kinds of screen differ, and it is the only place
806 + /// they do. A screen that reads nothing has a residual of one literal, so its
807 + /// `body` hands back `residual.settled()` and copies nothing. A screen that
808 + /// reads something has holes, so its `body` calls the generated filler, which
809 + /// walks the residual and writes the request's values into the gaps. Neither
810 + /// builds a `Node`, which is the property the seam exists for; a `Cow` is what
811 + /// lets the first path stay a borrow while the second returns a `String`.
812 + ///
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.
816 + ///
817 + /// Pairing a body with the wrong screen serves one page's markup inside
818 + /// another's document, which nothing here can catch; what catches it is
819 + /// `residuals::tests`, which produces each screen's markup both ways and
820 + /// compares them.
821 + fn served_document_mount(
822 + app: &AppState,
823 + path: &'static str,
824 + screen: fn(&Viewer) -> quasi_router::Screen,
825 + renderer: fn(&Viewer) -> quasi_webview::Webview,
826 + body: fn(&Viewer) -> std::borrow::Cow<'static, str>,
827 + ) -> axum::Router {
828 + let app = app.clone();
829 + axum::Router::new().route(
830 + path,
831 + axum::routing::get(move |parts: axum::http::request::Parts| {
832 + let app = app.clone();
833 + async move {
834 + use axum::response::IntoResponse as _;
835 +
836 + // `Audience::Anyone` never refuses a reader for being signed
837 + // out, so the error arm here is the session layer or the CSRF
838 + // store failing, which is this server being broken rather than
839 + // this page being unreachable. Same answer the adapter gives
840 + // every other screen for the same failure.
841 + let Ok(viewer) = viewer_factory(app.clone(), Audience::Anyone)(&parts).await else {
842 + return axum::http::StatusCode::INTERNAL_SERVER_ERROR.into_response();
843 + };
844 +
845 + let markup = body(&viewer);
846 + axum::response::Html(served(&viewer, screen, renderer, &markup)).into_response()
847 + }
848 + }),
849 + )
850 + }
851 +
852 + /// The document written around markup this renderer already produced.
853 + ///
854 + /// Split out only so [`served_document_mount`] reads as one expression, and
855 + /// because `Webview::served` carries the one caveat worth keeping next to a
856 + /// call: the markup has to have come from this renderer, which is what
857 + /// `residuals::tests` asserts and nothing at this level can.
858 + fn served(
859 + viewer: &Viewer,
860 + screen: fn(&Viewer) -> quasi_router::Screen,
861 + renderer: fn(&Viewer) -> quasi_webview::Webview,
862 + markup: &str,
863 + ) -> String {
864 + renderer(viewer).served(&screen(viewer), markup)
865 + }
866 +
772 867 /// Every described screen a reader with no session can reach, mounted.
773 868 ///
774 869 /// Separate from [`mounts`] because of the factory, not because of the address:
@@ -1044,14 +1139,14 @@
1044 1139 // Two mount styles, and both are public documents. Most screens go
1045 1140 // through the adapter, which calls a handler that answers a `Screen`
1046 1141 // and renders it. A screen on the residual seam has no `Screen` to
1047 - // answer with, so it brings its own mount; `policy` is the first
1142 + // answer with, so it takes [`served_document_mount`] instead
1048 1143 // (quasicoherent `793d99dd`). Counting only the first style would let a
1049 1144 // screen leave `PUBLIC_DOCUMENT_PATHS` unnoticed by moving onto the
1050 1145 // seam, which is the drift this test exists to catch.
1051 1146 let adapted = mounted.matches("public_document_mount(").count();
1052 - let own = mounted.matches("::mount(app)").count();
1147 + let served = mounted.matches("served_document_mount(").count();
1053 1148 assert_eq!(
1054 - adapted + own,
1149 + adapted + served,
1055 1150 PUBLIC_DOCUMENT_PATHS.len(),
1056 1151 "public_document_mounts and PUBLIC_DOCUMENT_PATHS describe the same screens"
1057 1152 );
@@ -82,53 +82,6 @@
82 82 Ok(page_screen().into())
83 83 }
84 84
85 - /// The address, served from the residual rather than from a tree.
86 - ///
87 - /// The first screen on the seam (quasicoherent `793d99dd`). Its regions were
88 - /// derived on a build machine and are a `&'static str` in this binary, so
89 - /// answering is writing the document around them: no `Node` is constructed and
90 - /// nothing is rendered.
91 - ///
92 - /// **Its own mount rather than [`super::public_document_mount`]**, because the
93 - /// adapter's whole job is to call a handler that answers a `Screen` and render
94 - /// it. There is no `Screen` here to answer with, and inventing an `Outcome`
95 - /// that carries markup would put an opaque string in the vocabulary for every
96 - /// screen to reach for. The two paths coexist by construction, which is what
97 - /// the transition said they would: every other public document is still mounted
98 - /// the old way on the line above this one.
99 - pub fn mount(app: &crate::AppState) -> axum::Router {
100 - let app = app.clone();
101 - axum::Router::new().route(
102 - PATH,
103 - axum::routing::get(move |parts: axum::http::request::Parts| {
104 - let app = app.clone();
105 - async move { document(&app, &parts).await }
106 - }),
107 - )
108 - }
109 -
110 - /// The document, around markup that is already written.
111 - async fn document(
112 - app: &crate::AppState,
113 - parts: &axum::http::request::Parts,
114 - ) -> axum::response::Response {
115 - use axum::response::IntoResponse as _;
116 -
117 - // `Audience::Anyone` never refuses a reader for being signed out, so the
118 - // error arm here is the session layer or the CSRF store failing, which is
119 - // this server being broken rather than this page being unreachable. Same
120 - // answer the adapter gives every other screen for the same failure.
121 - let Ok(viewer) = super::viewer_factory(app.clone(), super::Audience::Anyone)(parts).await
122 - else {
123 - return axum::http::StatusCode::INTERNAL_SERVER_ERROR.into_response();
124 - };
125 - let body = super::residuals::POLICY
126 - .settled()
127 - .expect("the policy page reads nothing, so its residual is one literal");
128 -
129 - axum::response::Html(renderer(&viewer).served(&page_screen(), body)).into_response()
130 - }
131 -
132 85 declare! {
133 86 /// The whole document: the title, the measure, the body.
134 87 pub(crate) shape page_screen() -> Screen;
@@ -59,9 +59,38 @@
59 59 /// know what is on the seam and what is still building a tree, and the
60 60 /// alternative was a macro nobody can grep.
61 61 fn roster() -> Vec<Staged> {
62 - vec![("POLICY", |plan| {
63 - Node::Region(super::policy::page_region_staged(plan))
64 - })]
62 + vec![
63 + ("POLICY", |plan| {
64 + Node::Region(super::policy::page_region_staged(plan))
65 + }),
66 + ("TEAM", |plan| {
67 + Node::Region(super::team::page_region_staged(plan))
68 + }),
69 + ("USE_CASES", |plan| {
70 + Node::Region(super::use_cases::page_region_staged(plan))
71 + }),
72 + ]
73 + }
74 +
75 + /// A settled screen's markup, borrowed rather than built.
76 + ///
77 + /// The `body` half of [`super::served_document_mount`] for a screen that reads
78 + /// nothing. A `Cow::Borrowed` of the compiled literal: no allocation, no walk,
79 + /// no `Node`, and the same `&'static str` for every reader on every request.
80 + ///
81 + /// # Panics
82 + ///
83 + /// When the residual has a hole, a branch or a loop in it, which means the
84 + /// screen started reading the request and wants the filler instead. It cannot
85 + /// happen behind a mount that compiles, because
86 + /// `tests::a_settled_screen_is_one_literal` fails first.
87 + #[must_use]
88 + pub fn settled(residual: &'static Residual) -> std::borrow::Cow<'static, str> {
89 + std::borrow::Cow::Borrowed(
90 + residual
91 + .settled()
92 + .expect("a settled screen's residual is one literal"),
93 + )
65 94 }
66 95
67 96 /// The residual of one staged screen, derived by rendering it now.
@@ -112,24 +141,72 @@
112 141 );
113 142 }
114 143
115 - /// `/policy` compiles to a single `&'static str`.
144 + /// One screen on the seam, and the two ways to produce its markup.
145 + ///
146 + /// A table for [`roster`]'s reason: the checks below hold of every settled
147 + /// screen and not of `/policy` in particular, so a screen joins them by
148 + /// adding a row rather than by somebody copying two tests and renaming the
149 + /// halves they remembered to.
150 + struct Settled {
151 + /// What the address is, for a failure to name.
152 + path: &'static str,
153 + /// The compiled residual.
154 + residual: &'static Residual,
155 + /// The whole document, which is what the region has to appear inside.
156 + screen: fn() -> quasi_router::Screen,
157 + /// The region on its own, rendered the ordinary way.
158 + region: fn() -> Node,
159 + }
160 +
161 + /// Every screen whose residual settles to one literal.
162 + ///
163 + /// Named for what it returns rather than `settled`, which is the helper in
164 + /// the module above and would be shadowed by this under `use super::*`.
165 + ///
166 + /// Not every entry in [`roster`] belongs here: a screen that reads a request
167 + /// has holes, and `settled` answers `None` for it. Those are checked by
168 + /// filling instead.
169 + fn one_literal_screens() -> Vec<Settled> {
170 + vec![
171 + Settled {
172 + path: crate::quasi::policy::PATH,
173 + residual: &POLICY,
174 + screen: crate::quasi::policy::page_screen,
175 + region: || Node::Region(crate::quasi::policy::page_region()),
176 + },
177 + Settled {
178 + path: crate::quasi::team::PATH,
179 + residual: &TEAM,
180 + screen: crate::quasi::team::page_screen,
181 + region: || Node::Region(crate::quasi::team::page_region()),
182 + },
183 + ]
184 + }
185 +
186 + /// A settled screen's residual is one `Op::Lit` and nothing else.
116 187 ///
117 188 /// Phase 2's sharp test (`60047dc0`), which moved here when it turned out
118 - /// to be a pipeline property rather than a vocabulary one. The screen's
119 - /// declaration holds no operator and now reaches no value a request brings:
120 - /// its copy is in `content/policy.toml` and read at macro time, and its six
121 - /// prose includes are `#[constant]`, so each is evaluated once while the
122 - /// residual is derived rather than once per request.
189 + /// to be a pipeline property rather than a vocabulary one. These screens
190 + /// hold no operator and reach no value a request brings: their copy is in
191 + /// `content/` and read at macro time, and every shape they include is
192 + /// `#[constant]`, so each is evaluated once while the residual is derived
193 + /// rather than once per request.
123 194 ///
124 - /// One `Op::Lit` and nothing else is what that adds up to. A hole, a branch
125 - /// or a loop appearing here means something about the page started
126 - /// depending on the request, which is a real change and worth failing on.
195 + /// A hole, a branch or a loop appearing here means something about a page
196 + /// started depending on the request, which is a real change and worth
197 + /// failing on.
127 198 #[test]
128 - fn the_policy_page_is_one_literal() {
129 - let ops = POLICY.ops();
199 + fn a_settled_screen_is_one_literal() {
200 + for screen in one_literal_screens() {
201 + let ops = screen.residual.ops();
130 202
131 - assert_eq!(ops.len(), 1, "{ops:?}");
132 - assert!(matches!(ops[0], quasi_router::stage::Op::Lit(_)), "{ops:?}");
203 + assert_eq!(ops.len(), 1, "{}: {ops:?}", screen.path);
204 + assert!(
205 + matches!(ops[0], quasi_router::stage::Op::Lit(_)),
206 + "{}: {ops:?}",
207 + screen.path,
208 + );
209 + }
133 210 }
134 211
135 212 /// The residual is the document's own bytes, not a lookalike.
@@ -141,13 +218,18 @@
141 218 /// than the page has always had. So the fragment has to appear in the
142 219 /// document verbatim, and this is what says it does.
143 220 #[test]
144 - fn what_the_residual_holds_appears_in_the_document_verbatim() {
221 + fn what_a_residual_holds_appears_in_its_document_verbatim() {
145 222 use quasi_axum::Serves as _;
146 223
147 - let document = Webview::new().screen(&crate::quasi::policy::page_screen());
148 - let body = POLICY.settled().expect("nothing in the page varies");
224 + for screen in one_literal_screens() {
225 + let document = Webview::new().screen(&(screen.screen)());
226 + let body = screen
227 + .residual
228 + .settled()
229 + .expect("a settled screen's residual is one literal");
149 230
150 - assert!(document.contains(body), "{document}");
231 + assert!(document.contains(body), "{}: {document}", screen.path);
232 + }
151 233 }
152 234
153 235 /// Filling the residual gives back what the renderer gives.
@@ -157,14 +239,105 @@
157 239 /// check is that the two paths agree byte for byte on a screen that has
158 240 /// nothing varying in it.
159 241 #[test]
160 - fn the_residual_serves_what_the_renderer_serves() {
242 + fn a_residual_serves_what_the_renderer_serves() {
161 243 use quasi_axum::Serves as _;
162 244
163 - let rendered = Webview::new().fragment(&Node::Region(crate::quasi::policy::page_region()));
245 + for screen in one_literal_screens() {
246 + let rendered = Webview::new().fragment(&(screen.region)());
247 +
248 + assert_eq!(
249 + screen
250 + .residual
251 + .settled()
252 + .expect("a settled screen's residual is one literal"),
253 + rendered,
254 + "{}",
255 + screen.path,
256 + );
257 + }
258 + }
259 +
260 + /// A holed screen's residual fills to what the renderer builds.
261 + ///
262 + /// `/use-cases` is the first screen on the seam that does not fold to a
263 + /// literal, and this is the check the settled ones cannot have: it is the
264 + /// filler that is being tested, not a borrow. Nine holes, one per card's
265 + /// tier line, and the equality is byte for byte against the tree the screen
266 + /// has always built.
267 + ///
268 + /// Two price sets rather than one, and that is the point. Filling with the
269 + /// defaults would pass against a residual that had baked one request's
270 + /// prices into its literals, which is the exact failure the seam could
271 + /// have. Prices nothing else in the tree uses cannot be baked.
272 + #[test]
273 + fn the_use_cases_residual_fills_to_what_the_renderer_builds() {
274 + use quasi_axum::Serves as _;
275 +
276 + for prices in [crate::tier_prices::TierPrices::default(), odd_prices()] {
277 + let filled = crate::quasi::use_cases::page_region_serve(&USE_CASES, &prices);
278 + let rendered = Webview::new()
279 + .fragment(&Node::Region(crate::quasi::use_cases::page_region(&prices)));
280 +
281 + assert_eq!(filled, rendered);
282 + }
283 + }
284 +
285 + /// The nine holes are holes, and the rest of the page is not.
286 + ///
287 + /// What says the split landed where the module header claims. A tenth hole
288 + /// means something else on the page started reading the request; a loop or
289 + /// a branch means the copy stopped being read at macro time, which is the
290 + /// regression `98fbee62` exists to prevent and which no rendering test
291 + /// would notice.
292 + #[test]
293 + fn the_use_cases_residual_is_nine_holes_and_literals() {
294 + use quasi_router::stage::Op;
295 +
296 + let ops = USE_CASES.ops();
297 + let holes = ops
298 + .iter()
299 + .filter(|op| matches!(op, Op::Hole { .. }))
300 + .count();
301 +
302 + assert_eq!(holes, 9, "one per card's tier line: {ops:?}");
303 + assert!(
304 + ops.iter()
305 + .all(|op| matches!(op, Op::Lit(_) | Op::Hole { .. })),
306 + "a branch or a loop reached the residual: {ops:?}",
307 + );
308 + }
309 +
310 + /// Prices no default and no assumptions file would produce.
311 + fn odd_prices() -> crate::tier_prices::TierPrices {
312 + crate::tier_prices::TierPrices {
313 + basic_std: 4321,
314 + small_files_std: 5678,
315 + big_files_std: 8765,
316 + ..Default::default()
317 + }
318 + }
319 +
320 + /// Every screen the roster names is checked above.
321 + ///
322 + /// The gap this closes is the one a table opens: a screen added to
323 + /// [`roster`] and not checked anywhere is generated, compiled, served and
324 + /// never compared against the renderer, and nothing else here would say so.
325 + ///
326 + /// `HOLED` is the count of screens checked by a filler of their own rather
327 + /// than by [`one_literal_screens`], and it is written out rather than
328 + /// derived so that adding a screen to the roster and forgetting its check
329 + /// fails here. A screen that has holes cannot join the settled table, so
330 + /// the two counts have to be kept by hand or not at all.
331 + #[test]
332 + fn every_screen_on_the_seam_is_checked() {
333 + /// Screens with their own filling test: `/use-cases`.
334 + const HOLED: usize = 1;
164 335
165 336 assert_eq!(
166 - POLICY.settled().expect("nothing in the page varies"),
167 - rendered,
337 + roster().len(),
338 + one_literal_screens().len() + HOLED,
339 + "a screen on the seam is not checked: give it a row in \
340 + `one_literal_screens`, or a filling test and a bump to HOLED",
168 341 );
169 342 }
170 343 }
@@ -67,38 +67,54 @@
67 67
68 68 declare! {
69 69 /// The whole document: the title, the measure, the body.
70 - shape page_screen() -> Screen;
70 + pub(crate) shape page_screen() -> Screen;
71 71
72 72 screen single "Team - Makenotwork" {
73 73 measured MEASURE;
74 74 documented Document::default().classed(crate::shell::body_class(MEASURE, &["team-page"]));
75 75 summarised "Small by design: who builds Makenotwork, and what they did before.";
76 76
77 - region PAGE_REGION as Pane {
78 - page "Team";
79 - text "Small by design. The base platform was designed by the founder, who intends \
80 - to stay as the technical lead through the project's growth.";
81 - section "Founder";
82 - list [founder()];
83 - include bio();
84 - }
77 + include page_region();
85 78 }
86 79 }
87 80
88 81 declare! {
89 - /// The one card on the page.
82 + /// The page's one region, split out so it can be staged.
90 83 ///
91 - /// A row rather than a grid of one: `use-case-grid-2col` held a single article,
92 - /// so the grid was describing an intention rather than a layout. When there is a
93 - /// second person the list grows a second row and the design system decides how
94 - /// two rows sit, which is the arrangement question answered once for every
95 - /// screen instead of in this page's own CSS.
96 - shape founder() -> Row;
84 + /// The split is where the request stops mattering, which on this page is
85 + /// immediately: nothing below this line reads anything. See
86 + /// `policy::page_region` for why the document cannot come with it.
87 + #[staged]
88 + pub(crate) shape page_region() -> Slot;
97 89
98 - row "Max" {
99 - secondary "CEO & Tech Lead";
100 - meta "Designed and built the platform.";
101 - act "@max" to get "/u/max" navigating;
90 + region PAGE_REGION as Pane {
91 + page "Team";
92 + text "Small by design. The base platform was designed by the founder, who intends \
93 + to stay as the technical lead through the project's growth.";
94 + section "Founder";
95 + // A row rather than a grid of one: `use-case-grid-2col` held a single
96 + // article, so the grid was describing an intention rather than a
97 + // layout. When there is a second person the list grows a second row and
98 + // the design system decides how two rows sit, which is the arrangement
99 + // question answered once for every screen instead of in this page's own
100 + // CSS.
101 + //
102 + // Written here rather than in a `founder()` shape of its own, and that
103 + // is a staging constraint rather than a preference. `list [founder()]`
104 + // is a bracket list, which is the one place the grammar calls a shape
105 + // without an `include`, and the `#[constant]` fold does not reach it: a
106 + // bracket also holds plain constructors (`stats [Figure::new(..)]`), so
107 + // folding every all-literal call in one would demand a
108 + // `Figure::new_constant` that cannot exist. One row is not worth a
109 + // production, so it moved.
110 + list {
111 + row "Max" {
112 + secondary "CEO & Tech Lead";
113 + meta "Designed and built the platform.";
114 + act "@max" to get "/u/max" navigating;
115 + }
116 + }
117 + include bio();
102 118 }
103 119 }
104 120
@@ -106,25 +122,25 @@
106 122 /// The bio, closed until asked for.
107 123 ///
108 124 /// `showing_at_most_one None` is closed; `Some(0)` would be `<details open>`.
125 + ///
126 + /// `#[constant]`: it reads nothing, so a staged `include` of it folds to
127 + /// the disclosure's markup instead of opening a scope per request. The
128 + /// words are in `content/team.toml` and read at macro time, so the loop is
129 + /// unrolled before any AST exists and nothing here survives to a request.
130 + #[constant]
109 131 shape bio() -> Node;
110 132
111 133 region BIO as Group {
112 134 showing_at_most_one None;
113 135 region BIO_BODY as Pane {
114 136 label "Bio";
115 - text BIO_TEXT;
137 + for part in copy "content/team.toml" as bio {
138 + text part.body;
139 + }
116 140 }
117 141 }
118 142 }
119 143
120 - /// Carried over verbatim from the template. Prose the founder wrote, so it is
121 - /// content rather than copy and nothing here rewrites it.
122 - const BIO_TEXT: &str = "I started in the creator space as a young kid making backgrounds and \
123 - montages for CoD MW2 players. I picked up programming around the same time, helping Minecraft \
124 - server owners with whatever they needed. I spent the years after that focused on my \
125 - education: a BA in mathematics at the University of Chicago, then a PhD in mathematics at UC \
126 - San Diego. Afterward I decided to start my own business, and here we are.";
127 -
128 144 /// The document this screen is drawn in.
129 145 ///
130 146 /// The head, the tail and the token meta come off
@@ -186,6 +202,27 @@
186 202 assert!(html.contains(r#"href="/u/max""#), "{html}");
187 203 }
188 204
205 + /// The bio the content file holds is the bio on the page.
206 + ///
207 + /// Reads `content/team.toml` rather than holding a second copy of the
208 + /// prose, for `policy`'s reason: the test asks the content file what the
209 + /// page should say, so an edit to the words is an edit and not a failure,
210 + /// and words that stop being rendered are.
211 + #[test]
212 + fn the_bio_is_the_prose_the_content_file_holds() {
213 + let copy: toml::Table = include_str!("../../content/team.toml")
214 + .parse()
215 + .expect("the team copy is TOML");
216 + let bio = copy["bio"].as_array().expect("a list of one bio");
217 + assert_eq!(bio.len(), 1, "the page shows one founder");
218 +
219 + let body = bio[0]["body"].as_str().expect("a body");
220 + assert!(
221 + html().contains(&crate::helpers::escape_html(body)),
222 + "{body}"
223 + );
224 + }
225 +
189 226 /// `736f45a5`: this screen's markup carries none of the four spellings.
190 227 #[test]
191 228 fn the_page_spells_no_spinner() {
@@ -39,6 +39,25 @@
39 39 //! reads it off `viewer.app` rather than taking a state of its own: a public
40 40 //! document already has a per-request viewer, and a second copy of the prices
41 41 //! is a second thing to keep in step with the calculator.
42 + //!
43 + //! # The words are in `content/use-cases.toml`, and the prices are the holes
44 + //!
45 + //! The first screen on the build-time seam that does not fold to a literal, and
46 + //! it is the one that shows what the seam is for. Nine profiles, four promises
47 + //! and three onward links were `const` arrays here; they are copy now, read at
48 + //! macro time, so all three loops are unrolled before an AST exists. What is
49 + //! left varying is nine tier lines, each built from the live `TierPrices`.
50 + //!
51 + //! So the residual is the page's markup with nine holes in it. Serving it
52 + //! writes nine strings into gaps. The comparison is against what this page used
53 + //! to do, which was build some two hundred nodes and render them, on every
54 + //! request, to produce markup that differed in nine short strings.
55 + //!
56 + //! A profile's `features` is a list field, which is what the copy production
57 + //! grew to admit this page (`for feature in profile.features`). A list is still
58 + //! words; the seal that file's header describes is against types, and the
59 + //! alternative here was nine numbered fields with the count written into the
60 + //! declaration.
42 61
43 62 use makeover_layout as layout;
44 63 use quasi_declare::declare;
@@ -61,245 +80,84 @@
61 80
62 81 const MEASURE: layout::Measure = layout::Measure::Wide;
63 82
64 - /// One creator profile.
83 + /// The four tier lines a card can end with, by the name the copy gives.
65 84 ///
66 - /// `anchor` is the template's `id=`, kept because the marketing site links to
67 - /// these cards directly (`/use-cases#podcasters`) and a conversion that drops
68 - /// the id breaks somebody else's link rather than this page.
69 - struct Profile {
70 - anchor: &'static str,
71 - title: &'static str,
72 - who: &'static str,
73 - description: &'static str,
74 - features: &'static [&'static str],
75 - /// Which tier line the card ends with. See [`Priced`].
76 - priced: Priced,
77 - }
78 -
79 - /// How a card states its price.
85 + /// The one thing on this page a request decides. Everything else a card says is
86 + /// words and lives in `content/use-cases.toml`; the price does not, because a
87 + /// formatted price in a content file is a fourth place a price can go stale.
88 + /// So the file names the *shape* of the line and this builds it from the live
89 + /// `TierPrices`.
80 90 ///
81 - /// The template wrote three shapes of tier line by hand, and one of them
82 - /// (Educators) names two tiers. Naming the shape rather than the string is what
83 - /// keeps every price on this page coming from `TierPrices` -- a formatted string
84 - /// in the table here would be a fourth place a price can go stale.
85 - #[derive(Clone, Copy)]
86 - enum Priced {
87 - Basic,
88 - SmallFiles,
89 - BigFiles,
90 - /// "Basic $X/mo or Small Files $Y/mo", which is Educators and only
91 - /// Educators: the work fits in either tier depending on what is uploaded.
92 - BasicOrSmallFiles,
93 - }
94 -
95 - impl Priced {
96 - /// The line the card ends with, built from the live prices.
97 - fn line(self, prices: &TierPrices) -> String {
98 - match self {
99 - Self::Basic => format!(
100 - "Basic ${}/mo · {}, {}/file",
101 - prices.basic_std, prices.basic_total, prices.basic_per_file
102 - ),
103 - Self::SmallFiles => format!(
104 - "Small Files ${}/mo · {}, {}/file",
105 - prices.small_files_std, prices.small_files_total, prices.small_files_per_file
106 - ),
107 - Self::BigFiles => format!(
108 - "Big Files ${}/mo · {}, {}/file",
109 - prices.big_files_std, prices.big_files_total, prices.big_files_per_file
110 - ),
111 - Self::BasicOrSmallFiles => format!(
112 - "Basic ${}/mo or Small Files ${}/mo",
113 - prices.basic_std, prices.small_files_std
114 - ),
115 - }
91 + /// A `&str` rather than the enum this replaced, and that is the cost of the
92 + /// copy moving out of Rust: a proc macro cannot evaluate a path, so a variant
93 + /// written in the file would survive into the AST as one. What buys the
94 + /// exhaustiveness back is [`tests::every_priced_name_in_the_copy_is_one_of_the_four`],
95 + /// which holds the file to the four names this answers.
96 + ///
97 + /// # Panics
98 + ///
99 + /// On a name this does not know, which is a content file naming a line that
100 + /// does not exist. The test above is what makes that a test failure rather
101 + /// than a page that renders empty.
102 + fn tier_line(priced: &str, prices: &TierPrices) -> String {
103 + match priced {
104 + "basic" => format!(
105 + "Basic ${}/mo \u{b7} {}, {}/file",
106 + prices.basic_std, prices.basic_total, prices.basic_per_file
107 + ),
108 + "small-files" => format!(
109 + "Small Files ${}/mo \u{b7} {}, {}/file",
110 + prices.small_files_std, prices.small_files_total, prices.small_files_per_file
111 + ),
112 + "big-files" => format!(
113 + "Big Files ${}/mo \u{b7} {}, {}/file",
114 + prices.big_files_std, prices.big_files_total, prices.big_files_per_file
115 + ),
116 + // "Basic $X/mo or Small Files $Y/mo", which is Educators and only
117 + // Educators: the work fits in either tier depending on what is
118 + // uploaded.
119 + "basic-or-small-files" => format!(
120 + "Basic ${}/mo or Small Files ${}/mo",
121 + prices.basic_std, prices.small_files_std
122 + ),
123 + other => panic!("content/use-cases.toml names a tier line that does not exist: {other}"),
116 124 }
117 125 }
118 126
119 - /// The nine, in the order the template listed them.
120 - const PROFILE_LIST: &[Profile] = &[
121 - Profile {
122 - anchor: "musicians",
123 - title: "Musicians",
124 - who: "Bands, solo artists, producers",
125 - description: "Sell albums, singles, and EPs with in-browser streaming, chapter markers, and cover art.",
126 - features: &[
127 - "MP3, FLAC, WAV, OGG, AAC, AIFF upload",
128 - "In-browser audio player with chapters",
129 - "Cover art and rich metadata",
130 - "RSS feed per project",
131 - "Pay-what-you-want pricing",
132 - ],
133 - priced: Priced::SmallFiles,
134 - },
135 - Profile {
136 - anchor: "podcasters",
137 - title: "Podcasters",
138 - who: "Independent shows, networks",
139 - description: "Host and distribute your podcast with full RSS support for every major directory.",
140 - features: &[
141 - "RSS for Apple, Spotify, Pocket Casts",
142 - "Chapter markers",
143 - "Subscriber-only feeds",
144 - "Broadcast emails to followers",
145 - ],
146 - priced: Priced::SmallFiles,
147 - },
148 - Profile {
149 - anchor: "writers",
150 - title: "Writers",
151 - who: "Bloggers, newsletter writers, authors",
152 - description: "Publish long-form writing with Markdown, per-project blogs, and subscriber broadcasts.",
153 - features: &[
154 - "Markdown editor with formatting",
155 - "Per-project blog",
156 - "RSS feed",
157 - "Scheduled publishing",
158 - "Broadcast emails",
159 - ],
160 - priced: Priced::Basic,
161 - },
162 - Profile {
163 - anchor: "developers",
164 - title: "Software Developers",
165 - who: "Indie devs, tool makers, plugin authors",
166 - description: "Distribute software with versioned releases, license keys, and a built-in git browser.",
167 - features: &[
168 - "Versioned releases with changelogs",
169 - "License keys with activation tracking",
170 - "Git source browser",
171 - "Promo codes and discounts",
172 - ],
173 - priced: Priced::SmallFiles,
174 - },
175 - Profile {
176 - anchor: "sample-packs",
177 - title: "Sample Packs",
178 - who: "Sound designers, beat makers, foley artists",
179 - description: "Sell sample packs and sound effects with audio previews and versioned downloads.",
180 - features: &[
181 - "Audio previews in-browser",
182 - "Versioned downloads",
183 - "Hierarchical tags for organization",
184 - "Pay-what-you-want pricing",
185 - "Cover art",
186 - ],
187 - priced: Priced::SmallFiles,
188 - },
189 - Profile {
190 - anchor: "digital-art",
191 - title: "Digital Art & Photography",
192 - who: "Illustrators, photographers, font designers",
193 - description: "Sell digital art, photography, and fonts with versioned releases and subscription tiers.",
194 - features: &[
195 - "Any file type supported",
196 - "Versioned releases",
197 - "Subscription tiers",
198 - "Cover images",
199 - ],
200 - priced: Priced::SmallFiles,
201 - },
202 - Profile {
203 - anchor: "educators",
204 - title: "Educators",
205 - who: "Course creators, tutors, workshop leaders",
206 - description: "Publish courses and teaching materials with text content, downloads, and subscriptions.",
207 - features: &[
208 - "Text content with Markdown",
209 - "Downloadable materials",
210 - "Subscription tiers",
211 - "Scheduled publishing",
212 - ],
213 - priced: Priced::BasicOrSmallFiles,
214 - },
215 - Profile {
216 - anchor: "game-devs",
217 - title: "Game Developers",
218 - who: "Indie games, mods, TTRPG content",
219 - description: "Distribute games up to 20GB per file with versioned downloads, license keys, and promo codes. Need more than 20GB? Request a size increase from your dashboard.",
220 - features: &[
221 - "Up to 20GB per file (increase on request)",
222 - "Versioned downloads with changelogs",
223 - "License keys with activation limits",
224 - "Promo codes",
225 - ],
226 - priced: Priced::BigFiles,
227 - },
228 - Profile {
229 - anchor: "comic-creators",
230 - title: "Comic Creators",
231 - who: "Webcomic artists, graphic novelists",
232 - description: "Sell digital comics and graphic novels with project organization and scheduled publishing.",
233 - features: &[
234 - "PDF, CBZ, and any file type",
235 - "Project organization",
236 - "Subscriptions",
237 - "Scheduled publishing",
238 - ],
239 - priced: Priced::SmallFiles,
240 - },
241 - ];
127 + /// Every name [`tier_line`] answers to, which is what the copy is held to.
128 + #[cfg(test)]
129 + const TIER_LINES: &[&str] = &["basic", "small-files", "big-files", "basic-or-small-files"];
242 130
243 - /// One thing every tier includes.
244 - struct Included {
245 - name: &'static str,
246 - sentence: &'static str,
247 - }
248 -
249 - /// What every tier includes, name and sentence, in the template's order.
250 - const UNIVERSAL_LIST: &[Included] = &[
251 - Included {
252 - name: "0% platform fee",
253 - sentence: "Only ~3% payment processing. No percentage cuts, no revenue sharing.",
254 - },
255 - Included {
256 - name: "Full data export",
257 - sentence: "Download everything anytime. Projects, items, sales, contacts: all yours.",
258 - },
259 - Included {
260 - name: "No lock-in",
261 - sentence: "Month-to-month. Cancel anytime. Take your audience with you.",
262 - },
263 - Included {
264 - name: "Source available",
265 - sentence: "Read the codebase. Verify every claim about privacy and data handling.",
266 - },
267 - ];
268 -
269 - /// One way onward off this page.
270 - struct Onward {
271 - label: &'static str,
272 - route: &'static str,
273 - }
274 -
275 - /// Where the page sends a reader who is convinced, and where it sends one who
276 - /// is not yet.
277 - const ONWARD: &[Onward] = &[
278 - Onward {
279 - label: "Pricing Calculator",
280 - route: "/pricing",
281 - },
282 - Onward {
283 - label: "Browse as guest",
284 - route: "/discover",
285 - },
286 - Onward {
287 - label: "Creator Guide",
288 - route: "/docs/getting-started",
289 - },
290 - ];
291 -
292 - /// The page. Reads no database, only the prices already in memory.
293 - pub fn screen(viewer: &super::Viewer, _request: Request) -> Result<Response, RouteError> {
131 + /// The prices this page ends every card with.
132 + ///
133 + /// Read off the viewer rather than from a state of this screen's own, which is
134 + /// the module header's point: a public document already has a per-request
135 + /// viewer, and a second copy of the prices is a second thing to keep in step
136 + /// with the calculator.
137 + ///
138 + /// Its own function because both halves of the mount want it: the document, to
139 + /// state the screen, and the body, to fill the nine holes.
140 + #[must_use]
141 + pub fn prices(viewer: &super::Viewer) -> TierPrices {
294 142 use axum::extract::FromRef as _;
295 143
296 - let billing = crate::Billing::from_ref(&viewer.app);
297 - Ok(page_screen(&billing.tier_prices).into())
144 + crate::Billing::from_ref(&viewer.app).tier_prices
145 + }
146 +
147 + /// The page. Reads no database, only the prices already in memory.
148 + ///
149 + /// Kept beside the residual mount rather than replaced by it, and it is what
150 + /// the residual is checked against: `quasi::residuals` asserts that filling the
151 + /// compiled one gives back exactly what building and rendering this gives. A
152 + /// screen with no second way to produce its markup has nothing to check the
153 + /// first one with.
154 + pub fn screen(viewer: &super::Viewer, _request: Request) -> Result<Response, RouteError> {
155 + Ok(page_screen(&prices(viewer)).into())
298 156 }
299 157
300 158 declare! {
301 159 /// The whole document: the title, the measure, the body.
302 - shape page_screen(prices: &TierPrices) -> Screen;
160 + pub(crate) shape page_screen(prices: &TierPrices) -> Screen;
303 161
304 162 screen single "Use Cases - Makenotwork" {
305 163 measured MEASURE;
@@ -307,53 +165,77 @@
307 165 summarised "A flat monthly fee and no platform cut, for musicians, podcasters, writers, \
308 166 developers and six more kinds of creator.";
309 167
310 - region PAGE_REGION as Pane {
311 - page "Use Cases";
312 - text "A flat monthly fee. 0% platform cut. Who it's built for:";
313 -
314 - section "Available now";
315 - region PROFILES as Group {
316 - for profile in PROFILE_LIST {
317 - include card(profile, prices);
318 - }
319 - }
320 -
321 - section "Everyone gets";
322 - region UNIVERSAL as Group {
323 - list {
324 - for gets in UNIVERSAL_LIST {
325 - row gets.name {
326 - secondary gets.sentence;
327 - }
328 - }
329 - }
330 - }
331 -
332 - act "Join the Alpha" to get "/join" navigating;
333 - for onward in ONWARD {
334 - act onward.label to get onward.route navigating;
335 - }
336 - }
168 + include page_region(prices);
337 169 }
338 170 }
339 171
340 172 declare! {
341 - /// One profile card: who it is for, what they get, what it costs.
173 + /// The page's one region, split out so it can be staged.
342 174 ///
343 - /// The region is identified by the template's anchor so `/use-cases#podcasters`
344 - /// still lands on the right card.
345 - shape card(profile: &Profile, prices: &TierPrices) -> Slot;
175 + /// The split is where the request stops mattering, and on this page that is
176 + /// not the whole region: nine cards each end in a price built from
177 + /// `TierPrices`, which the server holds in memory and can change under a
178 + /// running process. So the residual here is not one literal the way
179 + /// `/policy` and `/team` are. It is the page's markup with nine holes in
180 + /// it, and serving it is writing nine strings into gaps rather than
181 + /// building and rendering a tree of some two hundred nodes.
182 + ///
183 + /// **The card is written here rather than in a `card()` shape**, and that
184 + /// is the copy production deciding it. A card's words are an entry in
185 + /// `content/use-cases.toml`, and a shape takes arguments one at a time: a
186 + /// `card(anchor, title, who, description, ...)` would have to spell each
187 + /// field at the call site and could not pass `features` at all, because a
188 + /// list field is iterated and not written. Unrolled here the loop is gone
189 + /// either way, so the shape bought a name and cost the copy file.
190 + #[staged]
191 + pub(crate) shape page_region(prices: &TierPrices) -> Slot;
346 192
347 - region profile.anchor as Group {
348 - label profile.title;
349 - text profile.who;
350 - text profile.description;
351 - list {
352 - for &feature in profile.features {
353 - row feature;
193 + region PAGE_REGION as Pane {
194 + page "Use Cases";
195 + text "A flat monthly fee. 0% platform cut. Who it's built for:";
196 +
197 + section "Available now";
198 + region PROFILES as Group {
199 + for profile in copy "content/use-cases.toml" as profiles {
200 + // The region is identified by the template's anchor so
201 + // `/use-cases#podcasters` still lands on the right card.
202 + region profile.anchor as Group {
203 + // `section` and not `label`. A region's label is rendered
204 + // only by a container that discloses or steps through its
205 + // members, and `PROFILES` is a plain group, so a `label`
206 + // here reaches no markup at all. The template drew
207 + // `<div class="use-case-title">Musicians</div>`, and every
208 + // one of the nine has been missing from the page since
209 + // `84f0886b` described it; the card's own test asserted the
210 + // anchor, the who and the features, and never the title.
211 + section profile.title;
212 + text profile.who;
213 + text profile.description;
214 + list {
215 + for feature in profile.features {
216 + row feature;
217 + }
218 + }
219 + text super::use_cases::tier_line(profile.priced, prices);
220 + }
354 221 }
355 222 }
356 - text profile.priced.line(prices);
223 +
224 + section "Everyone gets";
225 + region UNIVERSAL as Group {
226 + list {
227 + for gets in copy "content/use-cases.toml" as universal {
228 + row gets.name {
229 + secondary gets.sentence;
230 + }
231 + }
232 + }
233 + }
234 +
235 + act "Join the Alpha" to get "/join" navigating;
236 + for onward in copy "content/use-cases.toml" as onward {
237 + act onward.label to get onward.route navigating;
238 + }
357 239 }
358 240 }
359 241
@@ -401,23 +283,78 @@
401 283 );
402 284 }
403 285
286 + /// The copy the page is built from, read the way the macro reads it.
287 + ///
288 + /// The test asks the content file what the page should say rather than
289 + /// holding a second copy of it, which is `policy`'s rule: a card somebody
290 + /// deletes from the file is a deletion and not a failure, and a card that
291 + /// stops being rendered is a failure.
292 + fn copy() -> toml::Table {
293 + include_str!("../../content/use-cases.toml")
294 + .parse()
295 + .expect("the use-cases copy is TOML")
296 + }
297 +
404 298 /// Every card the template drew is still drawn, and still reachable by the
405 299 /// anchor the marketing links use.
406 300 #[test]
407 301 fn every_profile_keeps_its_card_and_its_anchor() {
408 302 let html = html();
409 303
410 - assert_eq!(PROFILE_LIST.len(), 9, "the template drew nine");
411 - for profile in PROFILE_LIST {
412 - assert!(html.contains(profile.anchor), "{} missing", profile.anchor);
413 - assert!(html.contains(profile.who), "{} missing", profile.who);
414 - for feature in profile.features {
415 - assert!(
416 - html.contains(feature),
417 - "{feature} missing from {}",
418 - profile.title
419 - );
304 + let copy = copy();
305 + let profiles = copy["profiles"].as_array().expect("a list of profiles");
306 + assert_eq!(profiles.len(), 9, "the template drew nine");
307 + for profile in profiles {
308 + let anchor = profile["anchor"].as_str().expect("an anchor");
309 + assert!(html.contains(anchor), "{anchor} missing");
310 + for field in ["who", "title", "description"] {
311 + let text = profile[field].as_str().expect("words");
312 + let escaped = crate::helpers::escape_html(text);
313 + assert!(html.contains(&escaped), "{anchor}: {field} missing");
420 314 }
315 + for feature in profile["features"].as_array().expect("a list") {
316 + let feature = feature.as_str().expect("words");
317 + let escaped = crate::helpers::escape_html(feature);
318 + assert!(html.contains(&escaped), "{feature} missing from {anchor}");
319 + }
320 + }
Lines truncated
@@ -10,3 +10,53 @@
10 10 "<div id=\"policy\" class=\"region pane\"><h1 class=\"heading\">Content Policy</h1><p class=\"text\">Makenotwork exists so creators can sell their work on fair terms. This policy describes what belongs here, what doesn&#39;t, and how we handle problems.</p><h2 class=\"heading\">What&#39;s Welcome</h2><div class=\"rich\" data-disable-scripting><p>Creative work across all supported types:</p>\n<ul>\n<li>Software, plugins, presets, and templates</li>\n<li>Audio: music, podcasts, samples, sound design</li>\n<li>Writing: articles, guides, courses, fiction</li>\n<li>Visual work: images, photography, design assets</li>\n<li>Video: tutorials, performances, documentaries</li>\n</ul>\n<p>If you made it and it's legal to distribute, it belongs here.</p>\n</div><h2 class=\"heading\">What&#39;s Not Allowed</h2><div class=\"rich\" data-disable-scripting><ul>\n<li>Content that violates applicable law</li>\n<li>Harassment, threats, or doxxing</li>\n<li>Spam, deceptive listings, or bait-and-switch pricing</li>\n<li>Malware, exploits, or tools designed to cause harm</li>\n<li>Impersonation of other creators or organizations</li>\n<li>Content you don't have the rights to distribute</li>\n</ul>\n</div><h2 class=\"heading\">How We Handle Issues</h2><div class=\"rich\" data-disable-scripting><p>During private alpha, every creator has a direct relationship with the admin. If something comes up, we talk about it. No automated takedowns, no faceless tickets.</p>\n<p>Post-alpha, we'll introduce a formal process with written notice of any policy violation, an opportunity to appeal, and continued access to data export throughout.</p>\n</div><h2 class=\"heading\">Your Rights</h2><div class=\"rich\" data-disable-scripting><ul>\n<li>Full data export is always available: your content, metadata, and transaction history</li>\n<li>If we ever moderate content or suspend an account, you'll get a clear explanation of what policy was violated</li>\n<li>You'll have the opportunity to appeal</li>\n<li>You can export your data even while suspended (excluding content we can't legally host)</li>\n</ul>\n<p>These commitments are part of our <a href=\"/docs/guarantees\" rel=\"noopener noreferrer\">creator guarantees</a>.</p>\n</div><h2 class=\"heading\">Software Downloads</h2><div class=\"rich\" data-disable-scripting><p>Makenotwork hosts downloadable software uploaded by creators. While we take steps to make sure downloads are safe:</p>\n<ul>\n<li>Creators are responsible for the safety and integrity of their uploads</li>\n<li>Users should verify downloads with antivirus software before running them</li>\n<li>We do not guarantee that any download is free of malware or other harmful content</li>\n<li>Report suspicious downloads to <strong>reports@makenot.work</strong></li>\n</ul>\n</div><h2 class=\"heading\">Other Policies</h2><ul class=\"list\"><li class=\"row\" data-row><span class=\"row-primary\">Terms of Service</span><span class=\"row-secondary\">What you agree to by using Makenotwork</span><span class=\"row-actions\"><a class=\"button\" data-act href=\"/docs/terms-of-service\">Read</a></span></li><li class=\"row\" data-row><span class=\"row-primary\">Privacy Policy</span><span class=\"row-secondary\">What we collect, why, and how to exercise your rights</span><span class=\"row-actions\"><a class=\"button\" data-act href=\"/docs/privacy-policy\">Read</a></span></li><li class=\"row\" data-row><span class=\"row-primary\">Payments &amp; Refunds</span><span class=\"row-secondary\">Merchant-of-record model, refunds, chargebacks</span><span class=\"row-actions\"><a class=\"button\" data-act href=\"/docs/payments\">Read</a></span></li><li class=\"row\" data-row><span class=\"row-primary\">Acceptable Use</span><span class=\"row-secondary\">Specific behaviour that gets accounts suspended</span><span class=\"row-actions\"><a class=\"button\" data-act href=\"/docs/acceptable-use\">Read</a></span></li><li class=\"row\" data-row><span class=\"row-primary\">Copyright &amp; DMCA</span><span class=\"row-secondary\">How takedowns and counter-notifications work</span><span class=\"row-actions\"><a class=\"button\" data-act href=\"/docs/copyright\">Read</a></span></li><li class=\"row\" data-row><span class=\"row-primary\">Appeals</span><span class=\"row-secondary\">How to challenge a moderation decision</span><span class=\"row-actions\"><a class=\"button\" data-act href=\"/docs/appeals\">Read</a></span></li><li class=\"row\" data-row><span class=\"row-primary\">Mailing List Data Processing</span><span class=\"row-secondary\">Who answers a subscriber&#39;s request about a project mailing list</span><span class=\"row-actions\"><a class=\"button\" data-act href=\"/docs/mailing-list-data-processing\">Read</a></span></li></ul><h2 class=\"heading\">Questions</h2><div class=\"rich\" data-disable-scripting><p>If something's unclear or you want to check before posting, reach out at <strong>policy@makenot.work</strong>.</p>\n</div><div class=\"anchored\" id=\"policy-anchored\" data-menu=\"anchored\" hidden></div></div>",
11 11 )),
12 12 ]);
13 +
14 + pub static TEAM: ::quasi_router::stage::Residual = ::quasi_router::stage::Residual::compiled(&[
15 + ::quasi_router::stage::Op::Lit(::std::borrow::Cow::Borrowed(
16 + "<div id=\"team\" class=\"region pane\"><h1 class=\"heading\">Team</h1><p class=\"text\">Small by design. The base platform was designed by the founder, who intends to stay as the technical lead through the project&#39;s growth.</p><h2 class=\"heading\">Founder</h2><ul class=\"list\"><li class=\"row\" data-row><span class=\"row-primary\">Max</span><span class=\"row-secondary\">CEO &amp; Tech Lead</span><span class=\"row-meta\">Designed and built the platform.</span><span class=\"row-actions\"><a class=\"button\" data-act href=\"/u/max\">@max</a></span></li></ul><section id=\"founder-bio\" class=\"region group\" data-showing=\"at-most-one\"><button type=\"button\" class=\"button\" data-shows=\"0\" aria-expanded=\"false\" _=\"on click toggle .current on the first &lt;[data-frame=&#39;founder-bio&#39;]/&gt; then if the first &lt;[data-frame=&#39;founder-bio&#39;]/&gt; matches &#39;.current&#39; set @aria-expanded of me to &#39;true&#39; else set @aria-expanded of me to &#39;false&#39; end\">Bio</button><div class=\"showing-frame\" data-frame=\"founder-bio\" data-shown=\"0\"><div id=\"founder-bio-body\" class=\"region pane\"><p class=\"text\">I started in the creator space as a young kid making backgrounds and montages for CoD MW2 players. I picked up programming around the same time, helping Minecraft server owners with whatever they needed. I spent the years after that focused on my education: a BA in mathematics at the University of Chicago, then a PhD in mathematics at UC San Diego. Afterward I decided to start my own business, and here we are.</p><div class=\"anchored\" id=\"founder-bio-body-anchored\" data-menu=\"anchored\" hidden></div></div></div><div class=\"anchored\" id=\"founder-bio-anchored\" data-menu=\"anchored\" hidden></div></section><div class=\"anchored\" id=\"team-anchored\" data-menu=\"anchored\" hidden></div></div>",
17 + )),
18 + ]);
19 +
20 + pub static USE_CASES: ::quasi_router::stage::Residual = ::quasi_router::stage::Residual::compiled(
21 + &[
22 + ::quasi_router::stage::Op::Lit(::std::borrow::Cow::Borrowed(
23 + "<div id=\"use-cases\" class=\"region pane\"><h1 class=\"heading\">Use Cases</h1><p class=\"text\">A flat monthly fee. 0% platform cut. Who it&#39;s built for:</p><h2 class=\"heading\">Available now</h2><section id=\"use-case-profiles\" class=\"region group\"><section id=\"musicians\" class=\"region group\"><h2 class=\"heading\">Musicians</h2><p class=\"text\">Bands, solo artists, producers</p><p class=\"text\">Sell albums, singles, and EPs with in-browser streaming, chapter markers, and cover art.</p><ul class=\"list\"><li class=\"row\" data-row><span class=\"row-primary\">MP3, FLAC, WAV, OGG, AAC, AIFF upload</span></li><li class=\"row\" data-row><span class=\"row-primary\">In-browser audio player with chapters</span></li><li class=\"row\" data-row><span class=\"row-primary\">Cover art and rich metadata</span></li><li class=\"row\" data-row><span class=\"row-primary\">RSS feed per project</span></li><li class=\"row\" data-row><span class=\"row-primary\">Pay-what-you-want pricing</span></li></ul><p class=\"text\">",
24 + )),
25 + ::quasi_router::stage::Op::Hole { scope: 0, id: 0 },
26 + ::quasi_router::stage::Op::Lit(::std::borrow::Cow::Borrowed(
27 + "</p><div class=\"anchored\" id=\"musicians-anchored\" data-menu=\"anchored\" hidden></div></section><section id=\"podcasters\" class=\"region group\"><h2 class=\"heading\">Podcasters</h2><p class=\"text\">Independent shows, networks</p><p class=\"text\">Host and distribute your podcast with full RSS support for every major directory.</p><ul class=\"list\"><li class=\"row\" data-row><span class=\"row-primary\">RSS for Apple, Spotify, Pocket Casts</span></li><li class=\"row\" data-row><span class=\"row-primary\">Chapter markers</span></li><li class=\"row\" data-row><span class=\"row-primary\">Subscriber-only feeds</span></li><li class=\"row\" data-row><span class=\"row-primary\">Broadcast emails to followers</span></li></ul><p class=\"text\">",
28 + )),
29 + ::quasi_router::stage::Op::Hole { scope: 0, id: 1 },
30 + ::quasi_router::stage::Op::Lit(::std::borrow::Cow::Borrowed(
31 + "</p><div class=\"anchored\" id=\"podcasters-anchored\" data-menu=\"anchored\" hidden></div></section><section id=\"writers\" class=\"region group\"><h2 class=\"heading\">Writers</h2><p class=\"text\">Bloggers, newsletter writers, authors</p><p class=\"text\">Publish long-form writing with Markdown, per-project blogs, and subscriber broadcasts.</p><ul class=\"list\"><li class=\"row\" data-row><span class=\"row-primary\">Markdown editor with formatting</span></li><li class=\"row\" data-row><span class=\"row-primary\">Per-project blog</span></li><li class=\"row\" data-row><span class=\"row-primary\">RSS feed</span></li><li class=\"row\" data-row><span class=\"row-primary\">Scheduled publishing</span></li><li class=\"row\" data-row><span class=\"row-primary\">Broadcast emails</span></li></ul><p class=\"text\">",
32 + )),
33 + ::quasi_router::stage::Op::Hole { scope: 0, id: 2 },
34 + ::quasi_router::stage::Op::Lit(::std::borrow::Cow::Borrowed(
35 + "</p><div class=\"anchored\" id=\"writers-anchored\" data-menu=\"anchored\" hidden></div></section><section id=\"developers\" class=\"region group\"><h2 class=\"heading\">Software Developers</h2><p class=\"text\">Indie devs, tool makers, plugin authors</p><p class=\"text\">Distribute software with versioned releases, license keys, and a built-in git browser.</p><ul class=\"list\"><li class=\"row\" data-row><span class=\"row-primary\">Versioned releases with changelogs</span></li><li class=\"row\" data-row><span class=\"row-primary\">License keys with activation tracking</span></li><li class=\"row\" data-row><span class=\"row-primary\">Git source browser</span></li><li class=\"row\" data-row><span class=\"row-primary\">Promo codes and discounts</span></li></ul><p class=\"text\">",
36 + )),
37 + ::quasi_router::stage::Op::Hole { scope: 0, id: 3 },
38 + ::quasi_router::stage::Op::Lit(::std::borrow::Cow::Borrowed(
39 + "</p><div class=\"anchored\" id=\"developers-anchored\" data-menu=\"anchored\" hidden></div></section><section id=\"sample-packs\" class=\"region group\"><h2 class=\"heading\">Sample Packs</h2><p class=\"text\">Sound designers, beat makers, foley artists</p><p class=\"text\">Sell sample packs and sound effects with audio previews and versioned downloads.</p><ul class=\"list\"><li class=\"row\" data-row><span class=\"row-primary\">Audio previews in-browser</span></li><li class=\"row\" data-row><span class=\"row-primary\">Versioned downloads</span></li><li class=\"row\" data-row><span class=\"row-primary\">Hierarchical tags for organization</span></li><li class=\"row\" data-row><span class=\"row-primary\">Pay-what-you-want pricing</span></li><li class=\"row\" data-row><span class=\"row-primary\">Cover art</span></li></ul><p class=\"text\">",
40 + )),
41 + ::quasi_router::stage::Op::Hole { scope: 0, id: 4 },
42 + ::quasi_router::stage::Op::Lit(::std::borrow::Cow::Borrowed(
43 + "</p><div class=\"anchored\" id=\"sample-packs-anchored\" data-menu=\"anchored\" hidden></div></section><section id=\"digital-art\" class=\"region group\"><h2 class=\"heading\">Digital Art &amp; Photography</h2><p class=\"text\">Illustrators, photographers, font designers</p><p class=\"text\">Sell digital art, photography, and fonts with versioned releases and subscription tiers.</p><ul class=\"list\"><li class=\"row\" data-row><span class=\"row-primary\">Any file type supported</span></li><li class=\"row\" data-row><span class=\"row-primary\">Versioned releases</span></li><li class=\"row\" data-row><span class=\"row-primary\">Subscription tiers</span></li><li class=\"row\" data-row><span class=\"row-primary\">Cover images</span></li></ul><p class=\"text\">",
44 + )),
45 + ::quasi_router::stage::Op::Hole { scope: 0, id: 5 },
46 + ::quasi_router::stage::Op::Lit(::std::borrow::Cow::Borrowed(
47 + "</p><div class=\"anchored\" id=\"digital-art-anchored\" data-menu=\"anchored\" hidden></div></section><section id=\"educators\" class=\"region group\"><h2 class=\"heading\">Educators</h2><p class=\"text\">Course creators, tutors, workshop leaders</p><p class=\"text\">Publish courses and teaching materials with text content, downloads, and subscriptions.</p><ul class=\"list\"><li class=\"row\" data-row><span class=\"row-primary\">Text content with Markdown</span></li><li class=\"row\" data-row><span class=\"row-primary\">Downloadable materials</span></li><li class=\"row\" data-row><span class=\"row-primary\">Subscription tiers</span></li><li class=\"row\" data-row><span class=\"row-primary\">Scheduled publishing</span></li></ul><p class=\"text\">",
48 + )),
49 + ::quasi_router::stage::Op::Hole { scope: 0, id: 6 },
50 + ::quasi_router::stage::Op::Lit(::std::borrow::Cow::Borrowed(
51 + "</p><div class=\"anchored\" id=\"educators-anchored\" data-menu=\"anchored\" hidden></div></section><section id=\"game-devs\" class=\"region group\"><h2 class=\"heading\">Game Developers</h2><p class=\"text\">Indie games, mods, TTRPG content</p><p class=\"text\">Distribute games up to 20GB per file with versioned downloads, license keys, and promo codes. Need more than 20GB? Request a size increase from your dashboard.</p><ul class=\"list\"><li class=\"row\" data-row><span class=\"row-primary\">Up to 20GB per file (increase on request)</span></li><li class=\"row\" data-row><span class=\"row-primary\">Versioned downloads with changelogs</span></li><li class=\"row\" data-row><span class=\"row-primary\">License keys with activation limits</span></li><li class=\"row\" data-row><span class=\"row-primary\">Promo codes</span></li></ul><p class=\"text\">",
52 + )),
53 + ::quasi_router::stage::Op::Hole { scope: 0, id: 7 },
54 + ::quasi_router::stage::Op::Lit(::std::borrow::Cow::Borrowed(
55 + "</p><div class=\"anchored\" id=\"game-devs-anchored\" data-menu=\"anchored\" hidden></div></section><section id=\"comic-creators\" class=\"region group\"><h2 class=\"heading\">Comic Creators</h2><p class=\"text\">Webcomic artists, graphic novelists</p><p class=\"text\">Sell digital comics and graphic novels with project organization and scheduled publishing.</p><ul class=\"list\"><li class=\"row\" data-row><span class=\"row-primary\">PDF, CBZ, and any file type</span></li><li class=\"row\" data-row><span class=\"row-primary\">Project organization</span></li><li class=\"row\" data-row><span class=\"row-primary\">Subscriptions</span></li><li class=\"row\" data-row><span class=\"row-primary\">Scheduled publishing</span></li></ul><p class=\"text\">",
56 + )),
57 + ::quasi_router::stage::Op::Hole { scope: 0, id: 8 },
58 + ::quasi_router::stage::Op::Lit(::std::borrow::Cow::Borrowed(
59 + "</p><div class=\"anchored\" id=\"comic-creators-anchored\" data-menu=\"anchored\" hidden></div></section><div class=\"anchored\" id=\"use-case-profiles-anchored\" data-menu=\"anchored\" hidden></div></section><h2 class=\"heading\">Everyone gets</h2><section id=\"everyone-gets\" class=\"region group\"><ul class=\"list\"><li class=\"row\" data-row><span class=\"row-primary\">0% platform fee</span><span class=\"row-secondary\">Only ~3% payment processing. No percentage cuts, no revenue sharing.</span></li><li class=\"row\" data-row><span class=\"row-primary\">Full data export</span><span class=\"row-secondary\">Download everything anytime. Projects, items, sales, contacts: all yours.</span></li><li class=\"row\" data-row><span class=\"row-primary\">No lock-in</span><span class=\"row-secondary\">Month-to-month. Cancel anytime. Take your audience with you.</span></li><li class=\"row\" data-row><span class=\"row-primary\">Source available</span><span class=\"row-secondary\">Read the codebase. Verify every claim about privacy and data handling.</span></li></ul><div class=\"anchored\" id=\"everyone-gets-anchored\" data-menu=\"anchored\" hidden></div></section><a class=\"button\" data-act href=\"/join\">Join the Alpha</a><a class=\"button\" data-act href=\"/pricing\">Pricing Calculator</a><a class=\"button\" data-act href=\"/discover\">Browse as guest</a><a class=\"button\" data-act href=\"/docs/getting-started\">Creator Guide</a><div class=\"anchored\" id=\"use-cases-anchored\" data-menu=\"anchored\" hidden></div></div>",
60 + )),
61 + ],
62 + );
@@ -1,0 +1,14 @@
1 + # The copy at `/team`, which is words and nothing else.
2 + #
3 + # Read at build time by the `declare!` in `src/quasi/team.rs`. Editing this file
4 + # is editing the page: no Rust to read, no code to touch, and a rebuild picks it
5 + # up. See `content/policy.toml` for the split this follows.
6 + #
7 + # The bio is prose the founder wrote, so nothing here rewrites it. It lives in
8 + # TOML rather than in a Rust `const` for the reason `policy.toml` records: a
9 + # `const` is a path and not a literal, so an `include` of one opens a staged
10 + # scope and the sentence gets built and rendered on every request. Copy the
11 + # macro can read is copy the macro can fold.
12 +
13 + [[bio]]
14 + body = "I started in the creator space as a young kid making backgrounds and montages for CoD MW2 players. I picked up programming around the same time, helping Minecraft server owners with whatever they needed. I spent the years after that focused on my education: a BA in mathematics at the University of Chicago, then a PhD in mathematics at UC San Diego. Afterward I decided to start my own business, and here we are."
@@ -1,0 +1,162 @@
1 + # The copy at `/use-cases`, which is words and nothing else.
2 + #
3 + # Read at build time by the `declare!` in `src/quasi/use_cases.rs`. Editing this
4 + # file is editing the page. See `content/policy.toml` for the split this follows.
5 + #
6 + # `priced` names which tier line a card ends with, and that is the one thing on
7 + # this page a request decides: the numbers come from `TierPrices` in memory, so
8 + # the card's last line is a hole and everything above it is a literal. The word
9 + # here is a shape name and not a price, which is what keeps a stale number from
10 + # ever being written down.
11 +
12 + [[profiles]]
13 + anchor = "musicians"
14 + title = "Musicians"
15 + who = "Bands, solo artists, producers"
16 + description = "Sell albums, singles, and EPs with in-browser streaming, chapter markers, and cover art."
17 + features = [
18 + "MP3, FLAC, WAV, OGG, AAC, AIFF upload",
19 + "In-browser audio player with chapters",
20 + "Cover art and rich metadata",
21 + "RSS feed per project",
22 + "Pay-what-you-want pricing",
23 + ]
24 + priced = "small-files"
25 +
26 + [[profiles]]
27 + anchor = "podcasters"
28 + title = "Podcasters"
29 + who = "Independent shows, networks"
30 + description = "Host and distribute your podcast with full RSS support for every major directory."
31 + features = [
32 + "RSS for Apple, Spotify, Pocket Casts",
33 + "Chapter markers",
34 + "Subscriber-only feeds",
35 + "Broadcast emails to followers",
36 + ]
37 + priced = "small-files"
38 +
39 + [[profiles]]
40 + anchor = "writers"
41 + title = "Writers"
42 + who = "Bloggers, newsletter writers, authors"
43 + description = "Publish long-form writing with Markdown, per-project blogs, and subscriber broadcasts."
44 + features = [
45 + "Markdown editor with formatting",
46 + "Per-project blog",
47 + "RSS feed",
48 + "Scheduled publishing",
49 + "Broadcast emails",
50 + ]
51 + priced = "basic"
52 +
53 + [[profiles]]
54 + anchor = "developers"
55 + title = "Software Developers"
56 + who = "Indie devs, tool makers, plugin authors"
57 + description = "Distribute software with versioned releases, license keys, and a built-in git browser."
58 + features = [
59 + "Versioned releases with changelogs",
60 + "License keys with activation tracking",
61 + "Git source browser",
62 + "Promo codes and discounts",
63 + ]
64 + priced = "small-files"
65 +
66 + [[profiles]]
67 + anchor = "sample-packs"
68 + title = "Sample Packs"
69 + who = "Sound designers, beat makers, foley artists"
70 + description = "Sell sample packs and sound effects with audio previews and versioned downloads."
71 + features = [
72 + "Audio previews in-browser",
73 + "Versioned downloads",
74 + "Hierarchical tags for organization",
75 + "Pay-what-you-want pricing",
76 + "Cover art",
77 + ]
78 + priced = "small-files"
79 +
80 + [[profiles]]
81 + anchor = "digital-art"
82 + title = "Digital Art & Photography"
83 + who = "Illustrators, photographers, font designers"
84 + description = "Sell digital art, photography, and fonts with versioned releases and subscription tiers."
85 + features = [
86 + "Any file type supported",
87 + "Versioned releases",
88 + "Subscription tiers",
89 + "Cover images",
90 + ]
91 + priced = "small-files"
92 +
93 + [[profiles]]
94 + anchor = "educators"
95 + title = "Educators"
96 + who = "Course creators, tutors, workshop leaders"
97 + description = "Publish courses and teaching materials with text content, downloads, and subscriptions."
98 + features = [
99 + "Text content with Markdown",
100 + "Downloadable materials",
101 + "Subscription tiers",
102 + "Scheduled publishing",
103 + ]
104 + priced = "basic-or-small-files"
105 +
106 + [[profiles]]
107 + anchor = "game-devs"
108 + title = "Game Developers"
109 + who = "Indie games, mods, TTRPG content"
110 + description = "Distribute games up to 20GB per file with versioned downloads, license keys, and promo codes. Need more than 20GB? Request a size increase from your dashboard."
111 + features = [
112 + "Up to 20GB per file (increase on request)",
113 + "Versioned downloads with changelogs",
114 + "License keys with activation limits",
115 + "Promo codes",
116 + ]
117 + priced = "big-files"
118 +
119 + [[profiles]]
120 + anchor = "comic-creators"
121 + title = "Comic Creators"
122 + who = "Webcomic artists, graphic novelists"
123 + description = "Sell digital comics and graphic novels with project organization and scheduled publishing."
124 + features = [
125 + "PDF, CBZ, and any file type",
126 + "Project organization",
127 + "Subscriptions",
128 + "Scheduled publishing",
129 + ]
130 + priced = "small-files"
131 +
132 + # What every tier includes, in the order the page shows them.
133 +
134 + [[universal]]
135 + name = "0% platform fee"
136 + sentence = "Only ~3% payment processing. No percentage cuts, no revenue sharing."
137 +
138 + [[universal]]
139 + name = "Full data export"
140 + sentence = "Download everything anytime. Projects, items, sales, contacts: all yours."
141 +
142 + [[universal]]
143 + name = "No lock-in"
144 + sentence = "Month-to-month. Cancel anytime. Take your audience with you."
145 +
146 + [[universal]]
147 + name = "Source available"
148 + sentence = "Read the codebase. Verify every claim about privacy and data handling."
149 +
150 + # Where the page sends a reader who is convinced, and one who is not yet.
151 +
152 + [[onward]]
153 + label = "Pricing Calculator"
154 + route = "/pricing"
155 +
156 + [[onward]]
157 + label = "Browse as guest"
158 + route = "/discover"
159 +
160 + [[onward]]
161 + label = "Creator Guide"
162 + route = "/docs/getting-started"