Skip to main content

max / makenotwork

Describe the content policy, where prose is prose The third public document, and the case /use-cases' rule does not reach: six sections of sentences, with bullets that are prose rather than records, carrying an inline link and an emphasised address. A Row can hold neither, and forcing them through rows loses both silently. So each section's body is one Node::rich, rendered by docengine, the same path /docs/* takes. What stays described is the structure: a Node::section per heading. The one list that is data - seven other policies, each a title, a sentence and a route - is rows with acts. Deletes templates/pages/policy.html, PolicyTemplate, landing::policy_page and the eight .policy-page rules, whose headings, section spacing and list indents are the design system's now. The measure moved onto .policy-page main.
Author: Max Johnson <me@maxj.phd> · 2026-09-01 13:07 UTC
Signed with PGP, not checked
Commit: e00614cd2f55a469e1507f9963be11f5cf043442
Parent: 84f0886
8 files changed, +299 insertions, -144 deletions
@@ -4222,24 +4222,12 @@
4222 4222 .changelog-page .changelog-entry li { margin-bottom: var(--gap-bound); }
4223 4223
4224 4224 /* Policy page (templates/pages/policy.html). */
4225 - .policy-page .container { max-width: 900px; margin: 0 auto; }
4226 - .policy-page h1 { font-size: var(--text-display); margin-bottom: var(--gap-peer); }
4227 - .policy-page h2 {
4228 - font-size: var(--text-head);
4229 - margin-top: var(--gap-page);
4230 - margin-bottom: var(--gap-section);
4231 - }
4232 - .policy-page .policy-section { margin-bottom: var(--gap-page); line-height: 1.7; }
4233 - .policy-page .policy-section ul {
4234 - padding-left: var(--gap-pane);
4235 - margin: var(--gap-section) 0;
4236 - }
4237 - .policy-page .policy-section li { margin-bottom: var(--gap-bound); }
4238 - .policy-page .contact {
4239 - margin-top: var(--gap-page);
4240 - padding: var(--gap-pane);
4241 - background: var(--surface-overlay);
4242 - }
4225 + /* The content policy moved to the description layer. Its headings, its section
4226 + spacing and its list indents are the design system's now; the prose itself
4227 + goes through docengine, the same renderer `/docs/*` uses. What is left is the
4228 + measure, on <main> rather than on a `.container`, as every converted document
4229 + does. */
4230 + .policy-page main { max-width: 900px; margin: 0 auto; }
4243 4231
4244 4232 /* Admin page extras (in addition to global .admin-page h1). */
4245 4233 .admin-page .metrics-grid {
@@ -55,6 +55,7 @@
55 55 pub mod library_tabs;
56 56 pub mod media_picker;
57 57 pub mod payout_summary;
58 + pub mod policy;
58 59 pub mod pricing;
59 60 pub mod project_analytics;
60 61 pub mod project_content;
@@ -386,7 +387,7 @@
386 387 /// mechanism: those screens resolve nothing per request and take a state built
387 388 /// once at startup. These resolve a session when there is one, so they carry a
388 389 /// per-request viewer and can mint a CSRF token for the form on them.
389 - pub const PUBLIC_DOCUMENT_PATHS: &[&str] = &[team::PATH, use_cases::PATH];
390 + pub const PUBLIC_DOCUMENT_PATHS: &[&str] = &[team::PATH, use_cases::PATH, policy::PATH];
390 391
391 392 /// Every screen's switch name, in the same order as [`PATHS`].
392 393 ///
@@ -572,6 +573,10 @@
572 573 use_cases::PATH,
573 574 public_document_mount(app, use_cases::PATH, use_cases::screen, use_cases::renderer),
574 575 ),
576 + (
577 + policy::PATH,
578 + public_document_mount(app, policy::PATH, policy::screen, policy::renderer),
579 + ),
575 580 ]
576 581 }
577 582
@@ -141,7 +141,6 @@
141 141 impl_into_response!(
142 142 // Public pages
143 143 SandboxTemplate,
144 - PolicyTemplate,
145 144 IndexTemplate,
146 145 LibraryTemplate,
147 146 CartTemplate,
@@ -31,16 +31,6 @@
31 31 pub csrf_token: CsrfTokenOption,
32 32 }
33 33
34 - /// Content policy page.
35 - #[derive(Template)]
36 - #[template(path = "pages/policy.html")]
37 - pub struct PolicyTemplate {
38 - /// CSRF token injected into forms; `None` on public pages that have no forms.
39 - pub csrf_token: CsrfTokenOption,
40 - /// Logged-in user context for the site header; `None` when not authenticated.
41 - pub session_user: Option<SessionUser>,
42 - }
43 -
44 34 /// Landing page.
45 35 #[derive(Template)]
46 36 #[template(path = "pages/index.html")]
@@ -24,7 +24,7 @@
24 24 templates::{
25 25 CarouselFrame, CartTemplate, EconomicsTemplate, FanPlusTemplate, IndexTemplate,
26 26 LandingVelocity, LibraryCollectionsTabTemplate, LibraryPurchasesTabTemplate,
27 - LibraryTemplate, LoginTemplate, PolicyTemplate,
27 + LibraryTemplate, LoginTemplate,
28 28 },
29 29 types::{Collection, UserSubscription},
30 30 };
@@ -485,19 +485,6 @@
485 485 )
486 486 }
487 487
488 - /// Render the content policy page.
489 - #[tracing::instrument(skip_all, name = "landing::policy_page")]
490 - pub(super) async fn policy_page(
491 - session: Session,
492 - MaybeUserUnverified(maybe_user): MaybeUserUnverified,
493 - ) -> impl IntoResponse {
494 - let csrf_token = get_csrf_token(&session).await;
495 - PolicyTemplate {
496 - csrf_token,
497 - session_user: maybe_user,
498 - }
499 - }
500 -
501 488 /// Query params for the Fan+ page.
502 489 #[derive(Debug, Deserialize)]
503 490 pub(super) struct FanPlusQuery {
@@ -134,7 +134,6 @@
134 134 .route_get("/receipt/{transaction_id}", get(content::receipt_page))
135 135 .route_get("/buy/{item_id}", get(content::buy_page))
136 136 .route_get("/checkout/complete", get(landing::checkout_complete))
137 - .route_get("/policy", get(landing::policy_page))
138 137 .route_get("/fan-plus", get(landing::fan_plus_page))
139 138 // Landing "notify me". CSRF-protected like the other public forms, and
140 139 // rate limited because it is unauthenticated and writes a row.
@@ -1,0 +1,286 @@
1 + //! The content policy at `/policy`, described.
2 + //!
3 + //! The third public document. It replaces `templates/pages/policy.html`,
4 + //! `PolicyTemplate` and `landing::policy_page`.
5 + //!
6 + //! # Prose is prose, and rows are for data
7 + //!
8 + //! `/use-cases` set the rule that a card holding a structure is a region and a
9 + //! card holding a sentence is a row. This page is the case that rule does not
10 + //! reach: six sections of prose, with bullets that are sentences rather than
11 + //! records, carrying inline links and one emphasised address.
12 + //!
13 + //! A `Row` cannot hold either. `Row::new("Report suspicious downloads to
14 + //! reports@makenot.work")` loses the emphasis, and nothing in a row can carry
15 + //! the link inside "part of our [creator guarantees]". Forcing prose through
16 + //! rows would silently flatten both, and neither loss is visible in a test that
17 + //! checks the text is present.
18 + //!
19 + //! So each section's body is one [`Node::rich`], which is markdown and renders
20 + //! through docengine -- the same path `/docs/*` takes, so the policy prose and
21 + //! the documents it links to are formatted by one renderer. **What stays
22 + //! described is the structure**: [`Node::section`] per heading, so the section
23 + //! hierarchy is a fact of the screen rather than an `<h2>` inside a blob.
24 + //!
25 + //! The escape hatch is not swallowing the page. It is carrying the one thing
26 + //! this page is made of, which is sentences.
27 + //!
28 + //! # The exception, and it is the one list that is data
29 + //!
30 + //! "Other Policies" is seven links, each a title and a sentence saying what the
31 + //! document covers. That is a record per row and a route per row, so it is a
32 + //! [`Node::list`] of rows with acts, not markdown. The test is what it would
33 + //! cost to add an eighth: a row, versus a line of prose somebody has to match
34 + //! against six others by hand.
35 +
36 + use makeover_layout as layout;
37 + use quasi_router::screen::{Act, Row};
38 + use quasi_router::{
39 + Action, Document, Node, RegionKind, Request, Response, RouteError, Screen as Described, Slot,
40 + };
41 + use quasi_webview::Webview;
42 +
43 + /// The address, registered whole. See [`super::public_document_mount`].
44 + pub const PATH: &str = "/policy";
45 +
46 + /// The page's own region, and what the skip link points at.
47 + pub const PAGE_REGION: &str = "policy";
48 +
49 + const MEASURE: layout::Measure = layout::Measure::Wide;
50 +
51 + /// One headed section of the policy.
52 + struct Section {
53 + heading: &'static str,
54 + /// The body, as markdown. See the module header for why this is prose
55 + /// rather than nodes.
56 + body: &'static str,
57 + }
58 +
59 + /// The five prose sections, in the template's order. "Other Policies" is not
60 + /// here; see [`OTHER`].
61 + const SECTIONS: &[Section] = &[
62 + Section {
63 + heading: "What's Welcome",
64 + body: "Creative work across all supported types:\n\
65 + \n\
66 + - Software, plugins, presets, and templates\n\
67 + - Audio: music, podcasts, samples, sound design\n\
68 + - Writing: articles, guides, courses, fiction\n\
69 + - Visual work: images, photography, design assets\n\
70 + - Video: tutorials, performances, documentaries\n\
71 + \n\
72 + If you made it and it's legal to distribute, it belongs here.",
73 + },
74 + Section {
75 + heading: "What's Not Allowed",
76 + body: "- Content that violates applicable law\n\
77 + - Harassment, threats, or doxxing\n\
78 + - Spam, deceptive listings, or bait-and-switch pricing\n\
79 + - Malware, exploits, or tools designed to cause harm\n\
80 + - Impersonation of other creators or organizations\n\
81 + - Content you don't have the rights to distribute",
82 + },
83 + Section {
84 + heading: "How We Handle Issues",
85 + body: "During private alpha, every creator has a direct relationship with the admin. \
86 + If something comes up, we talk about it. No automated takedowns, no faceless \
87 + tickets.\n\
88 + \n\
89 + Post-alpha, we'll introduce a formal process with written notice of any policy \
90 + violation, an opportunity to appeal, and continued access to data export \
91 + throughout.",
92 + },
93 + Section {
94 + heading: "Your Rights",
95 + body: "- Full data export is always available: your content, metadata, and transaction \
96 + history\n\
97 + - If we ever moderate content or suspend an account, you'll get a clear \
98 + explanation of what policy was violated\n\
99 + - You'll have the opportunity to appeal\n\
100 + - You can export your data even while suspended (excluding content we can't \
101 + legally host)\n\
102 + \n\
103 + These commitments are part of our [creator guarantees](/docs/guarantees).",
104 + },
105 + Section {
106 + heading: "Software Downloads",
107 + body: "Makenotwork hosts downloadable software uploaded by creators. While we take \
108 + steps to make sure downloads are safe:\n\
109 + \n\
110 + - Creators are responsible for the safety and integrity of their uploads\n\
111 + - Users should verify downloads with antivirus software before running them\n\
112 + - We do not guarantee that any download is free of malware or other harmful \
113 + content\n\
114 + - Report suspicious downloads to **reports@makenot.work**",
115 + },
116 + ];
117 +
118 + /// The other policy documents: a title, what it covers, and where it lives.
119 + const OTHER: &[(&str, &str, &str)] = &[
120 + (
121 + "Terms of Service",
122 + "What you agree to by using Makenotwork",
123 + "/docs/terms-of-service",
124 + ),
125 + (
126 + "Privacy Policy",
127 + "What we collect, why, and how to exercise your rights",
128 + "/docs/privacy-policy",
129 + ),
130 + (
131 + "Payments & Refunds",
132 + "Merchant-of-record model, refunds, chargebacks",
133 + "/docs/payments",
134 + ),
135 + (
136 + "Acceptable Use",
137 + "Specific behaviour that gets accounts suspended",
138 + "/docs/acceptable-use",
139 + ),
140 + (
141 + "Copyright & DMCA",
142 + "How takedowns and counter-notifications work",
143 + "/docs/copyright",
144 + ),
145 + (
146 + "Appeals",
147 + "How to challenge a moderation decision",
148 + "/docs/appeals",
149 + ),
150 + (
151 + "Mailing List Data Processing",
152 + "Who answers a subscriber's request about a project mailing list",
153 + "/docs/mailing-list-data-processing",
154 + ),
155 + ];
156 +
157 + /// The page. Reads nothing.
158 + pub fn screen(_viewer: &super::Viewer, _request: Request) -> Result<Response, RouteError> {
159 + Ok(page_screen().into())
160 + }
161 +
162 + /// The whole document: the title, the measure, the body.
163 + fn page_screen() -> Described {
164 + let mut page = Slot::new(PAGE_REGION, RegionKind::Pane)
165 + .with(Node::page("Content Policy"))
166 + .with(Node::text(
167 + "Makenotwork exists so creators can sell their work on fair terms. This policy \
168 + describes what belongs here, what doesn't, and how we handle problems.",
169 + ));
170 +
171 + for section in SECTIONS {
172 + page = page
173 + .with(Node::section(section.heading))
174 + .with(Node::rich(section.body));
175 + }
176 +
177 + page = page
178 + .with(Node::section("Other Policies"))
179 + .with(Node::list(OTHER.iter().map(|(title, covers, route)| {
180 + Row::new(*title)
181 + .secondary(*covers)
182 + .act(Act::new("Read", Action::get(*route).navigating()))
183 + })));
184 +
185 + page = page.with(Node::section("Questions")).with(Node::rich(
186 + "If something's unclear or you want to check before posting, reach out at \
187 + **policy@makenot.work**.",
188 + ));
189 +
190 + Described::single("Content Policy - Makenotwork")
191 + .measured(MEASURE)
192 + .documented(
193 + Document::default().classed(crate::shell::body_class(MEASURE, &["policy-page"])),
194 + )
195 + .summarised("What belongs on Makenotwork, what doesn't, and how problems are handled.")
196 + .with(page)
197 + }
198 +
199 + /// The document this screen is drawn in. Same shape as the other public
200 + /// documents: the skip link and the site header, whose user is optional here.
201 + #[must_use]
202 + pub fn renderer(viewer: &super::Viewer) -> Webview {
203 + Webview::new().with_shell(viewer.document_shell().with_body_first(format!(
204 + "{}{}",
205 + crate::shell::skip_link(PAGE_REGION),
206 + crate::shell::site_header(viewer.user.as_ref(), Some(&viewer.csrf)),
207 + )))
208 + }
209 +
210 + #[cfg(test)]
211 + mod tests {
212 + use super::*;
213 +
214 + fn html() -> String {
215 + use quasi_axum::Serves as _;
216 +
217 + Webview::new().screen(&page_screen())
218 + }
219 +
220 + /// `2790e5c4`. This template carried both classes on the body already, so
221 + /// this one is a copy rather than the merge `/team` and `/use-cases` were.
222 + #[test]
223 + fn the_document_carries_the_classes_the_template_carried() {
224 + let screen = page_screen();
225 +
226 + assert_eq!(
227 + screen.document.body_class.as_deref(),
228 + Some("padded-page policy-page")
229 + );
230 + }
231 +
232 + /// The two things rows would have flattened, which is the whole argument
233 + /// for prose being prose: an inline link inside a sentence, and an
234 + /// emphasised address inside a bullet.
235 + #[test]
236 + fn the_prose_keeps_its_inline_link_and_its_emphasis() {
237 + let html = html();
238 +
239 + assert!(
240 + html.contains(r#"href="/docs/guarantees""#),
241 + "the creator-guarantees link did not survive: {html}"
242 + );
243 + assert!(
244 + html.contains("<strong>reports@makenot.work</strong>")
245 + || html.contains("<b>reports@makenot.work</b>"),
246 + "the reports address lost its emphasis: {html}"
247 + );
248 + }
249 +
250 + /// Every policy document the page pointed at is still pointed at.
251 + ///
252 + /// The titles are compared escaped, because that is what lands in the
253 + /// markup: two of the seven carry an ampersand, and the description layer
254 + /// escapes it exactly as the template's `&amp;` did.
255 + #[test]
256 + fn every_other_policy_keeps_its_row_and_its_route() {
257 + let html = html();
258 +
259 + for (title, _, route) in OTHER {
260 + let escaped = crate::helpers::escape_html(title);
261 + assert!(html.contains(&escaped), "{title} missing");
262 + assert!(html.contains(route), "{route} missing");
263 + }
264 + }
265 +
266 + /// Both addresses a reader is told to write to are still on the page. They
267 + /// are different mailboxes on purpose (`feedback_mnw_email_routing`), so a
268 + /// conversion that collapsed them would be a real loss.
269 + #[test]
270 + fn both_contact_addresses_survive_and_stay_distinct() {
271 + let html = html();
272 +
273 + assert!(html.contains("reports@makenot.work"), "{html}");
274 + assert!(html.contains("policy@makenot.work"), "{html}");
275 + }
276 +
277 + /// `736f45a5`: this screen's markup carries none of the four spellings.
278 + #[test]
279 + fn the_page_spells_no_spinner() {
280 + let html = html();
281 +
282 + for spelling in ["htmx-indicator", "spinner", "loading-text", "loading-state"] {
283 + assert!(!html.contains(spelling), "{spelling} survives in {html}");
284 + }
285 + }
286 + }
@@ -1,99 +1,0 @@
1 - {% extends "base.html" %}
2 -
3 - {% block title %}Content Policy - Makenotwork{% endblock %}
4 - {% block body_attrs %} class="{{ crate::shell::measure(crate::shell::Measure::Wide) }} policy-page"{% endblock %}
5 -
6 - {% block head %}
7 - {% endblock %}
8 -
9 - {% block content %}
10 - {% include "partials/site_header.html" %}
11 -
12 - <div class="container">
13 - <h1 class="page-title">Content Policy</h1>
14 -
15 - <div class="page-intro">
16 - Makenotwork exists so creators can sell their work on fair terms.
17 - This policy describes what belongs here, what doesn't, and how we handle problems.
18 - </div>
19 -
20 - <div class="policy-section">
21 - <h2 class="section-header">What's Welcome</h2>
22 - <p>Creative work across all supported types:</p>
23 - <ul>
24 - <li>Software, plugins, presets, and templates</li>
25 - <li>Audio: music, podcasts, samples, sound design</li>
26 - <li>Writing: articles, guides, courses, fiction</li>
27 - <li>Visual work: images, photography, design assets</li>
28 - <li>Video: tutorials, performances, documentaries</li>
29 - </ul>
30 - <p>If you made it and it's legal to distribute, it belongs here.</p>
31 - </div>
32 -
33 - <div class="policy-section">
34 - <h2 class="section-header">What's Not Allowed</h2>
35 - <ul>
36 - <li>Content that violates applicable law</li>
37 - <li>Harassment, threats, or doxxing</li>
38 - <li>Spam, deceptive listings, or bait-and-switch pricing</li>
39 - <li>Malware, exploits, or tools designed to cause harm</li>
40 - <li>Impersonation of other creators or organizations</li>
41 - <li>Content you don't have the rights to distribute</li>
42 - </ul>
43 - </div>
44 -
45 - <div class="policy-section">
46 - <h2 class="section-header">How We Handle Issues</h2>
47 - <p>
48 - During private alpha, every creator has a direct relationship with the admin.
49 - If something comes up, we talk about it. No automated takedowns, no faceless tickets.
50 - </p>
51 - <p>
52 - Post-alpha, we'll introduce a formal process with written notice of any policy
53 - violation, an opportunity to appeal, and continued access to data export throughout.
54 - </p>
55 - </div>
56 -
57 - <div class="policy-section">
58 - <h2 class="section-header">Your Rights</h2>
59 - <ul>
60 - <li>Full data export is always available: your content, metadata, and transaction history</li>
61 - <li>If we ever moderate content or suspend an account, you'll get a clear explanation of what policy was violated</li>
62 - <li>You'll have the opportunity to appeal</li>
63 - <li>You can export your data even while suspended (excluding content we can't legally host)</li>
64 - </ul>
65 - <p>These commitments are part of our <a href="/docs/guarantees">creator guarantees</a>.</p>
66 - </div>
67 -
68 - <div class="policy-section">
69 - <h2 class="section-header">Software Downloads</h2>
70 - <p>Makenotwork hosts downloadable software uploaded by creators.
71 - While we take steps to make sure downloads are safe:</p>
72 - <ul>
73 - <li>Creators are responsible for the safety and integrity of their uploads</li>
74 - <li>Users should verify downloads with antivirus software before running them</li>
75 - <li>We do not guarantee that any download is free of malware or other harmful content</li>
76 - <li>Report suspicious downloads to <strong>reports@makenot.work</strong></li>
77 - </ul>
78 - </div>
79 -
80 - <div class="policy-section">
81 - <h2 class="section-header">Other Policies</h2>
82 - <ul>
83 - <li><a href="/docs/terms-of-service">Terms of Service</a>: what you agree to by using Makenotwork</li>
84 - <li><a href="/docs/privacy-policy">Privacy Policy</a>: what we collect, why, and how to exercise your rights</li>
85 - <li><a href="/docs/payments">Payments &amp; Refunds</a>: merchant-of-record model, refunds, chargebacks</li>
86 - <li><a href="/docs/acceptable-use">Acceptable Use</a>: specific behaviour that gets accounts suspended</li>
87 - <li><a href="/docs/copyright">Copyright &amp; DMCA</a>: how takedowns and counter-notifications work</li>
88 - <li><a href="/docs/appeals">Appeals</a>: how to challenge a moderation decision</li>
89 - <li><a href="/docs/mailing-list-data-processing">Mailing List Data Processing</a>: who answers a subscriber's request about a project mailing list</li>
90 - </ul>
91 - </div>
92 -
93 - <div class="contact">
94 - <h2 class="section-header">Questions</h2>
95 - <p>If something's unclear or you want to check before posting, reach out at
96 - <strong>policy@makenot.work</strong>.</p>
97 - </div>
98 - </div>
99 - {% endblock %}