Skip to main content

max / makenotwork

Put the creators page on the residual seam Thirteen of seventeen. Almost all of /creators is prose this repository wrote, so almost all of it folds into one literal; what is left varying is the creator count, the four tier rows' two figures each, and which of the three calls to action this reader is shown. The tier table was a `const TIERS: &[Tier]` whose rows carried two `fn(&TierPrices)` pointers. A `const` is a path, so the loop over it opened a scope per request and the four rows were rebuilt on every load. It moves to content/creators.toml and is unrolled at macro time, which folds the names and what each tier is for into the residual's literals and leaves eight holes. The trade is `/use-cases`' exactly: a proc macro cannot evaluate a path, so the file names a tier as a `&str` and two functions match it, with a test holding the file to the four names. Three sites take `include each` rather than spreading a `Vec<Node>` through a loop: /creators' call to action, the feed's body and the analytics range chips. That is the same splice said as a splice, and the first two are staged shapes that could not have said it any other way. The residual test now fills with stated prices rather than `TierPrices::default()`, and the reason is a rule worth having: a cell holding one piece of text says so with `cell-value`, and an empty string is no content at all rather than shorter content, so an empty cell and a filled one are two shapes and a residual holds one. **A hole that can be empty is guarded** -- which is what the converted screens already do. Every envelope is read from the assumptions file at boot, so nothing serving this page carries an empty one.
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-08 03:07 UTC
Commit: 68c4985e68aaa8962f3f2c2c21ab9fa275dde5fe
Parent: 293be7a
7 files changed, +365 insertions, -124 deletions
@@ -53,66 +53,56 @@
53 53
54 54 const MEASURE: layout::Measure = layout::Measure::Wide;
55 55
56 - /// One row of the tier table.
56 + /// What one tier's monthly fee says, by the name the copy gives it.
57 57 ///
58 - /// The two figures are read off [`TierPrices`] rather than written here, for
59 - /// the reason `/use-cases` gives: a formatted price in a table is a price that
60 - /// goes stale on its own.
58 + /// The tier table was a `const TIERS: &[Tier]` here, whose rows carried two
59 + /// `fn(&TierPrices)` pointers. A `const` is a path, so a loop over it opens a
60 + /// scope per request and the four rows were rebuilt on every load; read out of
61 + /// `content/creators.toml` instead they are unrolled at macro time and fold
62 + /// into the residual's literals, with only the two figures left as holes.
61 63 ///
62 - /// [`price`](Self::price) and [`storage`](Self::storage) are what the table
63 - /// reads, and the two fields behind them are what those read. A description
64 - /// names what it draws, and `(tier.price)(prices)` is a call through a field
65 - /// rather than a name -- the same ruling policy's `OTHER` got when it was a
66 - /// slice of tuples.
67 - struct Tier {
68 - name: &'static str,
69 - best_for: &'static str,
70 - monthly: fn(&TierPrices) -> i32,
71 - total: fn(&TierPrices) -> String,
64 + /// What it costs is exhaustiveness, which is `/use-cases`' trade exactly: a
65 + /// proc macro cannot evaluate a path, so the file names a tier as a `&str` and
66 + /// this matches it. [`tests::every_priced_name_in_the_copy_is_one_of_the_four`]
67 + /// is what buys it back.
68 + ///
69 + /// # Panics
70 + ///
71 + /// On a name this does not know, which is a content file naming a tier that
72 + /// does not exist. The test above makes that a test failure rather than a page
73 + /// that renders a blank column.
74 + fn tier_price(priced: &str, prices: &TierPrices) -> String {
75 + let monthly = match priced {
76 + "basic" => prices.basic_std,
77 + "small-files" => prices.small_files_std,
78 + "big-files" => prices.big_files_std,
79 + "everything" => prices.everything_std,
80 + other => panic!("content/creators.toml names a tier that does not exist: {other}"),
81 + };
82 + format!("${monthly}")
72 83 }
73 84
74 - impl Tier {
75 - /// The monthly fee, formatted as the table shows it.
76 - fn price(&self, prices: &TierPrices) -> String {
77 - format!("${}", (self.monthly)(prices))
78 - }
79 -
80 - /// The storage envelope this tier buys.
81 - fn storage(&self, prices: &TierPrices) -> String {
82 - (self.total)(prices)
85 + /// The storage envelope one tier buys. See [`tier_price`].
86 + ///
87 + /// # Panics
88 + ///
89 + /// On a name this does not know, for the same reason.
90 + fn tier_storage(priced: &str, prices: &TierPrices) -> String {
91 + match priced {
92 + "basic" => prices.basic_total.clone(),
93 + "small-files" => prices.small_files_total.clone(),
94 + "big-files" => prices.big_files_total.clone(),
95 + "everything" => prices.everything_total.clone(),
96 + other => panic!("content/creators.toml names a tier that does not exist: {other}"),
83 97 }
84 98 }
85 99
86 - /// The four, in the order the shipped table listed them.
87 - const TIERS: &[Tier] = &[
88 - Tier {
89 - name: "Basic",
90 - best_for: "Text, blogs, newsletters",
91 - monthly: |p| p.basic_std,
92 - total: |p| p.basic_total.clone(),
93 - },
94 - Tier {
95 - name: "Small Files",
96 - best_for: "Audio, plugins, small software",
97 - monthly: |p| p.small_files_std,
98 - total: |p| p.small_files_total.clone(),
99 - },
100 - Tier {
101 - name: "Big Files",
102 - best_for: "Video, games, large software",
103 - monthly: |p| p.big_files_std,
104 - total: |p| p.big_files_total.clone(),
105 - },
106 - Tier {
107 - name: "Everything",
108 - best_for: "All features, current and future",
109 - monthly: |p| p.everything_std,
110 - total: |p| p.everything_total.clone(),
111 - },
112 - ];
100 + /// Every name the two above answer to, which is what the copy is held to.
101 + #[cfg(test)]
102 + const TIER_NAMES: &[&str] = &["basic", "small-files", "big-files", "everything"];
113 103
114 104 /// What this request knows about the reader's standing.
115 - enum Standing {
105 + pub(crate) enum Standing {
116 106 /// Nobody signed in.
117 107 Visitor,
118 108 /// Signed in, not yet a creator.
@@ -143,7 +133,11 @@
143 133 }
144 134
145 135 /// The page.
146 - pub fn screen(viewer: &super::Viewer, _request: Request) -> Result<Response, RouteError> {
136 + /// The three things this page reads, for the mount that serves it from a
137 + /// residual.
138 + ///
139 + /// One read, stating the document and filling the holes.
140 + pub(crate) fn reading(viewer: &super::Viewer) -> Result<(Standing, i64, TierPrices), RouteError> {
147 141 use axum::extract::FromRef as _;
148 142
149 143 let total_creators = viewer
@@ -158,12 +152,22 @@
158 152
159 153 let billing = crate::Billing::from_ref(&viewer.app);
160 154
161 - Ok(page_screen(&standing, total_creators, &billing.tier_prices).into())
155 + Ok((standing, total_creators, billing.tier_prices))
156 + }
157 +
158 + pub fn screen(viewer: &super::Viewer, _request: Request) -> Result<Response, RouteError> {
159 + let (standing, total_creators, prices) = reading(viewer)?;
160 +
161 + Ok(page_screen(&standing, total_creators, &prices).into())
162 162 }
163 163
164 164 declare! {
165 165 /// The whole document: the title, the measure, the body.
166 - shape page_screen(standing: &Standing, total_creators: i64, prices: &TierPrices) -> Screen;
166 + pub(crate) shape page_screen(
167 + standing: &Standing,
168 + total_creators: i64,
169 + prices: &TierPrices,
170 + ) -> Screen;
167 171
168 172 screen single "Creators - Makenotwork" {
169 173 measured MEASURE;
@@ -171,65 +175,80 @@
171 175 summarised "Apply for creator access: a flat monthly fee, no cut of your revenue, and four \
172 176 tiers that pick a file-size envelope rather than a feature set.";
173 177
174 - region PAGE_REGION as Pane {
175 - page "Become a Creator";
176 - text "Anyone can sign up to browse and buy. To create projects and sell your work, \
177 - apply for creator access. Most applications are approved within a few days. \
178 - Makenotwork is in private alpha; we're approving applications one cohort at a \
179 - time.";
178 + include page_region(standing, total_creators, prices);
179 + }
180 + }
180 181
181 - section "How It Works";
182 - include super::own_prose(
183 - "1. **Sign up** and verify your email\n\
184 - 2. **Apply** from your dashboard: tell us what you make and which tier fits\n\
185 - 3. **Get approved**: we review applications individually, usually within a few days\n\
186 - \n\
187 - We review applications to make sure applicants are here to share and sell creative \
188 - work. If you make something and want to sell it, you'll likely get in. Link to your \
189 - existing work (a portfolio, channel, or profile elsewhere) to speed things up.\n\
190 - \n\
191 - **Important:** You sell in the currency your Stripe account settles in, and \
192 - receiving payouts requires a [Stripe](https://stripe.com/global) account in a \
193 - supported country that settles in one of the six we support: **USD, CAD, GBP, AUD, \
194 - NZD or EUR**. Check both with Stripe before applying."
195 - );
182 + declare! {
183 + /// The page's one region, split out so it can be staged.
184 + ///
185 + /// Almost all of it is prose this repository wrote, so almost all of it
186 + /// folds into one literal. What is left varying is the creator count, the
187 + /// four tier rows' two figures each, and which of the three calls to action
188 + /// this reader is shown.
189 + #[staged]
190 + pub(crate) shape page_region(
191 + standing: &Standing,
192 + total_creators: i64,
193 + prices: &TierPrices,
194 + ) -> Slot;
196 195
197 - stats [Figure::new(total_creators.to_string(), "Active Creators")];
196 + region PAGE_REGION as Pane {
197 + page "Become a Creator";
198 + text "Anyone can sign up to browse and buy. To create projects and sell your work, \
199 + apply for creator access. Most applications are approved within a few days. \
200 + Makenotwork is in private alpha; we're approving applications one cohort at a \
201 + time.";
198 202
199 - section "Pricing";
200 - text "Flat monthly fee. 0% cut of your revenue. The only deduction from fan payments \
201 - is the payment processor's fee (~3%).";
202 - include tier_table(prices);
203 - include super::own_prose(
204 - "Every tier is the complete platform: `/u/username` profile, project and item pages, \
205 - project forum, Discover listing, memberships, pay-what-you-want, promo codes, RSS, \
206 - analytics, full data export, 2FA/passkeys. The tier picks the file-size envelope, \
207 - not the feature set. You sell in your Stripe account's currency (USD, CAD, GBP, AUD, \
208 - NZD or EUR); receiving payouts requires [Stripe](https://stripe.com/global) in a \
209 - supported country. [Full tier details](/docs/tiers) | \
210 - [Pricing models](/docs/pricing)"
211 - );
212 - include super::own_prose(
213 - "**Not ready to commit?** Request a **free trial** (2-6 weeks, no credit card) when \
214 - you apply. Or [try sandbox mode](/sandbox) to explore the dashboard without signing \
215 - up."
216 - );
203 + section "How It Works";
204 + include super::own_prose(
205 + "1. **Sign up** and verify your email\n\
206 + 2. **Apply** from your dashboard: tell us what you make and which tier fits\n\
207 + 3. **Get approved**: we review applications individually, usually within a few days\n\
208 + \n\
209 + We review applications to make sure applicants are here to share and sell creative \
210 + work. If you make something and want to sell it, you'll likely get in. Link to your \
211 + existing work (a portfolio, channel, or profile elsewhere) to speed things up.\n\
212 + \n\
213 + **Important:** You sell in the currency your Stripe account settles in, and \
214 + receiving payouts requires a [Stripe](https://stripe.com/global) account in a \
215 + supported country that settles in one of the six we support: **USD, CAD, GBP, AUD, \
216 + NZD or EUR**. Check both with Stripe before applying."
217 + );
217 218
218 - section "Who Runs This";
219 - include super::own_prose(
220 - "Makenotwork is built and operated by one person. No investors, no board, no outside \
221 - pressure. Decisions are fast and aligned with creators, but there's no large team \
222 - behind the scenes. Read the full picture in our \
223 - [continuity guarantee](/docs/guarantees#continuity) and \
224 - [platform economics](/docs/economics)."
225 - );
219 + stats [Figure::new(total_creators.to_string(), "Active Creators")];
226 220
227 - // The one part of the page that differs by who is asking, spread
228 - // where it was appended. `feeds` does the same with its body.
229 - for node in call_to_action(standing) {
230 - include node;
231 - }
232 - }
221 + section "Pricing";
222 + text "Flat monthly fee. 0% cut of your revenue. The only deduction from fan payments \
223 + is the payment processor's fee (~3%).";
224 + include tier_table(prices);
225 + include super::own_prose(
226 + "Every tier is the complete platform: `/u/username` profile, project and item pages, \
227 + project forum, Discover listing, memberships, pay-what-you-want, promo codes, RSS, \
228 + analytics, full data export, 2FA/passkeys. The tier picks the file-size envelope, \
229 + not the feature set. You sell in your Stripe account's currency (USD, CAD, GBP, AUD, \
230 + NZD or EUR); receiving payouts requires [Stripe](https://stripe.com/global) in a \
231 + supported country. [Full tier details](/docs/tiers) | \
232 + [Pricing models](/docs/pricing)"
233 + );
234 + include super::own_prose(
235 + "**Not ready to commit?** Request a **free trial** (2-6 weeks, no credit card) when \
236 + you apply. Or [try sandbox mode](/sandbox) to explore the dashboard without signing \
237 + up."
238 + );
239 +
240 + section "Who Runs This";
241 + include super::own_prose(
242 + "Makenotwork is built and operated by one person. No investors, no board, no outside \
243 + pressure. Decisions are fast and aligned with creators, but there's no large team \
244 + behind the scenes. Read the full picture in our \
245 + [continuity guarantee](/docs/guarantees#continuity) and \
246 + [platform economics](/docs/economics)."
247 + );
248 +
249 + // The one part of the page that differs by who is asking, spread
250 + // where it was appended. `feeds` does the same with its body.
251 + include each call_to_action(standing);
233 252 }
234 253 }
235 254
@@ -239,6 +258,7 @@
239 258 /// Four columns and four cells, written together, with no branch between
240 259 /// them: every tier is a full row, so position is checkable by eye here and
241 260 /// naming the columns would be ceremony.
261 + #[staged]
242 262 shape tier_table(prices: &TierPrices) -> Node;
243 263
244 264 table {
@@ -256,12 +276,12 @@
256 276 width Content;
257 277 }
258 278
259 - for tier in TIERS {
279 + for tier in copy "content/creators.toml" as tiers {
260 280 cells {
261 281 cell tier.name;
262 - cell tier.price(prices);
282 + cell tier_price(tier.priced, prices);
263 283 cell tier.best_for;
264 - cell tier.storage(prices);
284 + cell tier_storage(tier.priced, prices);
265 285 }
266 286 }
267 287 }
@@ -275,6 +295,7 @@
275 295 /// a different number of things -- a visitor two controls, the other two one
276 296 /// each -- so the answer is a run of members and not one node. The three
277 297 /// guards are exhaustive and disjoint by construction.
298 + #[staged]
278 299 shape call_to_action(standing: &Standing) -> Vec<Node>;
279 300
280 301 text "You have creator access." when standing.is_creator();
@@ -342,16 +363,49 @@
342 363 Webview::new().screen(&page_screen(&Standing::Visitor, 0, &prices))
343 364 };
344 365
345 - assert_eq!(TIERS.len(), 4);
346 - for tier in TIERS {
347 - assert!(html.contains(tier.name), "{} missing", tier.name);
348 - assert!(html.contains(tier.best_for), "{} missing", tier.best_for);
366 + let tiers = copy_tiers();
367 + assert_eq!(tiers.len(), 4);
368 + for tier in &tiers {
369 + for said in ["name", "best_for"] {
370 + let words = tier[said].as_str().expect("a string");
371 + assert!(html.contains(words), "{words} missing");
372 + }
349 373 }
350 374 for price in ["4321", "5678", "8765", "9876"] {
351 375 assert!(html.contains(price), "{price} is not read from TierPrices");
352 376 }
353 377 }
354 378
379 + /// The tier rows, read the way the macro reads them.
380 + fn copy_tiers() -> Vec<toml::Table> {
381 + let copy: toml::Table = include_str!("../../content/creators.toml")
382 + .parse()
383 + .expect("the creators copy is TOML");
384 +
385 + copy["tiers"]
386 + .as_array()
387 + .expect("a list of tiers")
388 + .iter()
389 + .map(|tier| tier.as_table().expect("a table").clone())
390 + .collect()
391 + }
392 +
393 + /// Every tier the copy names is one the two pricing functions answer to.
394 + ///
395 + /// What buys back the exhaustiveness the copy move cost. `tier_price`
396 + /// panics on a name it does not know, and this is what makes that a test
397 + /// failure rather than a page that renders a blank column.
398 + #[test]
399 + fn every_priced_name_in_the_copy_is_one_of_the_four() {
400 + for tier in copy_tiers() {
401 + let priced = tier["priced"].as_str().expect("a string");
402 + assert!(
403 + TIER_NAMES.contains(&priced),
404 + "{priced} is not a tier the price functions answer to",
405 + );
406 + }
407 + }
408 +
355 409 /// The live disclosure: how many creators are actually here.
356 410 #[test]
357 411 fn the_active_creator_count_is_shown() {
@@ -272,9 +272,7 @@
272 272
273 273 region PAGE_REGION as Pane {
274 274 page "Your Feed";
275 - for node in body(page, Surface::Page) {
276 - include node;
277 - }
275 + include each body(page, Surface::Page);
278 276 }
279 277 }
280 278 }
@@ -774,7 +774,21 @@
774 774 ),
775 775 (
776 776 creators::PATH,
777 - public_document_mount(app, creators::PATH, creators::screen, creators::renderer),
777 + served_document_mount(app, creators::PATH, creators::renderer, |viewer, _| {
778 + // One read of the standing, the count and the prices, stating
779 + // the document and filling the holes.
780 + let (standing, total_creators, prices) = creators::reading(viewer)?;
781 + Ok(Served {
782 + screen: creators::page_screen(&standing, total_creators, &prices),
783 + markup: creators::page_region_serve(
784 + &residuals::CREATORS,
785 + &standing,
786 + total_creators,
787 + &prices,
788 + )
789 + .into(),
790 + })
791 + }),
778 792 ),
779 793 (
780 794 collections::PATH,
@@ -98,6 +98,9 @@
98 98 ("PAYOUT_SUMMARY", |plan| {
99 99 super::payout_summary::card_staged(plan)
100 100 }),
101 + ("CREATORS", |plan| {
102 + Node::Region(super::creators::page_region_staged(plan))
103 + }),
101 104 ]
102 105 }
103 106
@@ -344,11 +347,43 @@
344 347 }
345 348
346 349 /// Prices no default and no assumptions file would produce.
350 + ///
351 + /// The storage envelopes are set as well as the fees, and `/creators` is
352 + /// why. A cell holding one piece of text says so on its container with
353 + /// `cell-value`, and an empty string is not one piece of text but no
354 + /// content at all -- so an empty cell and a filled one are two shapes, and
355 + /// a residual holds one. `TierPrices::default()` leaves every envelope
356 + /// empty; nothing serving this page does, because they are read from the
357 + /// assumptions file at boot and the validator refuses a missing one.
358 + ///
359 + /// The rule that generalises, and it is the one the converted screens
360 + /// already follow: **a hole that can be empty is guarded**, because empty
361 + /// is a different shape rather than a shorter value. `/c/{username}/{slug}`
362 + /// says `unless loaded.description().is_empty()` for exactly this reason.
347 363 fn odd_prices() -> crate::tier_prices::TierPrices {
348 364 crate::tier_prices::TierPrices {
349 365 basic_std: 4321,
350 366 small_files_std: 5678,
351 367 big_files_std: 8765,
368 + everything_std: 9876,
369 + basic_total: "3GB".to_owned(),
370 + small_files_total: "40GB".to_owned(),
371 + big_files_total: "700GB".to_owned(),
372 + everything_total: "9TB".to_owned(),
373 + ..Default::default()
374 + }
375 + }
376 +
377 + /// The envelopes a served page actually carries.
378 + ///
379 + /// `TierPrices::default()` is a test artefact: every envelope is the empty
380 + /// string, which no request produces. See [`odd_prices`].
381 + fn stated_prices() -> crate::tier_prices::TierPrices {
382 + crate::tier_prices::TierPrices {
383 + basic_total: "1GB".to_owned(),
384 + small_files_total: "20GB".to_owned(),
385 + big_files_total: "500GB".to_owned(),
386 + everything_total: "5TB".to_owned(),
352 387 ..Default::default()
353 388 }
354 389 }
@@ -699,6 +734,60 @@
699 734 }
700 735 }
701 736
737 + /// The creators page fills to what the renderer builds.
738 + ///
739 + /// Three readers and two price sets. The standing decides which call to
740 + /// action is placed, which is the branch, and the prices fill the tier
741 + /// table's eight figures. Prices nothing else in the tree uses, for
742 + /// `/use-cases`' reason: filling with the defaults would pass against a
743 + /// residual that had baked one request's numbers into its literals.
744 + #[test]
745 + fn the_creators_residual_fills_to_what_the_renderer_builds() {
746 + use crate::quasi::creators::{Standing, page_region, page_region_serve};
747 + use quasi_axum::Serves as _;
748 +
749 + for standing in [Standing::Visitor, Standing::Reader, Standing::Creator] {
750 + for prices in [stated_prices(), odd_prices()] {
751 + for total in [0, 7] {
752 + assert_eq!(
753 + page_region_serve(&CREATORS, &standing, total, &prices),
754 + Webview::new()
755 + .fragment(&Node::Region(page_region(&standing, total, &prices))),
756 + "total={total}",
757 + );
758 + }
759 + }
760 + }
761 + }
762 +
763 + /// The four tier rows are compiled, not rebuilt per request.
764 + ///
765 + /// What says the copy move landed. The rows come out of
766 + /// `content/creators.toml` and are unrolled at macro time, so each tier's
767 + /// name and what it is for are literals here and only its two figures are
768 + /// holes. A loop appearing would mean they went back to being read through
769 + /// a path the macro cannot evaluate.
770 + #[test]
771 + fn the_creators_residual_unrolls_its_tier_table() {
772 + use quasi_router::stage::Op;
773 +
774 + let compiled = format!("{:?}", CREATORS.ops());
775 + for tier in ["Basic", "Small Files", "Big Files", "Everything"] {
776 + assert!(compiled.contains(tier), "{tier} is not in the residual");
777 + }
778 +
779 + fn loops(ops: &[Op]) -> usize {
780 + ops.iter()
781 + .map(|op| match op {
782 + Op::Loop(body) => 1 + loops(body),
783 + Op::Branch(body) => loops(body),
784 + _ => 0,
785 + })
786 + .sum()
787 + }
788 + assert_eq!(loops(CREATORS.ops()), 0, "the tiers are unrolled");
789 + }
790 +
702 791 /// Every screen the roster names is checked above.
703 792 ///
704 793 /// The gap this closes is the one a table opens: a screen added to
@@ -714,8 +803,9 @@
714 803 fn every_screen_on_the_seam_is_checked() {
715 804 /// Screens with their own filling test: `/use-cases`, `/fan-plus`,
716 805 /// `/c/{username}/{slug}`, `/dashboard/export`, `/git/{owner}`, and the
717 - /// two forum panes, the two contact panes, and the payout card.
718 - const HOLED: usize = 10;
806 + /// two forum panes, the two contact panes, the payout card and
807 + /// `/creators`.
808 + const HOLED: usize = 11;
719 809
720 810 assert_eq!(
721 811 roster().len(),
@@ -263,9 +263,7 @@
263 263 region REGION as Pane {
264 264 section super::range_heading(&analytics.range);
265 265
266 - for chip in range_chips(&analytics.range) {
267 - include chip;
268 - }
266 + include each range_chips(&analytics.range);
269 267
270 268 include stats(&analytics.stats);
271 269
@@ -419,4 +419,58 @@
419 419 ])),
420 420 ::quasi_router::stage::Op::Lit(::std::borrow::Cow::Borrowed("<div class=\"anchored\" id=\"payout-summary-section-anchored\" data-menu=\"anchored\" hidden></div></div>")),
421 421 ]);
422 +
423 + pub static CREATORS: ::quasi_router::stage::Residual =
424 + ::quasi_router::stage::Residual::compiled(&[
425 + ::quasi_router::stage::Op::Lit(::std::borrow::Cow::Borrowed("<div id=\"creators\" class=\"region pane\"><h1 class=\"heading\">Become a Creator</h1><p class=\"text\">Anyone can sign up to browse and buy. To create projects and sell your work, apply for creator access. Most applications are approved within a few days. Makenotwork is in private alpha; we&#39;re approving applications one cohort at a time.</p><h2 class=\"heading\">How It Works</h2><div class=\"rich\" data-disable-scripting><ol>\n<li><strong>Sign up</strong> and verify your email</li>\n<li><strong>Apply</strong> from your dashboard: tell us what you make and which tier fits</li>\n<li><strong>Get approved</strong>: we review applications individually, usually within a few days</li>\n</ol>\n<p>We review applications to make sure applicants are here to share and sell creative work. If you make something and want to sell it, you'll likely get in. Link to your existing work (a portfolio, channel, or profile elsewhere) to speed things up.</p>\n<p><strong>Important:</strong> You sell in the currency your Stripe account settles in, and receiving payouts requires a <a href=\"https://stripe.com/global\" rel=\"noopener noreferrer\">Stripe</a> account in a supported country that settles in one of the six we support: <strong>USD, CAD, GBP, AUD, NZD or EUR</strong>. Check both with Stripe before applying.</p>\n</div><div class=\"figures\"><div class=\"figure\" aria-label=\"Active Creators: ")),
426 + ::quasi_router::stage::Op::Hole { scope: 0, id: 0 },
427 + ::quasi_router::stage::Op::Lit(::std::borrow::Cow::Borrowed("\"><span class=\"figure-value\" aria-hidden=\"true\">")),
428 + ::quasi_router::stage::Op::Hole { scope: 0, id: 0 },
429 + ::quasi_router::stage::Op::Lit(::std::borrow::Cow::Borrowed("</span><span class=\"figure-caption\" aria-hidden=\"true\">Active Creators</span></div></div><h2 class=\"heading\">Pricing</h2><p class=\"text\">Flat monthly fee. 0% cut of your revenue. The only deduction from fan payments is the payment processor&#39;s fee (~3%).</p><div role=\"table\" class=\"table\"><div role=\"row\" class=\"table-head\"><span role=\"columnheader\" class=\"table-heading col-Tier cell-content cell-keeps\">Tier</span><span role=\"columnheader\" class=\"table-heading col-Monthly cell-content cell-drops-next\">Monthly</span><span role=\"columnheader\" class=\"table-heading col-Best-For cell-fill cell-drops-next\">Best For</span><span role=\"columnheader\" class=\"table-heading col-Storage cell-content cell-drops-next\">Storage</span></div><div role=\"row\" class=\"table-row\" data-row><div class=\"cell col-Tier cell-content cell-keeps cell-value\">Basic</div><div class=\"cell col-Monthly cell-content cell-drops-next cell-value\">")),
430 + ::quasi_router::stage::Op::Hole { scope: 1, id: 0 },
431 + ::quasi_router::stage::Op::Lit(::std::borrow::Cow::Borrowed("</div><div class=\"cell col-Best-For cell-fill cell-drops-next cell-value\">Text, blogs, newsletters</div><div class=\"cell col-Storage cell-content cell-drops-next cell-value\">")),
432 + ::quasi_router::stage::Op::Hole { scope: 1, id: 1 },
433 + ::quasi_router::stage::Op::Lit(::std::borrow::Cow::Borrowed("</div></div><div role=\"row\" class=\"table-row\" data-row><div class=\"cell col-Tier cell-content cell-keeps cell-value\">Small Files</div><div class=\"cell col-Monthly cell-content cell-drops-next cell-value\">")),
434 + ::quasi_router::stage::Op::Hole { scope: 1, id: 2 },
435 + ::quasi_router::stage::Op::Lit(::std::borrow::Cow::Borrowed("</div><div class=\"cell col-Best-For cell-fill cell-drops-next cell-value\">Audio, plugins, small software</div><div class=\"cell col-Storage cell-content cell-drops-next cell-value\">")),
436 + ::quasi_router::stage::Op::Hole { scope: 1, id: 3 },
437 + ::quasi_router::stage::Op::Lit(::std::borrow::Cow::Borrowed("</div></div><div role=\"row\" class=\"table-row\" data-row><div class=\"cell col-Tier cell-content cell-keeps cell-value\">Big Files</div><div class=\"cell col-Monthly cell-content cell-drops-next cell-value\">")),
438 + ::quasi_router::stage::Op::Hole { scope: 1, id: 4 },
439 + ::quasi_router::stage::Op::Lit(::std::borrow::Cow::Borrowed("</div><div class=\"cell col-Best-For cell-fill cell-drops-next cell-value\">Video, games, large software</div><div class=\"cell col-Storage cell-content cell-drops-next cell-value\">")),
440 + ::quasi_router::stage::Op::Hole { scope: 1, id: 5 },
441 + ::quasi_router::stage::Op::Lit(::std::borrow::Cow::Borrowed("</div></div><div role=\"row\" class=\"table-row\" data-row><div class=\"cell col-Tier cell-content cell-keeps cell-value\">Everything</div><div class=\"cell col-Monthly cell-content cell-drops-next cell-value\">")),
442 + ::quasi_router::stage::Op::Hole { scope: 1, id: 6 },
443 + ::quasi_router::stage::Op::Lit(::std::borrow::Cow::Borrowed("</div><div class=\"cell col-Best-For cell-fill cell-drops-next cell-value\">All features, current and future</div><div class=\"cell col-Storage cell-content cell-drops-next cell-value\">")),
444 + ::quasi_router::stage::Op::Hole { scope: 1, id: 7 },
445 + ::quasi_router::stage::Op::Lit(::std::borrow::Cow::Borrowed("</div></div></div><div class=\"rich\" data-disable-scripting><p>Every tier is the complete platform: <code>/u/username</code> profile, project and item pages, project forum, Discover listing, memberships, pay-what-you-want, promo codes, RSS, analytics, full data export, 2FA/passkeys. The tier picks the file-size envelope, not the feature set. You sell in your Stripe account's currency (USD, CAD, GBP, AUD, NZD or EUR); receiving payouts requires <a href=\"https://stripe.com/global\" rel=\"noopener noreferrer\">Stripe</a> in a supported country. <a href=\"/docs/tiers\" rel=\"noopener noreferrer\">Full tier details</a> | <a href=\"/docs/pricing\" rel=\"noopener noreferrer\">Pricing models</a></p>\n</div><div class=\"rich\" data-disable-scripting><p><strong>Not ready to commit?</strong> Request a <strong>free trial</strong> (2-6 weeks, no credit card) when you apply. Or <a href=\"/sandbox\" rel=\"noopener noreferrer\">try sandbox mode</a> to explore the dashboard without signing up.</p>\n</div><h2 class=\"heading\">Who Runs This</h2><div class=\"rich\" data-disable-scripting><p>Makenotwork is built and operated by one person. No investors, no board, no outside pressure. Decisions are fast and aligned with creators, but there's no large team behind the scenes. Read the full picture in our <a href=\"/docs/guarantees#continuity\" rel=\"noopener noreferrer\">continuity guarantee</a> and <a href=\"/docs/economics\" rel=\"noopener noreferrer\">platform economics</a>.</p>\n</div>")),
446 + ::quasi_router::stage::Op::Branch(::std::borrow::Cow::Borrowed(&[
447 + ::quasi_router::stage::Op::Lit(::std::borrow::Cow::Borrowed("<p class=\"text\">You have creator access.</p>")),
448 +
449 + ])),
450 + ::quasi_router::stage::Op::Branch(::std::borrow::Cow::Borrowed(&[
451 + ::quasi_router::stage::Op::Lit(::std::borrow::Cow::Borrowed("<a class=\"button\" data-act href=\"/dashboard\">Go to Dashboard</a>")),
452 +
453 + ])),
454 + ::quasi_router::stage::Op::Branch(::std::borrow::Cow::Borrowed(&[
455 + ::quasi_router::stage::Op::Lit(::std::borrow::Cow::Borrowed("<p class=\"text\">Ready to create?</p>")),
456 +
457 + ])),
458 + ::quasi_router::stage::Op::Branch(::std::borrow::Cow::Borrowed(&[
459 + ::quasi_router::stage::Op::Lit(::std::borrow::Cow::Borrowed("<a class=\"button\" data-act href=\"/dashboard?tab=settings&amp;section=creator\">Apply from Dashboard</a>")),
460 +
461 + ])),
462 + ::quasi_router::stage::Op::Branch(::std::borrow::Cow::Borrowed(&[
463 + ::quasi_router::stage::Op::Lit(::std::borrow::Cow::Borrowed("<p class=\"text\">Join to get started.</p>")),
464 +
465 + ])),
466 + ::quasi_router::stage::Op::Branch(::std::borrow::Cow::Borrowed(&[
467 + ::quasi_router::stage::Op::Lit(::std::borrow::Cow::Borrowed("<a class=\"button\" data-act href=\"/join\">Join</a>")),
468 +
469 + ])),
470 + ::quasi_router::stage::Op::Branch(::std::borrow::Cow::Borrowed(&[
471 + ::quasi_router::stage::Op::Lit(::std::borrow::Cow::Borrowed("<a class=\"button\" data-act href=\"/login\">Login</a>")),
472 +
473 + ])),
474 + ::quasi_router::stage::Op::Lit(::std::borrow::Cow::Borrowed("<div class=\"anchored\" id=\"creators-anchored\" data-menu=\"anchored\" hidden></div></div>")),
475 + ]);
422 476
@@ -1,0 +1,33 @@
1 + # The tier table at `/creators`, which is words and a tier name.
2 + #
3 + # Read at build time by the `declare!` in `src/quasi/creators.rs`. Editing this
4 + # file is editing the page. See `content/use-cases.toml` for the split this
5 + # follows, including the trade it makes.
6 + #
7 + # `priced` names which tier's figures the row ends with, and that is the one
8 + # thing on this table a request decides: the numbers come from `TierPrices` in
9 + # memory, so the last two cells are holes and the first two are literals. The
10 + # word here is a tier name and not a price, which is what keeps a stale number
11 + # from ever being written down.
12 + #
13 + # The order is the order the shipped table listed them.
14 +
15 + [[tiers]]
16 + name = "Basic"
17 + best_for = "Text, blogs, newsletters"
18 + priced = "basic"
19 +
20 + [[tiers]]
21 + name = "Small Files"
22 + best_for = "Audio, plugins, small software"
23 + priced = "small-files"
24 +
25 + [[tiers]]
26 + name = "Big Files"
27 + best_for = "Video, games, large software"
28 + priced = "big-files"
29 +
30 + [[tiers]]
31 + name = "Everything"
32 + best_for = "All features, current and future"
33 + priced = "everything"