max / makenotwork
- Co-Authored-By
- Claude Opus 5 (1M context) <noreply@anthropic.com>
7 files changed,
+88 insertions,
-9 deletions
| @@ -100,7 +100,7 @@ | |||
| 100 | 100 | </div> | |
| 101 | 101 | ||
| 102 | 102 | <div class="form-group"> | |
| 103 | - | <label for="ai_tier">AI Classification <a href="/docs/ai" class="text-xs dimmed">What's this?</a></label> | |
| 103 | + | <label for="ai_tier">AI Classification <a href="/docs/generative-ai" class="text-xs dimmed">What's this?</a></label> | |
| 104 | 104 | <select id="ai_tier" name="ai_tier"> | |
| 105 | 105 | <option value="handmade" {% if item.ai_tier == "handmade" %}selected{% endif %}>Handmade: no AI tools used</option> | |
| 106 | 106 | <option value="assisted" {% if item.ai_tier == "assisted" %}selected{% endif %}>AI-Assisted: AI tools with human creation</option> |
| @@ -1,11 +1,11 @@ | |||
| 1 | - | <div class="tab-docs"><a href="/docs/developer/synckit">Docs: Cloud Sync →</a></div> | |
| 1 | + | <div class="tab-docs"><a href="/docs/synckit">Docs: Cloud Sync →</a></div> | |
| 2 | 2 | ||
| 3 | 3 | <div class="content-section"> | |
| 4 | 4 | <div class="section-header"> | |
| 5 | 5 | <h2 class="subsection-title">Cloud Sync</h2> | |
| 6 | 6 | </div> | |
| 7 | 7 | ||
| 8 | - | <p class="form-hint proj-synckit-intro">Cloud Sync lets your desktop or mobile apps sync data with Makenotwork using end-to-end encryption. If you're building an app that stores user data, create a sync app here to get an API key your app uses to connect. <a href="/docs/developer/synckit">Learn more</a></p> | |
| 8 | + | <p class="form-hint proj-synckit-intro">Cloud Sync lets your desktop or mobile apps sync data with Makenotwork using end-to-end encryption. If you're building an app that stores user data, create a sync app here to get an API key your app uses to connect. <a href="/docs/synckit">Learn more</a></p> | |
| 9 | 9 | <p class="form-hint proj-synckit-intro-secondary">Most creators don't need this: it's for developers who ship their own software through Makenotwork.</p> | |
| 10 | 10 | ||
| 11 | 11 | <!-- Create new app form --> |
| @@ -1,4 +1,4 @@ | |||
| 1 | - | <div class="tab-docs"><a href="/docs/account">Docs: Account →</a></div> | |
| 1 | + | <div class="tab-docs"><a href="/docs/account-lifecycle">Docs: Account →</a></div> | |
| 2 | 2 | ||
| 3 | 3 | {% if !moderation_active.is_empty() || !moderation_history.is_empty() %} | |
| 4 | 4 | <div class="form-section"> |
| @@ -35,7 +35,7 @@ | |||
| 35 | 35 | {% if let Some(period_end) = creator_period_end %} | |
| 36 | 36 | <p class="muted text-sm">Current period ends {{ period_end }}</p> | |
| 37 | 37 | {% endif %} | |
| 38 | - | <p class="muted text-sm mt-peer">{{ storage_total }} of {{ storage_max }} storage used ({{ storage_pct }}%). <a href="/docs/guide/tiers" class="muted">Plan limits →</a></p> | |
| 38 | + | <p class="muted text-sm mt-peer">{{ storage_total }} of {{ storage_max }} storage used ({{ storage_pct }}%). <a href="/docs/tiers" class="muted">Plan limits →</a></p> | |
| 39 | 39 | </div> | |
| 40 | 40 | {% else %} | |
| 41 | 41 | <p class="muted text-sm mb-section"> | |
| @@ -55,7 +55,7 @@ | |||
| 55 | 55 | <div class="creator-tier-grid mb-section"> | |
| 56 | 56 | {% for card in tier_cards %} | |
| 57 | 57 | <div class="creator-tier-card"> | |
| 58 | - | <div class="creator-tier-name"><a href="/docs/guide/tiers" class="creator-tier-link">{{ card.label }}</a></div> | |
| 58 | + | <div class="creator-tier-name"><a href="/docs/tiers" class="creator-tier-link">{{ card.label }}</a></div> | |
| 59 | 59 | {% if show_founder_rate %} | |
| 60 | 60 | <div class="meta mb-peer"><strong>${{ card.founder_monthly }}/mo</strong> <span class="strike-dim">${{ card.standard_monthly }}</span></div> | |
| 61 | 61 | <div class="meta creator-tier-sub mb-peer">or ${{ card.founder_annual }}/yr (10% off)</div> | |
| @@ -81,7 +81,7 @@ | |||
| 81 | 81 | </div> | |
| 82 | 82 | {% endfor %} | |
| 83 | 83 | </div> | |
| 84 | - | <p class="text-center mb-section"><a href="/docs/guide/tiers" class="text-sm">Full tier details →</a></p> | |
| 84 | + | <p class="text-center mb-section"><a href="/docs/tiers" class="text-sm">Full tier details →</a></p> | |
| 85 | 85 | <p class="text-center text-sm muted mb-section"> | |
| 86 | 86 | Upgrades take effect immediately and are prorated. Downgrades take effect at the next billing period; if you're over the new tier's storage cap, new uploads stay blocked until you're under it, but your existing content stays published either way. | |
| 87 | 87 | </p> |
| @@ -155,7 +155,7 @@ | |||
| 155 | 155 | <button type="button" class="btn-secondary dns-row-copy" | |
| 156 | 156 | data-action="copyElementText" data-arg="dns-value">Copy</button> | |
| 157 | 157 | </div> | |
| 158 | - | <p class="callout-hint">DNS changes can take up to 24 hours to propagate. <a href="/docs/guide/custom-domains">Setup guide</a></p> | |
| 158 | + | <p class="callout-hint">DNS changes can take up to 24 hours to propagate. <a href="/docs/custom-domains">Setup guide</a></p> | |
| 159 | 159 | </div> | |
| 160 | 160 | <div class="profile-domain-actions"> | |
| 161 | 161 | <button class="btn-primary" |
| @@ -3,7 +3,7 @@ | |||
| 3 | 3 | <h2 class="subsection-title">Cloud Sync</h2> | |
| 4 | 4 | </div> | |
| 5 | 5 | ||
| 6 | - | <p class="form-hint mb-section">Cloud Sync lets your desktop or mobile apps sync data with Makenotwork using end-to-end encryption. Each app gets an API key your app uses to connect. <a href="/docs/developer/synckit">Learn more</a></p> | |
| 6 | + | <p class="form-hint mb-section">Cloud Sync lets your desktop or mobile apps sync data with Makenotwork using end-to-end encryption. Each app gets an API key your app uses to connect. <a href="/docs/synckit">Learn more</a></p> | |
| 7 | 7 | <p class="form-hint mb-pane">Most creators don't need this: it's for developers who ship their own software through Makenotwork.</p> | |
| 8 | 8 | ||
| 9 | 9 | <!-- Create new app form --> |
| @@ -320,3 +320,82 @@ | |||
| 320 | 320 | assert_eq!(before, seen.len(), "a slug appears in two subcategories"); | |
| 321 | 321 | } | |
| 322 | 322 | } | |
| 323 | + | ||
| 324 | + | #[cfg(test)] | |
| 325 | + | mod template_link_tests { | |
| 326 | + | /// Every `/docs/...` href in a template must be served. | |
| 327 | + | /// | |
| 328 | + | /// `MNW_CHECK_DOCS` walks the link graph *inside* the doc corpus, so a doc | |
| 329 | + | /// linking a moved page fails the pipeline. Nothing did the same for links | |
| 330 | + | /// pointing *into* the corpus from a template, which is how the landing | |
| 331 | + | /// page's own "Video" link came to be verified by hand rather than by the | |
| 332 | + | /// build. Templates are the higher-traffic direction: `/docs` is one page, | |
| 333 | + | /// the landing page is the front door. | |
| 334 | + | /// | |
| 335 | + | /// Two shapes fail here, and only the first is a typo: | |
| 336 | + | /// - a single-segment slug the loader does not resolve; | |
| 337 | + | /// - any two-segment path, e.g. `/docs/guide/tiers`. The route is | |
| 338 | + | /// `/docs/{slug}` and axum matches exactly one segment, so a | |
| 339 | + | /// section-qualified link never reaches `doc_page` at all. Docs are | |
| 340 | + | /// addressed by bare slug; the section is a display grouping. | |
| 341 | + | #[test] | |
| 342 | + | fn every_template_docs_link_resolves() { | |
| 343 | + | // Registered as exact routes before the `/docs/{slug}` catch-all, so | |
| 344 | + | // they are reachable without being loader slugs. | |
| 345 | + | const NON_SLUG_ROUTES: &[&str] = &[ | |
| 346 | + | "", // /docs, the index | |
| 347 | + | "search.json", // the search payload | |
| 348 | + | "economics", // 301 to /economics; the markdown source is gone | |
| 349 | + | ]; | |
| 350 | + | ||
| 351 | + | let assumptions = crate::site_docs::load_assumptions().expect("assumptions load"); | |
| 352 | + | let docs = crate::site_docs::build_doc_loader(assumptions); | |
| 353 | + | ||
| 354 | + | let re = regex::Regex::new(r"/docs(?:/([a-z0-9][a-z0-9./-]*))?").expect("valid regex"); | |
| 355 | + | ||
| 356 | + | let mut dead: Vec<String> = Vec::new(); | |
| 357 | + | for path in html_templates(concat!(env!("CARGO_MANIFEST_DIR"), "/templates")) { | |
| 358 | + | let body = std::fs::read_to_string(&path).expect("readable template"); | |
| 359 | + | for caps in re.captures_iter(&body) { | |
| 360 | + | let target = caps.get(1).map_or("", |m| m.as_str()); | |
| 361 | + | if NON_SLUG_ROUTES.contains(&target) { | |
| 362 | + | continue; | |
| 363 | + | } | |
| 364 | + | let ok = !target.contains('/') && docs.get(target).is_some(); | |
| 365 | + | if !ok { | |
| 366 | + | let file = path.rsplit('/').next().unwrap_or(&path); | |
| 367 | + | dead.push(format!("{file}: /docs/{target}")); | |
| 368 | + | } | |
| 369 | + | } | |
| 370 | + | } | |
| 371 | + | dead.sort_unstable(); | |
| 372 | + | dead.dedup(); | |
| 373 | + | ||
| 374 | + | assert!( | |
| 375 | + | dead.is_empty(), | |
| 376 | + | "templates link to docs that are not served:\n {}", | |
| 377 | + | dead.join("\n ") | |
| 378 | + | ); | |
| 379 | + | } | |
| 380 | + | ||
| 381 | + | /// Recursive `.html` walk. No `walkdir` in the tree, and this is the only | |
| 382 | + | /// caller. | |
| 383 | + | fn html_templates(root: &str) -> Vec<String> { | |
| 384 | + | let mut out = Vec::new(); | |
| 385 | + | let mut stack = vec![std::path::PathBuf::from(root)]; | |
| 386 | + | while let Some(dir) = stack.pop() { | |
| 387 | + | let Ok(entries) = std::fs::read_dir(&dir) else { | |
| 388 | + | continue; | |
| 389 | + | }; | |
| 390 | + | for entry in entries.filter_map(std::result::Result::ok) { | |
| 391 | + | let p = entry.path(); | |
| 392 | + | if p.is_dir() { | |
| 393 | + | stack.push(p); | |
| 394 | + | } else if p.extension().is_some_and(|e| e == "html") { | |
| 395 | + | out.push(p.to_string_lossy().into_owned()); | |
| 396 | + | } | |
| 397 | + | } | |
| 398 | + | } | |
| 399 | + | out | |
| 400 | + | } | |
| 401 | + | } |