summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorKornel <kornel@geekhood.net>2019-04-14 16:20:08 +0100
committerKornel <kornel@geekhood.net>2019-04-14 16:26:30 +0100
commitd692439dcc2d17272ec3f83564818efa8e23a055 (patch)
tree54f13f58dbc4d3477818e1cfba518db21de8824e
parent35e266efa5c021e245a4fa2639334f24227f94a4 (diff)
404 page
-rw-r--r--front_end/src/front_end.rs2
-rw-r--r--front_end/src/not_found_page.rs83
-rw-r--r--front_end/templates/not_found.rs.html65
-rw-r--r--server/src/main.rs26
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)
})