Skip to main content

max / makenotwork

Performance pass: layout shift fixes, reload elimination, style guide Philosophy: - Add performance_philosophy.md (Tufte/McMaster-Carr principles for MNW) - Add frontend performance section to CONTRIBUTING.md Layout shift: - Add aspect-ratio to images on item page, audio player, wizards - Buy page already correct (has aspect-ratio in CSS class) Reload elimination: - Item settings: 4x window.location.reload → tab re-click - Item details: primary tag set → tab re-click - User profile: domain removal → tab re-click Loading states: - Remove "Loading..." placeholder text from 2FA and passkey sections
Co-Authored-By
Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Author: Max J. <87768334+MaxJMath@users.noreply.github.com> · 2026-05-05 18:34 UTC
Commit: 2006499113fc244c3776261880ef1acca164c15b
Parent: 46bdd28
11 files changed, +138 insertions, -18 deletions
@@ -278,6 +278,59 @@
278 278 - `csrf_token: Option<String>` — for the CSRF meta tag
279 279 - `session_user: Option<SessionUser>` — for the header (login state, avatar)
280 280
281 + ## Frontend Performance
282 +
283 + These rules apply to all HTML templates. The goal is zero layout shift, no wasted pixels, and instant-feeling interactions.
284 +
285 + ### Images
286 +
287 + Every `<img>` must have explicit dimensions to prevent layout shift:
288 + ```html
289 + <!-- Inline style with both width and height -->
290 + <img src="{{ url }}" alt="{{ title }}"
291 + style="width: 120px; height: 120px; object-fit: cover;">
292 +
293 + <!-- Or use aspect-ratio for responsive images -->
294 + <img src="{{ url }}" alt="{{ title }}"
295 + style="width: 100%; aspect-ratio: 1 / 1; object-fit: cover;">
296 + ```
297 +
298 + Never use `loading="lazy"` for images that may appear above the fold (the first visible screen before scrolling).
299 +
300 + ### Navigation and Reloads
301 +
302 + Never use `window.location.reload()` when an HTMX partial swap can do the job. Full-page reloads discard all client state, re-parse all JS/CSS, and feel slow.
303 +
304 + ```html
305 + <!-- Bad: full-page reload after action -->
306 + <button hx-put="/api/items/{{ id }}"
307 + hx-on::after-request="if(event.detail.successful) window.location.reload()">
308 +
309 + <!-- Good: re-fetch the relevant tab/section -->
310 + <button hx-put="/api/items/{{ id }}"
311 + hx-on::after-request="if(event.detail.successful) document.getElementById('tab-settings').click()">
312 + ```
313 +
314 + Legitimate uses of `window.location.href`:
315 + - Navigating to a different page entirely (login, checkout redirect, file download)
316 + - After destructive account actions (delete account → login page)
317 +
318 + ### Loading States
319 +
320 + Prefer server-complete responses over client-side loading placeholders. The server should wait for data and send a complete fragment in one paint, rather than sending a skeleton and filling it in later.
321 +
322 + Use `hx-trigger="revealed"` with a loading placeholder only when the data is expensive to fetch AND hidden behind a `<details>` element (e.g., 2FA status, passkey list). Never use loading placeholders for content that's visible on initial tab load.
323 +
324 + ### Inline Scripts
325 +
326 + Keep inline `<script>` blocks under 20 lines. Extract larger scripts to static JS files in `server/static/`. Static files are cached by the browser; inline scripts are re-parsed on every HTMX fragment swap.
327 +
328 + ### Information Density
329 +
330 + Show more data in less space. Prefer tight rows over hero cards. Let users compare by scanning, not by clicking into detail pages. A page that shows 15 items is more useful than one that shows 3.
331 +
332 + Use typography (weight, size) and whitespace for hierarchy instead of borders and background colors. A 16px gap groups elements as well as a 1px border, with less visual noise.
333 +
281 334 ## CSRF Protection
282 335
283 336 The CSRF middleware (`src/csrf.rs`) validates all state-changing requests (POST, PUT, PATCH, DELETE) except exempted paths (webhooks, auth endpoints, OAuth).
@@ -30,7 +30,12 @@
30 30 Dashboard restructure complete (Phases 1-6 in todo_done.md). Tab layout: Projects, Payments, Analytics, Profile, Account, Plan + overflow. Fan/creator progressive disclosure implemented. Labels removed. Jargon renamed.
31 31
32 32 #### Performance
33 - - [ ] Cross-site performance pass — pre-fetch tab data on hover, optimize perceived load times
33 + - [x] Add performance philosophy doc (`docs/performance_philosophy.md`) — Tufte/McMaster-Carr principles applied to MNW
34 + - [x] Add frontend performance rules to CONTRIBUTING.md — image dimensions, reload avoidance, loading states, density
35 + - [x] Fix layout shift: add aspect-ratio to all images missing dimensions (item page, audio player, buy page, wizards)
36 + - [x] Replace `window.location.reload()` with HTMX tab re-fetch on item settings (4x), item details (1x), user profile (2x)
37 + - [x] Remove "Loading..." placeholder text from 2FA and passkey sections (empty until revealed)
38 + - [ ] Extract heavy inline scripts to static JS — item_details.html (278 lines), upload handlers (~250 lines combined). Requires data-attribute refactor for template variables
34 39
35 40 #### Discoverability
36 41 - [ ] Add Media Library access from content editors — "Insert Image" button in blog/item editors
@@ -459,7 +459,7 @@
459 459 {% match item.content %}
460 460 {% when crate::types::ItemContent::Audio with { cover_url, .. } %}
461 461 {% if let Some(url) = cover_url %}
462 - <img src="{{ url }}" alt="{{ item.title }} cover art">
462 + <img src="{{ url }}" alt="{{ item.title }} cover art" style="width: 100%; aspect-ratio: 1 / 1; object-fit: cover;">
463 463 {% else %}
464 464 [Cover Art]
465 465 {% endif %}
@@ -155,7 +155,7 @@
155 155 {% if item.cover_image_url.is_some() %}
156 156 <div class="item-media">
157 157 {% if let Some(img) = item.cover_image_url %}
158 - <img src="{{ img }}" alt="{{ item.title }}" style="width: 100%; display: block;">
158 + <img src="{{ img }}" alt="{{ item.title }}" style="width: 100%; aspect-ratio: 1 / 1; object-fit: cover; display: block;">
159 159 {% endif %}
160 160 </div>
161 161 {% endif %}
@@ -66,7 +66,7 @@
66 66 hx-put="/api/items/{{ item.id }}/primary-tag"
67 67 hx-vals='{"tag_id": "{{ tag.id }}"}'
68 68 hx-swap="none"
69 - hx-on::after-request="window.location.reload()">&#9734;</button>
69 + hx-on::after-request="document.getElementById('tab-details').click()">&#9734;</button>
70 70 {% endif %}
71 71 </span>
72 72 {% endfor %}
@@ -13,7 +13,7 @@
13 13 hx-put="/api/items/{{ item.id }}"
14 14 hx-vals='{"publish_at": ""}'
15 15 hx-confirm="Cancel the scheduled publish?"
16 - hx-on::after-request="if(event.detail.successful) window.location.reload()">Cancel Schedule</button>
16 + hx-on::after-request="if(event.detail.successful) document.getElementById('tab-settings').click()">Cancel Schedule</button>
17 17 </div>
18 18 {% else if item.is_public %}
19 19 <!-- Published: show unpublish button -->
@@ -25,7 +25,7 @@
25 25 hx-put="/api/items/{{ item.id }}"
26 26 hx-vals='{"is_public": "false"}'
27 27 hx-confirm="Unpublish this item? It will be hidden from public view."
28 - hx-on::after-request="if(event.detail.successful) window.location.reload()">Unpublish Item</button>
28 + hx-on::after-request="if(event.detail.successful) document.getElementById('tab-settings').click()">Unpublish Item</button>
29 29 </div>
30 30 {% else %}
31 31 <!-- Draft: show publish now + schedule -->
@@ -37,12 +37,12 @@
37 37 hx-put="/api/items/{{ item.id }}"
38 38 hx-vals='{"is_public": "true"}'
39 39 hx-confirm="Publish this item?"
40 - hx-on::after-request="if(event.detail.successful) window.location.reload()">Publish Now</button>
40 + hx-on::after-request="if(event.detail.successful) document.getElementById('tab-settings').click()">Publish Now</button>
41 41 <button class="secondary" onclick="document.getElementById('schedule-form').style.display='block'">Schedule</button>
42 42 </div>
43 43 <form id="schedule-form" style="display: none; margin-top: 1rem;"
44 44 hx-put="/api/items/{{ item.id }}"
45 - hx-on::after-request="if(event.detail.successful) window.location.reload()">
45 + hx-on::after-request="if(event.detail.successful) document.getElementById('tab-settings').click()">
46 46 <div class="form-group">
47 47 <label for="publish-at">Publish at (UTC)</label>
48 48 <input type="datetime-local" id="publish-at" name="publish_at" required>
@@ -93,7 +93,6 @@
93 93 hx-get="/api/users/me/totp/status"
94 94 hx-trigger="revealed"
95 95 hx-swap="innerHTML">
96 - <p style="opacity: 0.7;">Loading...</p>
97 96 </div>
98 97 </details>
99 98
@@ -103,7 +102,6 @@
103 102 hx-get="/api/users/me/passkeys"
104 103 hx-trigger="revealed"
105 104 hx-swap="innerHTML">
106 - <p style="opacity: 0.7;">Loading...</p>
107 105 </div>
108 106 </details>
109 107
@@ -171,7 +171,7 @@
171 171 hx-confirm="Remove this domain?"
172 172 hx-target="closest .form-section"
173 173 hx-swap="outerHTML"
174 - hx-on::after-request="if(event.detail.successful) location.reload()">Remove</button>
174 + hx-on::after-request="if(event.detail.successful) document.getElementById('tab-profile').click()">Remove</button>
175 175 </div>
176 176 <div id="domain-verify-result" style="margin-top: 0.75rem;"></div>
177 177 {% else %}
@@ -183,7 +183,7 @@
183 183 hx-confirm="Remove this domain? Your domain will stop serving your content."
184 184 hx-target="closest .form-section"
185 185 hx-swap="outerHTML"
186 - hx-on::after-request="if(event.detail.successful) location.reload()">Remove Domain</button>
186 + hx-on::after-request="if(event.detail.successful) document.getElementById('tab-profile').click()">Remove Domain</button>
187 187 {% endif %}
188 188 {% when None %}
189 189 <form hx-post="/api/domains"
@@ -27,7 +27,7 @@
27 27 <div class="project-image-dropzone" id="image-dropzone">
28 28 {% if let Some(url) = cover_image_url %}
29 29 <div class="project-image-current" id="image-current">
30 - <img src="{{ url }}" alt="Item image">
30 + <img src="{{ url }}" alt="Item image" style="width: 100%; aspect-ratio: 1 / 1; object-fit: cover;">
31 31 </div>
32 32 {% else %}
33 33 <div class="project-image-placeholder" id="image-placeholder">
@@ -154,7 +154,7 @@
154 154 var div = document.createElement('div');
155 155 div.className = 'project-image-current';
156 156 div.id = 'image-current';
157 - div.innerHTML = '<img src="' + url + '" alt="Item image">';
157 + div.innerHTML = '<img src="' + url + '" alt="Item image" style="width: 100%; aspect-ratio: 1 / 1; object-fit: cover;">';
158 158 dropzone.insertBefore(div, fileInput);
159 159 currentImg = div;
160 160 }
@@ -15,7 +15,7 @@
15 15 <div class="project-image-dropzone" id="image-dropzone">
16 16 {% if let Some(url) = cover_image_url %}
17 17 <div class="project-image-current" id="image-current">
18 - <img src="{{ url }}" alt="Project image">
18 + <img src="{{ url }}" alt="Project image" style="width: 100%; aspect-ratio: 1 / 1; object-fit: cover;">
19 19 </div>
20 20 {% else %}
21 21 <div class="project-image-placeholder" id="image-placeholder">
@@ -52,7 +52,7 @@
52 52 <div class="project-card-preview">
53 53 <div class="preview-cover" id="preview-cover">
54 54 {% if let Some(url) = cover_image_url %}
55 - <img src="{{ url }}" alt="Preview">
55 + <img src="{{ url }}" alt="Preview" style="width: 100%; aspect-ratio: 1 / 1; object-fit: cover;">
56 56 {% else %}
57 57 <div class="preview-cover-empty">No image</div>
58 58 {% endif %}
@@ -162,12 +162,12 @@
162 162 var div = document.createElement('div');
163 163 div.className = 'project-image-current';
164 164 div.id = 'image-current';
165 - div.innerHTML = '<img src="' + url + '" alt="Project image">';
165 + div.innerHTML = '<img src="' + url + '" alt="Project image" style="width: 100%; aspect-ratio: 1 / 1; object-fit: cover;">';
166 166 dropzone.insertBefore(div, fileInput);
167 167 currentImg = div;
168 168 }
169 169 var preview = document.getElementById('preview-cover');
170 - preview.innerHTML = '<img src="' + url + '" alt="Preview">';
170 + preview.innerHTML = '<img src="' + url + '" alt="Preview" style="width: 100%; aspect-ratio: 1 / 1; object-fit: cover;">';
171 171 }
172 172
173 173 function showError(message) {