Skip to content

Commit fe29dc1

Browse files
committed
Add canonical-site-url setting
Based on the conversation in #1238, this implements the suggestion by markhildreth to implement such a setting. I've additionally infixed `-site-` to highlight the relationship with the `site-url`, and to distinguish it from the canonical URL as it occurs in a page.
1 parent 97d9078 commit fe29dc1

File tree

8 files changed

+67
-0
lines changed

8 files changed

+67
-0
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
# Changelog
22

3+
## Upcoming release
4+
5+
### Added
6+
- Added [`canonical-site-url`](https://rust-lang.github.io/mdBook/format/configuration/renderers.html?highlight=canonical-site-url#html-renderer-options) setting, to set `<link rel="canonical">` in the HTML output of each page.
7+
38
## mdBook 0.4.52
49
[v0.4.51...v0.4.52](https://github.com/rust-lang/mdBook/compare/v0.4.51...v0.4.52)
510

crates/mdbook-core/src/config.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -580,6 +580,8 @@ pub struct HtmlConfig {
580580
pub input_404: Option<String>,
581581
/// Absolute url to site, used to emit correct paths for the 404 page, which might be accessed in a deeply nested directory
582582
pub site_url: Option<String>,
583+
/// Canonical site url, used to emit <link rel="canonical"> tags in the HTML.
584+
pub canonical_site_url: Option<String>,
583585
/// The DNS subdomain or apex domain at which your book will be hosted. This
584586
/// string will be written to a file named CNAME in the root of your site,
585587
/// as required by GitHub Pages (see [*Managing a custom domain for your
@@ -630,6 +632,7 @@ impl Default for HtmlConfig {
630632
edit_url_template: None,
631633
input_404: None,
632634
site_url: None,
635+
canonical_site_url: None,
633636
cname: None,
634637
live_reload_endpoint: None,
635638
redirect: HashMap::new(),

crates/mdbook-html/front-end/templates/index.hbs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@
1010
{{#if base_url}}
1111
<base href="{{ base_url }}">
1212
{{/if}}
13+
{{#if canonical_url}}
14+
<link rel="canonical" href="{{ canonical_url }}">
15+
{{/if}}
1316

1417

1518
<!-- Custom HTML head -->

crates/mdbook-html/src/html_handlebars/hbs_renderer.rs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,13 @@ impl HtmlHandlebars {
7474
.to_str()
7575
.with_context(|| "Could not convert path to str")?;
7676
let filepath = Path::new(&ctx_path).with_extension("html");
77+
let filepath_str = filepath
78+
.to_str()
79+
.with_context(|| format!("Could not convert path to str: {}", filepath.display()))?;
80+
let canonical_url = ctx.html_config.canonical_site_url.map(|canon_url| {
81+
let canon_url = canon_url.as_str().trim_end_matches('/');
82+
format!("{}/{}", canon_url, self.clean_path(filepath_str))
83+
});
7784

7885
// "print.html" is used for the print page.
7986
if path == Path::new("print.md") {
@@ -95,6 +102,8 @@ impl HtmlHandlebars {
95102
};
96103

97104
ctx.data.insert("path".to_owned(), json!(path));
105+
ctx.data
106+
.insert("canonical_url".to_owned(), json!(canonical_url));
98107
ctx.data.insert("content".to_owned(), json!(content));
99108
ctx.data.insert("chapter_title".to_owned(), json!(ch.name));
100109
ctx.data.insert("title".to_owned(), json!(title));
@@ -325,6 +334,15 @@ impl HtmlHandlebars {
325334

326335
Ok(())
327336
}
337+
338+
/// Strips `index.html` from the end of a path, if it exists.
339+
fn clean_path(&self, path: &str) -> String {
340+
if path == "index.html" || path.ends_with("/index.html") {
341+
path[..path.len() - 10].to_string()
342+
} else {
343+
path.to_string()
344+
}
345+
}
328346
}
329347

330348
impl Renderer for HtmlHandlebars {

guide/src/format/configuration/renderers.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,12 @@ The following configuration options are available:
164164
navigation links and script/css imports in the 404 file work correctly, even when accessing
165165
urls in subdirectories. Defaults to `/`. If `site-url` is set,
166166
make sure to use document relative links for your assets, meaning they should not start with `/`.
167+
- **canonical-site-url:** Set the canonical URL for the book, which is used by
168+
search engines to determine the primary URL for the content. Use this when
169+
your site is deployed at multiple URLs. For example, when you have site
170+
deployments for a range of versions, you can point all of them to the URL for
171+
the latest version. Without this, your content may be penalized for
172+
duplication, and visitors may be directed to an outdated version of the book.
167173
- **cname:** The DNS subdomain or apex domain at which your book will be hosted.
168174
This string will be written to a file named CNAME in the root of your site, as
169175
required by GitHub Pages (see [*Managing a custom domain for your GitHub Pages

tests/testsuite/rendering.rs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,3 +41,25 @@ fn first_chapter_is_copied_as_index_even_if_not_first_elem() {
4141
]],
4242
);
4343
}
44+
45+
// Checks that a canonical URL is generated correctly.
46+
#[test]
47+
fn canonical_url() {
48+
BookTest::from_dir("rendering/canonical_url")
49+
.check_file_contains(
50+
"book/index.html",
51+
"<link rel=\"canonical\" href=\"https://example.com/test/\">",
52+
)
53+
.check_file_contains(
54+
"book/canonical_url.html",
55+
"<link rel=\"canonical\" href=\"https://example.com/test/canonical_url.html\">",
56+
)
57+
.check_file_contains(
58+
"book/nested/page.html",
59+
"<link rel=\"canonical\" href=\"https://example.com/test/nested/page.html\">",
60+
)
61+
.check_file_contains(
62+
"book/nested/index.html",
63+
"<link rel=\"canonical\" href=\"https://example.com/test/nested/\">",
64+
);
65+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
[book]
2+
title = "canonical_url test"
3+
4+
[output.html]
5+
# trailing slash is not necessary or recommended, but tested here
6+
canonical-site-url = "https://example.com/test/"
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
- [Intro](README.md)
2+
- [Canonical URL](canonical_url.md)
3+
- [Nested Page](nested/page.md)
4+
- [Nested Index](nested/index.md)

0 commit comments

Comments
 (0)