Rust library to generate static websites
5
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: implement other strategies

+227 -22
+22 -17
crates/maudit/js/prefetch/hover.ts
··· 2 2 3 3 const listenedAnchors = new WeakSet<HTMLAnchorElement>(); 4 4 5 + // TODO: Make this configurable, needs rolldown_plugin_replace and stuff 6 + const observeMutations = true; 7 + 5 8 function init() { 6 9 let timeout: ReturnType<typeof setTimeout> | null = null; 7 10 ··· 28 31 anchor.addEventListener("mouseleave", handleHoverOut, { passive: true }); 29 32 } 30 33 }; 31 - 32 - document.addEventListener("DOMContentLoaded", attachListeners); 33 - 34 - // Re-attach listeners for dynamically added content 35 - const observer = new MutationObserver((mutations) => { 36 - for (const mutation of mutations) { 37 - if (mutation.type === "childList" && mutation.addedNodes.length > 0) { 38 - attachListeners(); 39 - break; 40 - } 41 - } 42 - }); 43 - 44 - observer.observe(document.body, { 45 - childList: true, 46 - subtree: true, 47 - }); 48 34 49 35 function handleHoverIn(e: Event) { 50 36 const target = e.target as HTMLAnchorElement; ··· 67 53 clearTimeout(timeout); 68 54 timeout = null; 69 55 } 56 + } 57 + 58 + document.addEventListener("DOMContentLoaded", attachListeners); 59 + 60 + if (observeMutations) { 61 + // Re-attach listeners for dynamically added content 62 + const observer = new MutationObserver((mutations) => { 63 + for (const mutation of mutations) { 64 + if (mutation.type === "childList" && mutation.addedNodes.length > 0) { 65 + attachListeners(); 66 + break; 67 + } 68 + } 69 + }); 70 + 71 + observer.observe(document.body, { 72 + childList: true, 73 + subtree: true, 74 + }); 70 75 } 71 76 } 72 77
+51
crates/maudit/js/prefetch/tap.ts
··· 1 + import { prefetch } from "../prefetch.ts"; 2 + 3 + const listenedAnchors = new WeakSet<HTMLAnchorElement>(); 4 + 5 + // TODO: Make this configurable, needs rolldown_plugin_replace and stuff 6 + const observeMutations = true; 7 + 8 + function init() { 9 + // Attach click listeners to all anchors 10 + const attachListeners = () => { 11 + const anchors = document.getElementsByTagName("a"); 12 + for (const anchor of anchors) { 13 + if (listenedAnchors.has(anchor)) continue; 14 + 15 + listenedAnchors.add(anchor); 16 + anchor.addEventListener("click", handleClick, { passive: true }); 17 + } 18 + }; 19 + 20 + document.addEventListener("DOMContentLoaded", attachListeners); 21 + 22 + function handleClick(e: Event) { 23 + const target = e.target as HTMLAnchorElement; 24 + 25 + if (!target.href) { 26 + return; 27 + } 28 + 29 + // Prefetch on click 30 + prefetch(target.href); 31 + } 32 + 33 + if (observeMutations) { 34 + // Re-attach listeners for dynamically added content 35 + const observer = new MutationObserver((mutations) => { 36 + for (const mutation of mutations) { 37 + if (mutation.type === "childList" && mutation.addedNodes.length > 0) { 38 + attachListeners(); 39 + break; 40 + } 41 + } 42 + }); 43 + 44 + observer.observe(document.body, { 45 + childList: true, 46 + subtree: true, 47 + }); 48 + } 49 + } 50 + 51 + init();
+106
crates/maudit/js/prefetch/viewport.ts
··· 1 + import { prefetch } from "../prefetch.ts"; 2 + 3 + const prefetchedAnchors = new WeakSet<HTMLAnchorElement>(); 4 + const observedAnchors = new WeakSet<HTMLAnchorElement>(); 5 + 6 + // TODO: Make this configurable, needs rolldown_plugin_replace and stuff 7 + const observeMutations = true; 8 + 9 + function init() { 10 + let intersectionObserver: IntersectionObserver | null = null; 11 + 12 + function createIntersectionObserver(): IntersectionObserver { 13 + const timeouts = new WeakMap<HTMLAnchorElement, ReturnType<typeof setTimeout>>(); 14 + 15 + return new IntersectionObserver( 16 + (entries) => { 17 + for (const entry of entries) { 18 + const anchor = entry.target as HTMLAnchorElement; 19 + const existingTimeout = timeouts.get(anchor); 20 + 21 + // Clear any pending timeout 22 + if (existingTimeout) { 23 + clearTimeout(existingTimeout); 24 + timeouts.delete(anchor); 25 + } 26 + 27 + if (entry.isIntersecting) { 28 + // Skip if already prefetched 29 + if (prefetchedAnchors.has(anchor)) { 30 + intersectionObserver?.unobserve(anchor); 31 + continue; 32 + } 33 + 34 + // Debounce by 300ms to avoid prefetching during rapid scrolling 35 + const timeout = setTimeout(() => { 36 + timeouts.delete(anchor); 37 + if (!prefetchedAnchors.has(anchor)) { 38 + prefetchedAnchors.add(anchor); 39 + prefetch(anchor.href); 40 + } 41 + intersectionObserver?.unobserve(anchor); 42 + }, 300); 43 + 44 + timeouts.set(anchor, timeout); 45 + } 46 + // If exited viewport, timeout already cleared above 47 + } 48 + }, 49 + { 50 + // Prefetch slightly before element enters viewport for smoother UX 51 + rootMargin: "50px", 52 + // Only trigger when at least 10% of the link is visible 53 + threshold: 0.1, 54 + }, 55 + ); 56 + } 57 + 58 + function observeAnchors() { 59 + intersectionObserver ??= createIntersectionObserver(); 60 + 61 + const anchors = document.getElementsByTagName("a"); 62 + for (const anchor of anchors) { 63 + // Skip if already observing or has no href 64 + if (observedAnchors.has(anchor) || !anchor.href) continue; 65 + 66 + observedAnchors.add(anchor); 67 + intersectionObserver.observe(anchor); 68 + } 69 + } 70 + 71 + // This is always in a type="module" script, so, it'll always run after the DOM is ready 72 + observeAnchors(); 73 + 74 + if (observeMutations) { 75 + // Watch for dynamically added anchors 76 + const mutationObserver = new MutationObserver((mutations) => { 77 + let hasNewAnchors = false; 78 + for (const mutation of mutations) { 79 + if (mutation.type === "childList" && mutation.addedNodes.length > 0) { 80 + // Check if any added nodes are or contain anchors 81 + for (const node of mutation.addedNodes) { 82 + if (node.nodeType === Node.ELEMENT_NODE) { 83 + const element = node as Element; 84 + if (element.tagName === "A" || element.getElementsByTagName("a").length > 0) { 85 + hasNewAnchors = true; 86 + break; 87 + } 88 + } 89 + } 90 + } 91 + if (hasNewAnchors) break; 92 + } 93 + 94 + if (hasNewAnchors) { 95 + observeAnchors(); 96 + } 97 + }); 98 + 99 + mutationObserver.observe(document.body, { 100 + childList: true, 101 + subtree: true, 102 + }); 103 + } 104 + } 105 + 106 + init();
+2
crates/maudit/src/assets/prefetch.rs
··· 19 19 match specifier { 20 20 "maudit:prefetch" => Some(base_dir.join("prefetch.ts")), 21 21 "maudit:prefetch:hover" => Some(base_dir.join("prefetch").join("hover.ts")), 22 + "maudit:prefetch:tap" => Some(base_dir.join("prefetch").join("tap.ts")), 23 + "maudit:prefetch:viewport" => Some(base_dir.join("prefetch").join("viewport.ts")), 22 24 _ => None, 23 25 } 24 26 }
+10 -2
crates/maudit/src/build.rs
··· 12 12 BuildOptions, BuildOutput, 13 13 assets::{self, PrefetchPlugin, RouteAssets, Script, TailwindPlugin, image_cache::ImageCache}, 14 14 build::images::process_image, 15 + build::options::PrefetchStrategy, 15 16 content::ContentSources, 16 17 is_dev, 17 18 logging::print_title, ··· 132 133 133 134 let mut default_scripts = vec![]; 134 135 135 - if options.prefetch { 136 + let prefetch_path = match options.prefetch.strategy { 137 + PrefetchStrategy::None => None, 138 + PrefetchStrategy::Hover => Some("maudit:prefetch:hover"), 139 + PrefetchStrategy::Tap => Some("maudit:prefetch:tap"), 140 + PrefetchStrategy::Viewport => Some("maudit:prefetch:viewport"), 141 + }; 142 + 143 + if let Some(prefetch_path) = prefetch_path { 136 144 let prefetch_script = Script::new( 137 - PathBuf::from("maudit:prefetch:hover"), 145 + PathBuf::from(prefetch_path), 138 146 true, 139 147 { 140 148 use rapidhash::fast::RapidHasher;
+32 -2
crates/maudit/src/build/options.rs
··· 23 23 /// ```rust 24 24 /// use maudit::{ 25 25 /// content_sources, coronate, routes, BuildOptions, BuildOutput, AssetsOptions, 26 + /// PrefetchOptions, PrefetchStrategy, 26 27 /// }; 27 28 /// 28 29 /// fn main() -> Result<BuildOutput, Box<dyn std::error::Error>> { ··· 37 38 /// tailwind_binary_path: "./node_modules/.bin/tailwindcss".into(), 38 39 /// image_cache_dir: ".cache/maudit/images".into(), 39 40 /// ..Default::default() 41 + /// }, 42 + /// prefetch: PrefetchOptions { 43 + /// strategy: PrefetchStrategy::Viewport, 40 44 /// }, 41 45 /// ..Default::default() 42 46 /// }, ··· 58 62 59 63 pub assets: AssetsOptions, 60 64 61 - pub prefetch: bool, 65 + pub prefetch: PrefetchOptions, 62 66 63 67 /// Options for sitemap generation. See [`SitemapOptions`] for configuration. 64 68 pub sitemap: SitemapOptions, 69 + } 70 + 71 + #[derive(Clone, Copy, PartialEq, Eq)] 72 + pub enum PrefetchStrategy { 73 + /// No prefetching 74 + None, 75 + /// Prefetch links when users hover over them (with 80ms delay) 76 + Hover, 77 + /// Prefetch links when users click/tap on them 78 + Tap, 79 + /// Prefetch all links currently visible in the viewport 80 + Viewport, 81 + } 82 + 83 + #[derive(Clone)] 84 + pub struct PrefetchOptions { 85 + /// The prefetch strategy to use 86 + pub strategy: PrefetchStrategy, 87 + } 88 + 89 + impl Default for PrefetchOptions { 90 + fn default() -> Self { 91 + Self { 92 + strategy: PrefetchStrategy::Tap, 93 + } 94 + } 65 95 } 66 96 67 97 impl BuildOptions { ··· 151 181 output_dir: "dist".into(), 152 182 static_dir: "static".into(), 153 183 clean_output_dir: true, 154 - prefetch: true, 184 + prefetch: PrefetchOptions::default(), 155 185 assets: AssetsOptions::default(), 156 186 sitemap: SitemapOptions::default(), 157 187 }
+3 -1
crates/maudit/src/lib.rs
··· 15 15 16 16 // Exports for end-users 17 17 pub use build::metadata::{BuildOutput, PageOutput, StaticAssetOutput}; 18 - pub use build::options::{AssetHashingStrategy, AssetsOptions, BuildOptions}; 18 + pub use build::options::{ 19 + AssetHashingStrategy, AssetsOptions, BuildOptions, PrefetchOptions, PrefetchStrategy, 20 + }; 19 21 pub use sitemap::{ChangeFreq, SitemapOptions}; 20 22 21 23 // Re-export FxHashMap so that macro-generated code can use it without requiring users to add it as a dependency.
+1
tsconfig.json
··· 6 6 "target": "es2020", 7 7 "lib": ["dom", "dom.iterable", "es2020"], 8 8 "moduleResolution": "bundler", 9 + "allowImportingTsExtensions": true, 9 10 10 11 // Other Outputs 11 12 "sourceMap": true,