Merge pull request #2747 from ehuss/fragment-redirect
Add support for fragment redirects
This commit is contained in:
commit
a918910a52
18 changed files with 306 additions and 15 deletions
|
|
@ -310,13 +310,21 @@ This is useful when you move, rename, or remove a page to ensure that links to t
|
||||||
[output.html.redirect]
|
[output.html.redirect]
|
||||||
"/appendices/bibliography.html" = "https://rustc-dev-guide.rust-lang.org/appendix/bibliography.html"
|
"/appendices/bibliography.html" = "https://rustc-dev-guide.rust-lang.org/appendix/bibliography.html"
|
||||||
"/other-installation-methods.html" = "../infra/other-installation-methods.html"
|
"/other-installation-methods.html" = "../infra/other-installation-methods.html"
|
||||||
|
|
||||||
|
# Fragment redirects also work.
|
||||||
|
"/some-existing-page.html#old-fragment" = "some-existing-page.html#new-fragment"
|
||||||
|
|
||||||
|
# Fragment redirects also work for deleted pages.
|
||||||
|
"/old-page.html" = "new-page.html"
|
||||||
|
"/old-page.html#old-fragment" = "new-page.html#new-fragment"
|
||||||
```
|
```
|
||||||
|
|
||||||
The table contains key-value pairs where the key is where the redirect file needs to be created, as an absolute path from the build directory, (e.g. `/appendices/bibliography.html`).
|
The table contains key-value pairs where the key is where the redirect file needs to be created, as an absolute path from the build directory, (e.g. `/appendices/bibliography.html`).
|
||||||
The value can be any valid URI the browser should navigate to (e.g. `https://rust-lang.org/`, `/overview.html`, or `../bibliography.html`).
|
The value can be any valid URI the browser should navigate to (e.g. `https://rust-lang.org/`, `/overview.html`, or `../bibliography.html`).
|
||||||
|
|
||||||
This will generate an HTML page which will automatically redirect to the given location.
|
This will generate an HTML page which will automatically redirect to the given location.
|
||||||
Note that the source location does not support `#` anchor redirects.
|
|
||||||
|
When fragment redirects are specified, the page must use JavaScript to redirect to the correct location. This is useful if you rename or move a section header. Fragment redirects work with existing pages and deleted pages.
|
||||||
|
|
||||||
## Markdown Renderer
|
## Markdown Renderer
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -347,6 +347,21 @@
|
||||||
{{/if}}
|
{{/if}}
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
||||||
|
{{#if fragment_map}}
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
const fragmentMap =
|
||||||
|
{{{fragment_map}}}
|
||||||
|
;
|
||||||
|
const target = fragmentMap[window.location.hash];
|
||||||
|
if (target) {
|
||||||
|
let url = new URL(target, window.location.href);
|
||||||
|
window.location.replace(url.href);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
|
|
@ -8,5 +8,29 @@
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<p>Redirecting to... <a href="{{url}}">{{url}}</a>.</p>
|
<p>Redirecting to... <a href="{{url}}">{{url}}</a>.</p>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// This handles redirects that involve fragments.
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
const fragmentMap =
|
||||||
|
{{{fragment_map}}}
|
||||||
|
;
|
||||||
|
const fragment = window.location.hash;
|
||||||
|
if (fragment) {
|
||||||
|
let redirectUrl = "{{url}}";
|
||||||
|
const target = fragmentMap[fragment];
|
||||||
|
if (target) {
|
||||||
|
let url = new URL(target, window.location.href);
|
||||||
|
redirectUrl = url.href;
|
||||||
|
} else {
|
||||||
|
let url = new URL(redirectUrl, window.location.href);
|
||||||
|
url.hash = window.location.hash;
|
||||||
|
redirectUrl = url.href;
|
||||||
|
}
|
||||||
|
window.location.replace(redirectUrl);
|
||||||
|
}
|
||||||
|
// else redirect handled by http-equiv
|
||||||
|
});
|
||||||
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
|
|
@ -111,6 +111,14 @@ impl HtmlHandlebars {
|
||||||
.insert("section".to_owned(), json!(section.to_string()));
|
.insert("section".to_owned(), json!(section.to_string()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let redirects = collect_redirects_for_path(&filepath, &ctx.html_config.redirect)?;
|
||||||
|
if !redirects.is_empty() {
|
||||||
|
ctx.data.insert(
|
||||||
|
"fragment_map".to_owned(),
|
||||||
|
json!(serde_json::to_string(&redirects)?),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Render the handlebars template with the data
|
// Render the handlebars template with the data
|
||||||
debug!("Render template");
|
debug!("Render template");
|
||||||
let rendered = ctx.handlebars.render("index", &ctx.data)?;
|
let rendered = ctx.handlebars.render("index", &ctx.data)?;
|
||||||
|
|
@ -266,15 +274,27 @@ impl HtmlHandlebars {
|
||||||
}
|
}
|
||||||
|
|
||||||
log::debug!("Emitting redirects");
|
log::debug!("Emitting redirects");
|
||||||
|
let redirects = combine_fragment_redirects(redirects);
|
||||||
|
|
||||||
for (original, new) in redirects {
|
for (original, (dest, fragment_map)) in redirects {
|
||||||
log::debug!("Redirecting \"{}\" → \"{}\"", original, new);
|
|
||||||
// Note: all paths are relative to the build directory, so the
|
// Note: all paths are relative to the build directory, so the
|
||||||
// leading slash in an absolute path means nothing (and would mess
|
// leading slash in an absolute path means nothing (and would mess
|
||||||
// up `root.join(original)`).
|
// up `root.join(original)`).
|
||||||
let original = original.trim_start_matches('/');
|
let original = original.trim_start_matches('/');
|
||||||
let filename = root.join(original);
|
let filename = root.join(original);
|
||||||
self.emit_redirect(handlebars, &filename, new)?;
|
if filename.exists() {
|
||||||
|
// This redirect is handled by the in-page fragment mapper.
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if dest.is_empty() {
|
||||||
|
bail!(
|
||||||
|
"redirect entry for `{original}` only has source paths with `#` fragments\n\
|
||||||
|
There must be an entry without the `#` fragment to determine the default \
|
||||||
|
destination."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
log::debug!("Redirecting \"{}\" → \"{}\"", original, dest);
|
||||||
|
self.emit_redirect(handlebars, &filename, &dest, &fragment_map)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
@ -285,23 +305,17 @@ impl HtmlHandlebars {
|
||||||
handlebars: &Handlebars<'_>,
|
handlebars: &Handlebars<'_>,
|
||||||
original: &Path,
|
original: &Path,
|
||||||
destination: &str,
|
destination: &str,
|
||||||
|
fragment_map: &BTreeMap<String, String>,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
if original.exists() {
|
|
||||||
// sanity check to avoid accidentally overwriting a real file.
|
|
||||||
let msg = format!(
|
|
||||||
"Not redirecting \"{}\" to \"{}\" because it already exists. Are you sure it needs to be redirected?",
|
|
||||||
original.display(),
|
|
||||||
destination,
|
|
||||||
);
|
|
||||||
return Err(Error::msg(msg));
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(parent) = original.parent() {
|
if let Some(parent) = original.parent() {
|
||||||
std::fs::create_dir_all(parent)
|
std::fs::create_dir_all(parent)
|
||||||
.with_context(|| format!("Unable to ensure \"{}\" exists", parent.display()))?;
|
.with_context(|| format!("Unable to ensure \"{}\" exists", parent.display()))?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let js_map = serde_json::to_string(fragment_map)?;
|
||||||
|
|
||||||
let ctx = json!({
|
let ctx = json!({
|
||||||
|
"fragment_map": js_map,
|
||||||
"url": destination,
|
"url": destination,
|
||||||
});
|
});
|
||||||
let f = File::create(original)?;
|
let f = File::create(original)?;
|
||||||
|
|
@ -934,6 +948,62 @@ struct RenderItemContext<'a> {
|
||||||
chapter_titles: &'a HashMap<PathBuf, String>,
|
chapter_titles: &'a HashMap<PathBuf, String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Redirect mapping.
|
||||||
|
///
|
||||||
|
/// The key is the source path (like `foo/bar.html`). The value is a tuple
|
||||||
|
/// `(destination_path, fragment_map)`. The `destination_path` is the page to
|
||||||
|
/// redirect to. `fragment_map` is the map of fragments that override the
|
||||||
|
/// destination. For example, a fragment `#foo` could redirect to any other
|
||||||
|
/// page or site.
|
||||||
|
type CombinedRedirects = BTreeMap<String, (String, BTreeMap<String, String>)>;
|
||||||
|
fn combine_fragment_redirects(redirects: &HashMap<String, String>) -> CombinedRedirects {
|
||||||
|
let mut combined: CombinedRedirects = BTreeMap::new();
|
||||||
|
// This needs to extract the fragments to generate the fragment map.
|
||||||
|
for (original, new) in redirects {
|
||||||
|
if let Some((source_path, source_fragment)) = original.rsplit_once('#') {
|
||||||
|
let e = combined.entry(source_path.to_string()).or_default();
|
||||||
|
if let Some(old) = e.1.insert(format!("#{source_fragment}"), new.clone()) {
|
||||||
|
log::error!(
|
||||||
|
"internal error: found duplicate fragment redirect \
|
||||||
|
{old} for {source_path}#{source_fragment}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let e = combined.entry(original.to_string()).or_default();
|
||||||
|
e.0 = new.clone();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
combined
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Collects fragment redirects for an existing page.
|
||||||
|
///
|
||||||
|
/// The returned map has keys like `#foo` and the value is the new destination
|
||||||
|
/// path or URL.
|
||||||
|
fn collect_redirects_for_path(
|
||||||
|
path: &Path,
|
||||||
|
redirects: &HashMap<String, String>,
|
||||||
|
) -> Result<BTreeMap<String, String>> {
|
||||||
|
let path = format!("/{}", path.display().to_string().replace('\\', "/"));
|
||||||
|
if redirects.contains_key(&path) {
|
||||||
|
bail!(
|
||||||
|
"redirect found for existing chapter at `{path}`\n\
|
||||||
|
Either delete the redirect or remove the chapter."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let key_prefix = format!("{path}#");
|
||||||
|
let map = redirects
|
||||||
|
.iter()
|
||||||
|
.filter_map(|(source, dest)| {
|
||||||
|
source
|
||||||
|
.strip_prefix(&key_prefix)
|
||||||
|
.map(|fragment| (format!("#{fragment}"), dest.to_string()))
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
Ok(map)
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use crate::config::TextDirection;
|
use crate::config::TextDirection;
|
||||||
|
|
|
||||||
|
|
@ -25,4 +25,27 @@ expand = true
|
||||||
heading-split-level = 2
|
heading-split-level = 2
|
||||||
|
|
||||||
[output.html.redirect]
|
[output.html.redirect]
|
||||||
"/format/config.html" = "configuration/index.html"
|
"/format/config.html" = "../prefix.html"
|
||||||
|
|
||||||
|
# This is a source without a fragment, and one with a fragment that goes to
|
||||||
|
# the same place. The redirect with the fragment is not necessary, since that
|
||||||
|
# is the default behavior.
|
||||||
|
"/pointless-fragment.html" = "prefix.html"
|
||||||
|
"/pointless-fragment.html#foo" = "prefix.html#foo"
|
||||||
|
|
||||||
|
"/rename-page-and-fragment.html" = "prefix.html"
|
||||||
|
"/rename-page-and-fragment.html#orig" = "prefix.html#new"
|
||||||
|
|
||||||
|
"/rename-page-fragment-elsewhere.html" = "prefix.html"
|
||||||
|
"/rename-page-fragment-elsewhere.html#orig" = "suffix.html#new"
|
||||||
|
|
||||||
|
# Rename fragment on an existing page.
|
||||||
|
"/prefix.html#orig" = "prefix.html#new"
|
||||||
|
# Rename fragment on an existing page to another page.
|
||||||
|
"/prefix.html#orig-new-page" = "suffix.html#new"
|
||||||
|
|
||||||
|
"/full-url-with-fragment.html" = "https://www.rust-lang.org/#fragment"
|
||||||
|
|
||||||
|
"/full-url-with-fragment-map.html" = "https://www.rust-lang.org/"
|
||||||
|
"/full-url-with-fragment-map.html#a" = "https://www.rust-lang.org/#new1"
|
||||||
|
"/full-url-with-fragment-map.html#b" = "https://www.rust-lang.org/#new2"
|
||||||
|
|
|
||||||
51
tests/gui/redirect.goml
Normal file
51
tests/gui/redirect.goml
Normal file
|
|
@ -0,0 +1,51 @@
|
||||||
|
go-to: |DOC_PATH| + "format/config.html"
|
||||||
|
assert-window-property: ({"location": |DOC_PATH| + "prefix.html"})
|
||||||
|
|
||||||
|
// Check that it preserves fragments when redirecting.
|
||||||
|
go-to: |DOC_PATH| + "format/config.html#fragment"
|
||||||
|
assert-window-property: ({"location": |DOC_PATH| + "prefix.html#fragment"})
|
||||||
|
|
||||||
|
// The fragment one here isn't necessary, but should still work.
|
||||||
|
go-to: |DOC_PATH| + "pointless-fragment.html"
|
||||||
|
assert-window-property: ({"location": |DOC_PATH| + "prefix.html"})
|
||||||
|
go-to: |DOC_PATH| + "pointless-fragment.html#foo"
|
||||||
|
assert-window-property: ({"location": |DOC_PATH| + "prefix.html#foo"})
|
||||||
|
|
||||||
|
// Page rename, and a fragment rename.
|
||||||
|
go-to: |DOC_PATH| + "rename-page-and-fragment.html"
|
||||||
|
assert-window-property: ({"location": |DOC_PATH| + "prefix.html"})
|
||||||
|
go-to: |DOC_PATH| + "rename-page-and-fragment.html#orig"
|
||||||
|
assert-window-property: ({"location": |DOC_PATH| + "prefix.html#new"})
|
||||||
|
|
||||||
|
// Page rename, and the fragment goes to a *different* page from the default.
|
||||||
|
go-to: |DOC_PATH| + "rename-page-fragment-elsewhere.html"
|
||||||
|
assert-window-property: ({"location": |DOC_PATH| + "prefix.html"})
|
||||||
|
go-to: |DOC_PATH| + "rename-page-fragment-elsewhere.html#orig"
|
||||||
|
assert-window-property: ({"location": |DOC_PATH| + "suffix.html#new"})
|
||||||
|
|
||||||
|
// Goes to an external site.
|
||||||
|
go-to: |DOC_PATH| + "full-url-with-fragment.html"
|
||||||
|
assert-window-property: ({"location": "https://www.rust-lang.org/#fragment"})
|
||||||
|
|
||||||
|
// External site with fragment renames.
|
||||||
|
go-to: |DOC_PATH| + "full-url-with-fragment-map.html#a"
|
||||||
|
assert-window-property: ({"location": "https://www.rust-lang.org/#new1"})
|
||||||
|
go-to: |DOC_PATH| + "full-url-with-fragment-map.html#b"
|
||||||
|
assert-window-property: ({"location": "https://www.rust-lang.org/#new2"})
|
||||||
|
|
||||||
|
// Rename fragment on an existing page.
|
||||||
|
go-to: |DOC_PATH| + "prefix.html#orig"
|
||||||
|
assert-window-property: ({"location": |DOC_PATH| + "prefix.html#new"})
|
||||||
|
|
||||||
|
// Other fragments aren't affected.
|
||||||
|
go-to: |DOC_PATH| + "index.html" // Reset page since redirects are processed on load.
|
||||||
|
go-to: |DOC_PATH| + "prefix.html"
|
||||||
|
assert-window-property: ({"location": |DOC_PATH| + "prefix.html"})
|
||||||
|
go-to: |DOC_PATH| + "index.html" // Reset page since redirects are processed on load.
|
||||||
|
go-to: |DOC_PATH| + "prefix.html#dont-change"
|
||||||
|
assert-window-property: ({"location": |DOC_PATH| + "prefix.html#dont-change"})
|
||||||
|
|
||||||
|
// Rename fragment on an existing page to another page.
|
||||||
|
go-to: |DOC_PATH| + "index.html" // Reset page since redirects are processed on load.
|
||||||
|
go-to: |DOC_PATH| + "prefix.html#orig-new-page"
|
||||||
|
assert-window-property: ({"location": |DOC_PATH| + "suffix.html#new"})
|
||||||
|
|
@ -16,3 +16,34 @@ fn redirects_are_emitted_correctly() {
|
||||||
file!["redirects/redirects_are_emitted_correctly/expected/nested/page.html"],
|
file!["redirects/redirects_are_emitted_correctly/expected/nested/page.html"],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Invalid redirect with only fragments.
|
||||||
|
#[test]
|
||||||
|
fn redirect_removed_with_fragments_only() {
|
||||||
|
BookTest::from_dir("redirects/redirect_removed_with_fragments_only").run("build", |cmd| {
|
||||||
|
cmd.expect_failure().expect_stderr(str![[r#"
|
||||||
|
[TIMESTAMP] [INFO] (mdbook::book): Book building has started
|
||||||
|
[TIMESTAMP] [INFO] (mdbook::book): Running the html backend
|
||||||
|
[TIMESTAMP] [ERROR] (mdbook::utils): Error: Rendering failed
|
||||||
|
[TIMESTAMP] [ERROR] (mdbook::utils): [TAB]Caused By: Unable to emit redirects
|
||||||
|
[TIMESTAMP] [ERROR] (mdbook::utils): [TAB]Caused By: redirect entry for `old-file.html` only has source paths with `#` fragments
|
||||||
|
There must be an entry without the `#` fragment to determine the default destination.
|
||||||
|
|
||||||
|
"#]]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Invalid redirect for an existing page.
|
||||||
|
#[test]
|
||||||
|
fn redirect_existing_page() {
|
||||||
|
BookTest::from_dir("redirects/redirect_existing_page").run("build", |cmd| {
|
||||||
|
cmd.expect_failure().expect_stderr(str![[r#"
|
||||||
|
[TIMESTAMP] [INFO] (mdbook::book): Book building has started
|
||||||
|
[TIMESTAMP] [INFO] (mdbook::book): Running the html backend
|
||||||
|
[TIMESTAMP] [ERROR] (mdbook::utils): Error: Rendering failed
|
||||||
|
[TIMESTAMP] [ERROR] (mdbook::utils): [TAB]Caused By: redirect found for existing chapter at `/chapter_1.html`
|
||||||
|
Either delete the redirect or remove the chapter.
|
||||||
|
|
||||||
|
"#]]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
[book]
|
||||||
|
title = "redirect_existing_page"
|
||||||
|
|
||||||
|
[output.html.redirect]
|
||||||
|
"/chapter_1.html" = "other-page.html"
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
# Summary
|
||||||
|
|
||||||
|
- [Chapter 1](./chapter_1.md)
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
# Chapter 1
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
[book]
|
||||||
|
title = "redirect_removed_with_fragments_only"
|
||||||
|
|
||||||
|
[output.html.redirect]
|
||||||
|
"/old-file.html#foo" = "chapter_1.html"
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
# Summary
|
||||||
|
|
||||||
|
- [Chapter 1](./chapter_1.md)
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
# Chapter 1
|
||||||
|
|
@ -3,4 +3,5 @@ title = "redirects_are_emitted_correctly"
|
||||||
|
|
||||||
[output.html.redirect]
|
[output.html.redirect]
|
||||||
"/overview.html" = "index.html"
|
"/overview.html" = "index.html"
|
||||||
|
"/overview.html#old" = "index.html#new"
|
||||||
"/nested/page.html" = "https://rust-lang.org/"
|
"/nested/page.html" = "https://rust-lang.org/"
|
||||||
|
|
|
||||||
|
|
@ -8,5 +8,29 @@
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<p>Redirecting to... <a href="https://rust-lang.org/">https://rust-lang.org/</a>.</p>
|
<p>Redirecting to... <a href="https://rust-lang.org/">https://rust-lang.org/</a>.</p>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// This handles redirects that involve fragments.
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
const fragmentMap =
|
||||||
|
{}
|
||||||
|
;
|
||||||
|
const fragment = window.location.hash;
|
||||||
|
if (fragment) {
|
||||||
|
let redirectUrl = "https://rust-lang.org/";
|
||||||
|
const target = fragmentMap[fragment];
|
||||||
|
if (target) {
|
||||||
|
let url = new URL(target, window.location.href);
|
||||||
|
redirectUrl = url.href;
|
||||||
|
} else {
|
||||||
|
let url = new URL(redirectUrl, window.location.href);
|
||||||
|
url.hash = window.location.hash;
|
||||||
|
redirectUrl = url.href;
|
||||||
|
}
|
||||||
|
window.location.replace(redirectUrl);
|
||||||
|
}
|
||||||
|
// else redirect handled by http-equiv
|
||||||
|
});
|
||||||
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
|
|
@ -8,5 +8,29 @@
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<p>Redirecting to... <a href="index.html">index.html</a>.</p>
|
<p>Redirecting to... <a href="index.html">index.html</a>.</p>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// This handles redirects that involve fragments.
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
const fragmentMap =
|
||||||
|
{"#old":"index.html#new"}
|
||||||
|
;
|
||||||
|
const fragment = window.location.hash;
|
||||||
|
if (fragment) {
|
||||||
|
let redirectUrl = "index.html";
|
||||||
|
const target = fragmentMap[fragment];
|
||||||
|
if (target) {
|
||||||
|
let url = new URL(target, window.location.href);
|
||||||
|
redirectUrl = url.href;
|
||||||
|
} else {
|
||||||
|
let url = new URL(redirectUrl, window.location.href);
|
||||||
|
url.hash = window.location.hash;
|
||||||
|
redirectUrl = url.href;
|
||||||
|
}
|
||||||
|
window.location.replace(redirectUrl);
|
||||||
|
}
|
||||||
|
// else redirect handled by http-equiv
|
||||||
|
});
|
||||||
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
# Redirects
|
# Redirects
|
||||||
|
|
||||||
- [Chapter 1](chapter_1.md)
|
- [Chapter 1](chapter_1.md)
|
||||||
|
- [Chapter 2](chapter_2.md)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
# Chapter 2
|
||||||
Loading…
Add table
Reference in a new issue