diff --git a/src/test/fakes.rs b/src/test/fakes.rs index 61dff0c35..5d59a066d 100644 --- a/src/test/fakes.rs +++ b/src/test/fakes.rs @@ -245,6 +245,11 @@ impl<'a> FakeRelease<'a> { self } + pub(crate) fn documentation_url(mut self, documentation: Option) -> Self { + self.package.documentation = documentation; + self + } + /// Returns the release_id pub(crate) fn create(mut self) -> Result { use std::fs; diff --git a/src/web/crate_details.rs b/src/web/crate_details.rs index 79186f21d..890b7d080 100644 --- a/src/web/crate_details.rs +++ b/src/web/crate_details.rs @@ -38,7 +38,7 @@ pub struct CrateDetails { pub(crate) metadata: MetaData, is_library: bool, license: Option, - documentation_url: Option, + pub(crate) documentation_url: Option, total_items: Option, documented_items: Option, total_items_needing_examples: Option, diff --git a/src/web/rustdoc.rs b/src/web/rustdoc.rs index bf5e23e2a..d9e031eef 100644 --- a/src/web/rustdoc.rs +++ b/src/web/rustdoc.rs @@ -195,6 +195,7 @@ pub fn rustdoc_redirector_handler(req: &mut Request) -> IronResult { #[derive(Debug, Clone, PartialEq, Serialize)] struct RustdocPage { latest_path: String, + canonical_url: String, permalink_path: String, latest_version: String, target: String, @@ -483,6 +484,27 @@ pub fn rustdoc_html_server_handler(req: &mut Request) -> IronResult { let latest_path = format!("/crate/{}/latest{}{}", name, target_redirect, query_string); + // Set the canonical URL for search engines to the `/latest/` page on docs.rs. + // For crates with a documentation URL, where that URL doesn't point at docs.rs, + // omit the canonical link to avoid penalizing external documentation. + // Note: The URL this points to may not exist. For instance, if we're rendering + // `struct Foo` in version 0.1.0 of a crate, and version 0.2.0 of that crate removes + // `struct Foo`, this will point at a 404. That's fine: search engines will crawl + // the target and will not canonicalize to a URL that doesn't exist. + let canonical_url = if krate.documentation_url.is_none() + || krate + .documentation_url + .as_ref() + .unwrap() + .starts_with("https://docs.rs/") + { + // Don't include index.html in the canonical URL. + let canonical_path = inner_path.replace("index.html", ""); + format!("https://docs.rs/{}/latest/{}", name, canonical_path) + } else { + "".to_string() + }; + metrics .recently_accessed_releases .record(krate.crate_id, krate.release_id, target); @@ -496,6 +518,7 @@ pub fn rustdoc_html_server_handler(req: &mut Request) -> IronResult { rendering_time.step("rewrite html"); RustdocPage { latest_path, + canonical_url, permalink_path, latest_version, target, @@ -1980,4 +2003,64 @@ mod test { Ok(()) }) } + + #[test] + fn canonical_url() { + wrapper(|env| { + env.fake_release() + .name("dummy-dash") + .version("0.1.0") + .documentation_url(Some("http://example.com".to_string())) + .rustdoc_file("dummy_dash/index.html") + .create()?; + + env.fake_release() + .name("dummy-docs") + .version("0.1.0") + .documentation_url(Some("https://docs.rs/foo".to_string())) + .rustdoc_file("dummy_docs/index.html") + .create()?; + + env.fake_release() + .name("dummy-nodocs") + .version("0.1.0") + .documentation_url(None) + .rustdoc_file("dummy_nodocs/index.html") + .rustdoc_file("dummy_nodocs/struct.Foo.html") + .create()?; + + let web = env.frontend(); + + assert!(!web + .get("/dummy-dash/0.1.0/dummy_dash/") + .send()? + .text()? + .contains("rel=\"canonical\""),); + + assert!(web + .get("/dummy-docs/0.1.0/dummy_docs/") + .send()? + .text()? + .contains( + "" + ),); + + assert!( + web + .get("/dummy-nodocs/0.1.0/dummy_nodocs/") + .send()? + .text()? + .contains(""), + ); + + assert!( + web + .get("/dummy-nodocs/0.1.0/dummy_nodocs/struct.Foo.html") + .send()? + .text()? + .contains(""), + ); + Ok(()) + }) + } } diff --git a/templates/rustdoc/head.html b/templates/rustdoc/head.html index f8f8cb611..16523abc4 100644 --- a/templates/rustdoc/head.html +++ b/templates/rustdoc/head.html @@ -3,4 +3,8 @@ + {%- if canonical_url -%} + + {%- endif -%} +