|
| 1 | +use axum::{ |
| 2 | + body::Body, |
| 3 | + http::{HeaderMap, StatusCode}, |
| 4 | + response::Html, |
| 5 | +}; |
| 6 | +use fmterr::fmt_err; |
| 7 | +use perseus::{ |
| 8 | + errors::err_to_status_code, |
| 9 | + internal::{ |
| 10 | + get_path_prefix_server, |
| 11 | + i18n::{TranslationsManager, Translator}, |
| 12 | + router::{match_route_atomic, RouteInfoAtomic, RouteVerdictAtomic}, |
| 13 | + serve::{ |
| 14 | + build_error_page, get_page_for_template, get_path_slice, GetPageProps, HtmlShell, |
| 15 | + ServerOptions, |
| 16 | + }, |
| 17 | + }, |
| 18 | + stores::{ImmutableStore, MutableStore}, |
| 19 | + ErrorPages, Request, SsrNode, |
| 20 | +}; |
| 21 | +use std::{collections::HashMap, rc::Rc, sync::Arc}; |
| 22 | + |
| 23 | +/// Builds on the internal Perseus primitives to provide a utility function that returns a `Response` automatically. |
| 24 | +fn return_error_page( |
| 25 | + url: &str, |
| 26 | + status: u16, |
| 27 | + // This should already have been transformed into a string (with a source chain etc.) |
| 28 | + err: &str, |
| 29 | + translator: Option<Rc<Translator>>, |
| 30 | + error_pages: &ErrorPages<SsrNode>, |
| 31 | + html_shell: &HtmlShell, |
| 32 | +) -> (StatusCode, HeaderMap, Html<String>) { |
| 33 | + let html = build_error_page(url, status, err, translator, error_pages, html_shell); |
| 34 | + ( |
| 35 | + StatusCode::from_u16(status).unwrap(), |
| 36 | + HeaderMap::new(), |
| 37 | + Html(html), |
| 38 | + ) |
| 39 | +} |
| 40 | + |
| 41 | +/// The handler for calls to any actual pages (first-time visits), which will render the appropriate HTML and then interpolate it into |
| 42 | +/// the app shell. |
| 43 | +#[allow(clippy::too_many_arguments)] // As for `page_data_handler`, we don't have a choice |
| 44 | +pub async fn initial_load_handler<M: MutableStore, T: TranslationsManager>( |
| 45 | + http_req: perseus::http::Request<Body>, |
| 46 | + opts: Arc<ServerOptions>, |
| 47 | + html_shell: Arc<HtmlShell>, |
| 48 | + render_cfg: Arc<HashMap<String, String>>, |
| 49 | + immutable_store: Arc<ImmutableStore>, |
| 50 | + mutable_store: Arc<M>, |
| 51 | + translations_manager: Arc<T>, |
| 52 | + global_state: Arc<Option<String>>, |
| 53 | +) -> (StatusCode, HeaderMap, Html<String>) { |
| 54 | + let path = http_req.uri().path().to_string(); |
| 55 | + let http_req = Request::from_parts(http_req.into_parts().0, ()); |
| 56 | + |
| 57 | + let templates = &opts.templates_map; |
| 58 | + let error_pages = &opts.error_pages; |
| 59 | + let path_slice = get_path_slice(&path); |
| 60 | + // Create a closure to make returning error pages easier (most have the same data) |
| 61 | + let html_err = |status: u16, err: &str| { |
| 62 | + return return_error_page(&path, status, err, None, error_pages, html_shell.as_ref()); |
| 63 | + }; |
| 64 | + |
| 65 | + // Run the routing algorithms on the path to figure out which template we need |
| 66 | + let verdict = match_route_atomic(&path_slice, render_cfg.as_ref(), templates, &opts.locales); |
| 67 | + match verdict { |
| 68 | + // If this is the outcome, we know that the locale is supported and the like |
| 69 | + // Given that all this is valid from the client, any errors are 500s |
| 70 | + RouteVerdictAtomic::Found(RouteInfoAtomic { |
| 71 | + path, // Used for asset fetching, this is what we'd get in `page_data` |
| 72 | + template, // The actual template to use |
| 73 | + locale, |
| 74 | + was_incremental_match, |
| 75 | + }) => { |
| 76 | + // Actually render the page as we would if this weren't an initial load |
| 77 | + let page_data = get_page_for_template( |
| 78 | + GetPageProps::<M, T> { |
| 79 | + raw_path: &path, |
| 80 | + locale: &locale, |
| 81 | + was_incremental_match, |
| 82 | + req: http_req, |
| 83 | + global_state: &global_state, |
| 84 | + immutable_store: &immutable_store, |
| 85 | + mutable_store: &mutable_store, |
| 86 | + translations_manager: &translations_manager, |
| 87 | + }, |
| 88 | + template, |
| 89 | + ) |
| 90 | + .await; |
| 91 | + let page_data = match page_data { |
| 92 | + Ok(page_data) => page_data, |
| 93 | + // We parse the error to return an appropriate status code |
| 94 | + Err(err) => { |
| 95 | + return html_err(err_to_status_code(&err), &fmt_err(&err)); |
| 96 | + } |
| 97 | + }; |
| 98 | + |
| 99 | + let final_html = html_shell |
| 100 | + .as_ref() |
| 101 | + .clone() |
| 102 | + .page_data(&page_data, &global_state) |
| 103 | + .to_string(); |
| 104 | + |
| 105 | + // http_res.content_type("text/html"); |
| 106 | + // Generate and add HTTP headers |
| 107 | + let mut header_map = HeaderMap::new(); |
| 108 | + for (key, val) in template.get_headers(page_data.state) { |
| 109 | + header_map.insert(key.unwrap(), val); |
| 110 | + } |
| 111 | + |
| 112 | + (StatusCode::OK, header_map, Html(final_html)) |
| 113 | + } |
| 114 | + // For locale detection, we don't know the user's locale, so there's not much we can do except send down the app shell, which will do the rest and fetch from `.perseus/page/...` |
| 115 | + RouteVerdictAtomic::LocaleDetection(path) => { |
| 116 | + // We use a `302 Found` status code to indicate a redirect |
| 117 | + // We 'should' generate a `Location` field for the redirect, but it's not RFC-mandated, so we can use the app shell |
| 118 | + ( |
| 119 | + StatusCode::FOUND, |
| 120 | + HeaderMap::new(), |
| 121 | + Html( |
| 122 | + html_shell |
| 123 | + .as_ref() |
| 124 | + .clone() |
| 125 | + .locale_redirection_fallback( |
| 126 | + // We'll redirect the user to the default locale |
| 127 | + &format!( |
| 128 | + "{}/{}/{}", |
| 129 | + get_path_prefix_server(), |
| 130 | + opts.locales.default, |
| 131 | + path |
| 132 | + ), |
| 133 | + ) |
| 134 | + .to_string(), |
| 135 | + ), |
| 136 | + ) |
| 137 | + } |
| 138 | + RouteVerdictAtomic::NotFound => html_err(404, "page not found"), |
| 139 | + } |
| 140 | +} |
0 commit comments