diff options
-rw-r--r-- | front_end/src/front_end.rs | 2 | ||||
-rw-r--r-- | front_end/src/not_found_page.rs | 83 | ||||
-rw-r--r-- | front_end/templates/not_found.rs.html | 65 | ||||
-rw-r--r-- | server/src/main.rs | 26 |
4 files changed, 169 insertions, 7 deletions
diff --git a/front_end/src/front_end.rs b/front_end/src/front_end.rs index 073deb5..08bd175 100644 --- a/front_end/src/front_end.rs +++ b/front_end/src/front_end.rs @@ -12,10 +12,12 @@ mod cat_page; mod crate_page; mod download_graph; mod home_page; +mod not_found_page; mod iter; mod search_page; mod urler; pub use crate::search_page::*; +pub use crate::not_found_page::*; use crate::crate_page::*; use crate::urler::Urler; diff --git a/front_end/src/not_found_page.rs b/front_end/src/not_found_page.rs new file mode 100644 index 0000000..27f8c79 --- /dev/null +++ b/front_end/src/not_found_page.rs @@ -0,0 +1,83 @@ +use crate::templates; +use crate::Page; +use crate::Urler; +use render_readme::Renderer; +use std::io::Write; + + +pub struct NotFoundPage<'a> { + markup: &'a Renderer, + pub results: &'a [search_index::CrateFound], + pub query: &'a str, +} + +impl NotFoundPage<'_> { + pub fn new<'a>(query: &'a str, results: &'a [search_index::CrateFound], markup: &'a Renderer) -> NotFoundPage<'a> { + NotFoundPage { + query, + markup, + results, + } + } + + pub fn page(&self) -> Page { + + Page { + title: "Crate not found".into(), + description: Some("Error".into()), + item_name: None, + item_description: None, + keywords: None, + created: None, + alternate: None, + alternate_type: None, + canonical: None, + noindex: true, + search_meta: true, + critical_css_data: Some(include_str!("../../style/public/search.css")), + } + } + + /// For color of the version + /// + /// It tries to guess which versions seem "unstable". + /// + /// TODO: Merge with the better version history analysis from the individual crate page. + pub fn version_class(&self, ver: &str) -> &str { + let v = semver::Version::parse(ver).expect("semver"); + match (v.major, v.minor, v.patch, v.is_prerelease()) { + (1..=15, _, _, false) => "stable", + (0, m, p, false) if m >= 2 && p >= 3 => "stable", + (m, ..) if m >= 1 => "okay", + (0, 1, p, _) if p >= 10 => "okay", + (0, 3..=10, p, _) if p > 0 => "okay", + _ => "unstable", + } + } + + /// Nicely rounded number of downloads + /// + /// To show that these numbers are just approximate. + pub fn downloads(&self, num: u64) -> (String, &str) { + match num { + a @ 0..=99 => (format!("{}", a), ""), + a @ 0..=500 => (format!("{}", a / 10 * 10), ""), + a @ 0..=999 => (format!("{}", a / 50 * 50), ""), + a @ 0..=9999 => (format!("{}.{}", a / 1000, a % 1000 / 100), "K"), + a @ 0..=999_999 => (format!("{}", a / 1000), "K"), + a => (format!("{}.{}", a / 1_000_000, a % 1_000_000 / 100_000), "M"), + } + } + + /// Used to render descriptions + pub fn render_markdown_str(&self, s: &str) -> templates::Html<String> { + templates::Html(self.markup.markdown_str(s, false)) + } +} + +pub fn render_404_page(out: &mut dyn Write, query: &str, results: &[search_index::CrateFound], markup: &Renderer) -> Result<(), failure::Error> { + let urler = Urler::new(); + let page = NotFoundPage::new(query, results, markup); + templates::not_found(out, &page, &urler)?; + Ok(()) +} diff --git a/front_end/templates/not_found.rs.html b/front_end/templates/not_found.rs.html new file mode 100644 index 0000000..bd33814 --- /dev/null +++ b/front_end/templates/not_found.rs.html @@ -0,0 +1,65 @@ +@use crate::templates::base; +@use crate::Urler; +@use crate::NotFoundPage; + +@(p: &NotFoundPage, url: &Urler) + +@:base(&p.page(), { + +<header id="serp"> + <div class="inner-col"> + <div class="breadcrumbs"> + <h1><a href="/">Crates.rs</a></h1> + › + 404 + </div> + <form role="search" id=search method="get" action="/search"> + <input placeholder="name, keywords, description" autocapitalize="off" autocorrect="off" autocomplete="off" tabindex="1" type=search value="@p.query" name=q><button type=submit>Search</button> + </form> + <nav> + <ul> + <li class=active>Error</li> + @if !p.results.is_empty() { + <li><a href="@url.search_crates_rs(&p.query)">Search</a> + } + <li><a href="@url.search_ddg(&p.query)">I'm feeling ducky</a></li> + </ul> + </nav> + </div> +</header> +<main id="results"> + <div class="inner-col"> + <p class=notfound>Page not found</p> + @if !p.results.is_empty() { + <p class=tryalso>Here are some crates that mention “@p.query”. You can also try <a href="@url.search_ddg(&p.query)">searching with DuckDuckGo</a>.</p> + <ol> + @for c in p.results.iter() { + <li> + <a href="@url.krate_by_name(&c.crate_name)"><div class=h> + <h4>@c.crate_name</h4> + <p class=desc>@p.render_markdown_str(&c.description)</p> + </div> + <div class=meta> + <span class="version @p.version_class(&c.version)"><span>v</span>@c.version</span> + @if c.monthly_downloads >= 100 { + <span class=downloads title="c.monthly_downloads recent downloads">@if let Some((num,unit)) = Some(p.downloads(c.monthly_downloads)) {@num<b>@unit</b>}</span> + } + </div></a> + </li> + } + </ol> + <p><a href="@url.search_crates_rs(&p.query)">See more results</a> or <a href="/">browse all categories</a>.</p> + } else { + <p>The URL you've followed is invalid, and there is no crate or category named “@p.query”.</p> + <p><a href="/">Browse categories instead</a>.</p> + } + </div> +</main> + +<footer> + <div class="inner-col"> + <p>Search powered by <a href="https://crates.rs/crates/tantivy">tantivy</a>.</p> + <p>Browse <a href="/">all categories</a>. + </div> +</footer> +}) diff --git a/server/src/main.rs b/server/src/main.rs index abea061..601745e 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -115,20 +115,32 @@ fn find_category<'a>(slugs: impl Iterator<Item=&'a str>) -> Option<&'static Cate fn default_handler(req: &HttpRequest<AServerState>) -> Result<HttpResponse> { let path = req.uri().path(); + let state = req.state(); assert!(path.starts_with('/')); if path.ends_with('/') { return Ok(HttpResponse::PermanentRedirect().header("Location", path.trim_end_matches('/')).body("")); } + if let Some(cat) = find_category(path.split('/').skip(1)) { return handle_category(req, cat); } - if let Some(name) = path.split('/').skip(1).next() { - if let Ok(_) = req.state().crates.rich_crate(&Origin::from_crates_io_name(name)) { - return Ok(HttpResponse::PermanentRedirect().header("Location", format!("/crates/{}", name)).body("")); - } + + let name = path.trim_matches('/'); + if let Ok(_) = state.crates.rich_crate(&Origin::from_crates_io_name(name)) { + return Ok(HttpResponse::PermanentRedirect().header("Location", format!("/crates/{}", name)).body("")); } - Ok(HttpResponse::NotFound().content_type("text/plain;charset=UTF-8").body("404\n")) + let query = path.chars().map(|c| if c.is_alphanumeric() {c} else {' '}).take(100).collect::<String>(); + let query = query.trim(); + let results = state.index.search(query, 5).unwrap_or_default(); + let mut page: Vec<u8> = Vec::with_capacity(50000); + front_end::render_404_page(&mut page, query, &results, &state.markup)?; + + Ok(HttpResponse::NotFound() + .content_type("text/html;charset=UTF-8") + .content_length(page.len() as u64) + .header("Cache-Control", "public, s-maxage=20, max-age=300, stale-while-revalidate=3600, stale-if-error=3600") + .body(page)) } fn handle_category(req: &HttpRequest<AServerState>, cat: &Category) -> Result<HttpResponse> { @@ -215,10 +227,10 @@ fn handle_keyword(req: &HttpRequest<AServerState>) -> FutureResponse<HttpRespons if !is_alnum(&query) { return Ok((query, None)); } - let mut page: Vec<u8> = Vec::with_capacity(50000); let keyword_query = format!("keywords:\"{}\"", query); let results = state2.index.search(&keyword_query, 100)?; if !results.is_empty() { + let mut page: Vec<u8> = Vec::with_capacity(50000); front_end::render_keyword_page(&mut page, &query, &results, &state2.markup)?; Ok((query, Some(page))) } else { @@ -269,8 +281,8 @@ fn handle_search(req: &HttpRequest<AServerState>) -> FutureResponse<HttpResponse state .search_pool .spawn_fn(move || { - let mut page: Vec<u8> = Vec::with_capacity(50000); let results = state2.index.search(&query, 50)?; + let mut page: Vec<u8> = Vec::with_capacity(50000); front_end::render_serp_page(&mut page, &query, &results, &state2.markup)?; Ok(page) }) |