Skip to content

Feature request: Heading-based redirects. #69

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
rudolfbyker opened this issue Nov 11, 2024 · 12 comments · May be fixed by #73
Open

Feature request: Heading-based redirects. #69

rudolfbyker opened this issue Nov 11, 2024 · 12 comments · May be fixed by #73

Comments

@rudolfbyker
Copy link

I use a custom scraper that runs in my CI to detect when I break any URLs that might exist in search engines, or that have been shared by users. Then I use mkdocs-redirects to fix those links before deploying the next version of my docs. This works well, until I have to split a page into two separate pages.

Since we're doing client-side redirects anyway, could we support heading-based redirects? Heading-based redirects are not possible in the context of SSR, because the anchor (the #foo part of the URL) is not sent to the server. But the browser knows what it is, so with a little bit of JS, we can easily detect which heading is desired, and redirect based on that.

Currently, the following script is generated:

var anchor=window.location.hash.substr(1);
location.href="destination.html"+(anchor?"#"+anchor:"")

This could easily be expanded with a simple switch statement:

var anchor=window.location.hash.substr(1);
switch (anchor) {
  case 'heading1':
    location.href="page1.html"
    break;
  case 'heading2':
    location.href="page2.html"
    break;
  default:
    location.href="page3.html"
}

The mkdocs.yaml config could look something like this:

plugins:
  - redirects:
      redirect_maps:
        "original.md#heading1": page1.md
        "original.md#heading2": page2.md
        "original.md": page3.md

or, if you don't like the flat structure:

plugins:
  - redirects:
      redirect_maps:
        "original.md":
          "#heading1": page1.md
          "#heading2": page2.md
          "default": page3.md

or, if you want to keep the door open for other types of redirects (which I can't conceive of now):

plugins:
  - redirects:
      redirect_maps:
        "original.md":
          "anchors":
            "heading1": page1.md
            "heading2": page2.md
          "default": page3.md
@pawamoy
Copy link
Contributor

pawamoy commented Nov 12, 2024

Thanks for the request @rudolfbyker. That would be a nice addition, similar to #64.

I worry that users will expect such redirections to work in existing pages. But the plugin would have to modify the existing page to add the relevant headings and JS code, which is not trivial. This could definitely be documented as a limitation though.

The flat structure would mean the plugin must now iterate on all redirections in a first step, to reconcile page redirects and page+heading redirects.

@rudolfbyker
Copy link
Author

Sounds more complicated to implement than I anticipated.

@pawamoy
Copy link
Contributor

pawamoy commented Nov 12, 2024

Sorry, not sure what I was thinking, but surely the plugin wouldn't have to add any heading, just JS code, to redirect depending on the anchor in the URL. So, a bit less complicated :)

@wwarriner
Copy link
Contributor

I took a look at implementing this, roughly, and here are a couple thoughts blocking me. I'm not sure the best way to proceed. I am going to use the term "anchor" to refer to the part of the URL beginning with a # symbol.

  1. Yes, JS can be used to redirect from old anchors to new pages. This would be accomplished by adding something like a JS object to serve as a "dict" with hard-coded sources and destinations. The JS code would select the destination based on the supplied anchor. The python side would need to write the JS code into the HTML template. This isn't impossible, but has some intricacies that may require some re-writing of the plugin.

  2. If anchors in the old page can lead to more than one new page, how do we indicate which is the canonical URL for the redirect? The HTML_TEMPLATE in the code has a <link rel="canonical" a=$url> tag. Right now, that $url is the URL of the redirect target. I have not found any advice in Google searches. I don't think this is a common-enough issue in most web applications to have any experts weighing in. Happy to be proven wrong!

  3. How do we handle the case where we want the old page to continue existing, and only specific anchors should be redirected? I'm sure this can be handled with redirects, but it means we need to add JS content to an existing page. The plugin does not have machinery for this right now.

Related: squidfunk/mkdocs-material#3764

@pawamoy
Copy link
Contributor

pawamoy commented Feb 18, 2025

If anchors in the old page can lead to more than one new page, how do we indicate which is the canonical URL for the redirect?

  • Page exists: no canonical URL (no link tag). The page is not a redirect so shouldn't be labeled as such.
  • Page does not exist:
    • Heading redirects only: use URL of first redirect. Alternatively, use page that is most redirected to.
    • Heading redirects and global/page redirect: use the page redirect

For example:

plugins:
  - redirects:
      redirect_maps:
        "old.md#heading1": page1.md
        "old.md#heading2": page2.md
        "old.md": page3.md

--> use page3


plugins:
  - redirects:
      redirect_maps:
        "old.md#heading1": page1.md
        "old.md#heading2": page2.md

--> use page1


plugins:
  - redirects:
      redirect_maps:
        "existing.md#heading1": page1.md
        "existing.md#heading2": page2.md

--> no link tag

@wwarriner
Copy link
Contributor

wwarriner commented Feb 18, 2025

Thanks for your thoughts! I think that the first and third cases are sensible. The second case ("use page1") would make the plugin opinionated.

This could be dealt with via some interface and options for how to select which page would serve as the canonical redirect, but that might complicate the user experience and the code unnecessarily. I'm interested to give this a try again, maybe later this week or next I'll give it a go.

Brainstorming options that spring to mind for case 2 (I'm ignoring how to implement selection, for now):

  • No canonical
  • First appearing
  • Last appearing
  • Most-frequently appearing
  • Manual selection

@pawamoy
Copy link
Contributor

pawamoy commented Feb 18, 2025

An interesting feature of "first" or "last" is that it allows manual selection thanks to ordering 🙂

@rudolfbyker
Copy link
Author

How do we handle the case where we want the old page to continue existing, and only specific anchors should be redirected?

mkdocs.yaml

plugins:
  - redirects:
      redirect_maps:
        "original.md#heading-1": "page1.md"  # This heading was moved to a new page.
        "original.md#heading-2": "page2.md#new-heading-2"  # This heading was moved to an existing page.
        # "original.md" continues to exist.

In the <head> of the original page:

var anchor=window.location.hash.substr(1);
switch (anchor) {
  case 'heading-1':
    location.href="page1.html"
    break;
  case 'heading-2':
    location.href="page2.html#new-heading-2"
    break;
  default:
    // Stay here.
}

Thanks for considering this :)

@wwarriner
Copy link
Contributor

An interesting feature of "first" or "last" is that it allows manual selection thanks to ordering 🙂

Definitely true! I like to keep them sorted alphabetically in our documentation to make lookup easier, which could be more challenging to cover with only "first" and "last".

@wwarriner
Copy link
Contributor

wwarriner commented Mar 10, 2025

I've attempted an implementation today and ran into a snag. The original plugin assumes the source page will no longer exist. This need not be the case in a world where we can redirect anchors to other anchors.

The HTML template for the redirect page does not make sense to use explicitly in the case where the source page continues to exist. We want to be able to reach a visibly-unaltered version of the source page, with only some anchors redirecting.

My first thought is to modify the HTML page in the on_post_build() event, adding a <script> tag to "inject" the redirection script.

  • Any alternatives to this approach? Flaws I'm missing?

  • BeautifulSoup has been my go-to for modifying HTML, but it adds a dependency. Thoughts on alternatives?

  • With no further modification to the page, people with "NoScript" style browser extensions won't know they are supposed to be getting redirected. They'll just hit the top of the page and wonder where the content went.

    The existing template takes care of this with text and a hyperlink as part of the page. I can't see a way around the problem, since we can't replace the source page if it is supposed to exist and be readable on the compiled site.

    One possibility is including explicit redirect text and hyperlinks in the source page. That doesn't seem like a very graceful solution and would make the page bulkier. It isn't clear how to gracefully and portably make this interact with themes, either.

    So, should we ignore the "NoScript" folks' needs and add warnings to this plugin's docs? Did I miss a solution that satisfies everyone, or is otherwise more graceful than what I've described?

@pawamoy
Copy link
Contributor

pawamoy commented Mar 11, 2025

@wwarriner, nice, thank you for taking a stab at this!

Any alternatives to this approach? Flaws I'm missing?

I think on_page_content is a better candidate. By the time you reach on_post_build, the page content has already been written to disk.

BeautifulSoup has been my go-to for modifying HTML, but it adds a dependency. Thoughts on alternatives?

I generally do not recommend using BeautifulSoup in MkDocs plugins, as parsing and re-dumping HTML is pretty slow. If a naive solution like appending the script to the page's content works well, I'd suggest doing that.

So, should we ignore the "NoScript" folks' needs and add warnings to this plugin's docs?

Yes. I don't see any practical fallback for clients with Javascript disabled. Such users are used to notice when a script is blocked and will likely know to try and enable it to get to the page/section they wanted to go to.

@reteps reteps linked a pull request Apr 29, 2025 that will close this issue
@reteps
Copy link

reteps commented May 5, 2025

Anything I can do to help move this along? I really need this feature!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

4 participants