larust_core/
error_pages.rs1use std::sync::OnceLock;
17
18pub struct ErrorPages {
25 pub not_found: String,
26 pub internal: String,
27}
28
29static PAGES: OnceLock<ErrorPages> = OnceLock::new();
30
31pub(crate) fn set(pages: ErrorPages) {
35 let _ = PAGES.set(pages);
36}
37
38pub(crate) fn not_found_html() -> String {
39 PAGES
40 .get()
41 .map(|p| p.not_found.clone())
42 .unwrap_or_else(default_not_found_html)
43}
44
45pub(crate) fn internal_html() -> String {
46 PAGES
47 .get()
48 .map(|p| p.internal.clone())
49 .unwrap_or_else(default_internal_html)
50}
51
52pub fn default_not_found_html() -> String {
56 page_shell(
57 "404",
58 "Page not found",
59 "The page you're looking for doesn't exist, or it's moved somewhere else.",
60 )
61}
62
63pub fn default_internal_html() -> String {
66 page_shell(
67 "500",
68 "Something went wrong",
69 "An unexpected error occurred on our end. We're looking into it.",
70 )
71}
72
73fn page_shell(status: &str, title: &str, message: &str) -> String {
85 let html = format!(
86 r#"<!doctype html>
87<html lang="en">
88<head>
89<meta charset="utf-8">
90<meta name="viewport" content="width=device-width, initial-scale=1">
91<title>{status} {title}</title>
92<style>
93 :root {{
94 --ink: #202124; --muted: #6b6d73; --paper: #fffdf9; --canvas: #f4f0e8;
95 --line: #e4ddd2; --brand: #f4513d; --brand-dark: #cf3628;
96 }}
97 @media (prefers-color-scheme: dark) {{
98 :root {{
99 --ink: #f6f1e8; --muted: #b8b1a8; --paper: #272522; --canvas: #181716;
100 --line: #45413b; --brand: #ff735f; --brand-dark: #ff8a79;
101 }}
102 }}
103 * {{ box-sizing: border-box; }}
104 body {{
105 margin: 0; min-height: 100vh; display: flex; flex-direction: column;
106 color: var(--ink); background: var(--canvas);
107 font-family: Inter, ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
108 line-height: 1.5;
109 }}
110 a {{ color: inherit; }}
111 main {{
112 flex: 1; display: flex; flex-direction: column; align-items: center;
113 justify-content: center; text-align: center; padding: 40px 20px;
114 }}
115 .brand {{
116 display: inline-flex; gap: 10px; align-items: center; margin-bottom: 32px;
117 font-size: 1.15rem; font-weight: 800; letter-spacing: -.04em; text-decoration: none;
118 }}
119 .brand-mark {{
120 display: grid; place-items: center; width: 30px; height: 30px; color: #fff;
121 background: var(--brand); border-radius: 9px 9px 9px 2px;
122 font-family: ui-monospace, monospace; font-size: .9rem;
123 }}
124 .status {{
125 font-size: clamp(3rem, 10vw, 5rem); font-weight: 800; letter-spacing: -.03em;
126 color: var(--brand); margin: 0;
127 }}
128 h1 {{ font-size: 1.5rem; margin: 8px 0 12px; }}
129 p.message {{ color: var(--muted); max-width: 40ch; margin: 0 0 28px; }}
130 .home-link {{
131 display: inline-block; padding: 12px 22px; border-radius: 10px; font-weight: 700;
132 color: #fff; background: var(--brand); text-decoration: none;
133 }}
134 .home-link:hover {{ background: var(--brand-dark); }}
135 footer {{
136 padding: 24px 0 34px; text-align: center; color: var(--muted);
137 font-size: .82rem; border-top: 1px solid var(--line);
138 }}
139 .wallaby {{ color: var(--brand); font-weight: 700; }}
140</style>
141</head>
142<body>
143<main>
144 <a class="brand" href="/"><span class="brand-mark">>_</span><span>larust</span></a>
145 <p class="status">{status}</p>
146 <h1>{title}</h1>
147 <p class="message">{message}</p>
148 <a class="home-link" href="/">Go back home</a>
149</main>
150<footer>Larust by <span class="wallaby">Wallaby Designs</span></footer>
151</body>
152</html>"#
153 );
154 if dev_reload_enabled() {
155 inject_dev_reload_script(html)
156 } else {
157 html
158 }
159}
160
161fn dev_reload_enabled() -> bool {
171 static ENABLED: OnceLock<bool> = OnceLock::new();
172 *ENABLED.get_or_init(|| std::env::var_os("LARUST_DEV_RELOAD").is_some())
173}
174
175const DEV_RELOAD_SCRIPT: &str = r#"<script>
194(function () {
195 var opened = false;
196 var es = new EventSource('/__larust_dev');
197 es.onopen = function () {
198 // `location.replace`, not `location.reload` - `reload()` reissues the
199 // request with whatever HTTP method got this page here in the first
200 // place. A page that's the direct render of a POST (no redirect after
201 // it - a real, common pattern) would then silently *resubmit* that
202 // POST the instant any unrelated file save elsewhere triggers a
203 // rebuild, with a CSRF token that's had no reason to still be valid -
204 // landing on a bare, unstyled 419 with no way back. `replace` always
205 // performs a plain GET navigation instead, the standard technique for
206 // getting fresh content without resubmitting.
207 if (opened) location.replace(location.href);
208 opened = true;
209 };
210 es.addEventListener('build-status', function (event) {
211 var banner = document.getElementById('__larust_build_banner');
212 if (!banner) {
213 banner = document.createElement('div');
214 banner.id = '__larust_build_banner';
215 // Fixed to the *bottom*, not the top - a top banner would cover a
216 // page's own nav/header and get in the way of actually using the
217 // still-serving old page while a rebuild is in flight, defeating the
218 // whole point of showing it non-disruptively in the first place.
219 banner.style.cssText = 'position:fixed;bottom:0;left:0;right:0;z-index:2147483647;' +
220 'padding:.6rem 1rem;color:#fff;background:#f4513d;' +
221 'font:600 13px/1.4 system-ui,sans-serif;text-align:center;';
222 document.body.append(banner);
223 }
224 banner.textContent = event.data === 'failed'
225 ? 'Build failed - check your terminal'
226 : 'Rebuilding… this page may be stale until the new build is live.';
227 });
228})();
229</script>"#;
230
231fn inject_dev_reload_script(html: String) -> String {
238 match html.find("</body>") {
239 Some(index) => {
240 let mut out = String::with_capacity(html.len() + DEV_RELOAD_SCRIPT.len());
241 out.push_str(&html[..index]);
242 out.push_str(DEV_RELOAD_SCRIPT);
243 out.push_str(&html[index..]);
244 out
245 }
246 None => html + DEV_RELOAD_SCRIPT,
247 }
248}
249
250#[cfg(test)]
251mod tests {
252 use super::*;
253
254 #[test]
255 fn default_not_found_html_carries_the_404_status_and_brand_mark() {
256 let html = default_not_found_html();
257 assert!(html.contains("404"));
258 assert!(html.contains("Page not found"));
259 assert!(html.contains("larust"));
260 }
261
262 #[test]
263 fn default_internal_html_carries_the_500_status_and_no_leaked_detail() {
264 let html = default_internal_html();
265 assert!(html.contains("500"));
266 assert!(html.contains("Something went wrong"));
267 assert!(!html.contains("Caused by"));
270 }
271
272 #[test]
282 fn inject_dev_reload_script_is_placed_before_closing_body_tag() {
283 let html = "<html><body><h1>hi</h1></body></html>".to_string();
284 let out = inject_dev_reload_script(html);
285 assert!(out.contains("EventSource('/__larust_dev')"));
286 let script_pos = out.find("<script>").unwrap();
287 let body_close_pos = out.find("</body>").unwrap();
288 assert!(script_pos < body_close_pos);
289 assert!(out.ends_with("</html>"));
290 }
291
292 #[test]
293 fn inject_dev_reload_script_listens_for_a_named_build_status_event() {
294 let html = "<html><body></body></html>".to_string();
295 let out = inject_dev_reload_script(html);
296 assert!(out.contains("addEventListener('build-status'"));
297 assert!(out.contains("__larust_build_banner"));
298 assert!(out.contains("Build failed"));
299 }
300}