From b14eba52d7dcec767a1162ad5735dd98ed9bf1d0 Mon Sep 17 00:00:00 2001 From: Zach Himsel Date: Mon, 12 Aug 2019 13:41:41 -0600 Subject: [PATCH 01/10] Move plugin config to its own section This is required because any configuration that doesn't match the default config schema (https://github.com/mkdocs/mkdocs/blob/1.0.4/mkdocs/config/defaults.py) will result in a warning being produced by mkdocs (which fails hard if strict mode is set). As per the mkdocs documentation, plugins should specify their own settings in as sub-objects in their `plugins:` section. While _technically_ the plugin does read the root-level `redirects:` config section, it should be using it at all. Note: this is technically a backwards-incompatible change. Any user who upgrades their installed plugin to this version will need to modify their config accordingly. As such, we should probably make a note of it in the release/version tag. --- mkdocs_redirects/plugin.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/mkdocs_redirects/plugin.py b/mkdocs_redirects/plugin.py index 7ecb641..38fe30d 100644 --- a/mkdocs_redirects/plugin.py +++ b/mkdocs_redirects/plugin.py @@ -13,8 +13,13 @@ class RedirectPlugin(BasePlugin): + # Config schema for this plugin + config_scheme = ( + ('redirect_maps', config_options.Type(dict, default={})), # note the trailing comma + ) + def on_post_build(self, config, **kwargs): - redirects = config.get('redirects', {}) + redirects = self.config.get('redirect_maps', {}) for old_page, new_page in redirects.items(): old_page_path = os.path.join(config['site_dir'], '{}.html'.format(old_page)) From 45e33bf8e199bf9c1fee3fb1ed26e4afb8587960 Mon Sep 17 00:00:00 2001 From: Zach Himsel Date: Wed, 14 Aug 2019 21:51:16 -0600 Subject: [PATCH 02/10] Fix logging --- mkdocs_redirects/plugin.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mkdocs_redirects/plugin.py b/mkdocs_redirects/plugin.py index 38fe30d..a305a4a 100644 --- a/mkdocs_redirects/plugin.py +++ b/mkdocs_redirects/plugin.py @@ -7,8 +7,8 @@ from mkdocs.plugins import BasePlugin from mkdocs.structure.files import File -log = logging.getLogger(__name__) -log.addFilter(mkdocs_utils.warning_filter) +log = logging.getLogger('mkdocs.plugin.redirects') +log.addFilter(utils.warning_filter) class RedirectPlugin(BasePlugin): From 01fdc1c447ef9a17560d36407696d41bfe8dd6ee Mon Sep 17 00:00:00 2001 From: Zach Himsel Date: Wed, 14 Aug 2019 21:51:53 -0600 Subject: [PATCH 03/10] Remove unused imports --- mkdocs_redirects/plugin.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/mkdocs_redirects/plugin.py b/mkdocs_redirects/plugin.py index a305a4a..4b90329 100644 --- a/mkdocs_redirects/plugin.py +++ b/mkdocs_redirects/plugin.py @@ -3,9 +3,8 @@ import textwrap from mkdocs import utils as mkdocs_utils -from mkdocs.config import config_options, Config +from mkdocs.config import config_options from mkdocs.plugins import BasePlugin -from mkdocs.structure.files import File log = logging.getLogger('mkdocs.plugin.redirects') log.addFilter(utils.warning_filter) From e20198702eec48fcd627ab85ebafecfde2afe29f Mon Sep 17 00:00:00 2001 From: Zach Himsel Date: Wed, 14 Aug 2019 21:52:29 -0600 Subject: [PATCH 04/10] Fix plugin logic Prior to this commit, the logic being used here does not match the logic that MkDocs uses internally to generate the final site_dir. More specifically: we don't take into account the `use_directory_urls` setting at all, which alters how the site_dir is built (in regard to URLs and paths). --- .flake8 | 2 + mkdocs_redirects/plugin.py | 113 +++++++++++++++++++++++++------------ 2 files changed, 78 insertions(+), 37 deletions(-) create mode 100644 .flake8 diff --git a/.flake8 b/.flake8 new file mode 100644 index 0000000..6deafc2 --- /dev/null +++ b/.flake8 @@ -0,0 +1,2 @@ +[flake8] +max-line-length = 120 diff --git a/mkdocs_redirects/plugin.py b/mkdocs_redirects/plugin.py index 4b90329..7ae80ac 100644 --- a/mkdocs_redirects/plugin.py +++ b/mkdocs_redirects/plugin.py @@ -2,7 +2,7 @@ import os import textwrap -from mkdocs import utils as mkdocs_utils +from mkdocs import utils from mkdocs.config import config_options from mkdocs.plugins import BasePlugin @@ -10,45 +10,84 @@ log.addFilter(utils.warning_filter) -class RedirectPlugin(BasePlugin): +def write_html(config, old_path, new_path): + """ Write an HTML file in the site_dir with a meta redirect to the new page """ + old_path_abs = os.path.join(config['site_dir'], old_path) + old_dir_abs = os.path.dirname(old_path_abs) + if not os.path.exists(old_dir_abs): + log.debug("Creating directory '%s'", old_dir_abs) + os.makedirs(old_dir_abs) + with open(old_path_abs, 'w') as f: + log.debug("Creating redirect file: '%s', pointing to '%s'", + old_path_abs, new_path) + f.write(textwrap.dedent( + """ + + + + + + + + Redirecting... + + + """ + ).format(url=new_path)) + + +def get_html_path(path, use_directory_urls): + """ Return the HTML file path for a given markdown file """ + parent, filename = os.path.split(path) + name_orig, ext = os.path.splitext(filename) + if use_directory_urls: + name = 'index' if name_orig.lower() in ('index', 'readme') else name_orig + if name == 'index': + return os.path.join(parent, 'index.html') + else: + return os.path.join(parent, name, 'index.html') + else: + return os.path.join(parent, (name_orig + '.html')) + - # Config schema for this plugin +class RedirectPlugin(BasePlugin): config_scheme = ( ('redirect_maps', config_options.Type(dict, default={})), # note the trailing comma ) + # Build a list of redirects on file generation + def on_files(self, files, config, **kwargs): + self.redirects = self.config.get('redirect_maps', {}) + + # Validate user-provided redirect "old files" + for page_old in self.redirects.keys(): + if not utils.is_markdown_file(page_old): + log.warn("redirects plugin: '%s' is not a valid markdown file!", page_old) + + # Build a dict of known document pages + self.doc_pages = {} + for page in files.documentation_pages(): # object type: mkdocs.structure.files.File + self.doc_pages[page.src_path] = page + + # Create HTML files for redirects after site dir has been built def on_post_build(self, config, **kwargs): - redirects = self.config.get('redirect_maps', {}) - - for old_page, new_page in redirects.items(): - old_page_path = os.path.join(config['site_dir'], '{}.html'.format(old_page)) - if not new_page.startswith(('http','HTTP')): - new_page_path = os.path.join(config['site_dir'], '{}.html'.format(new_page)) - # check that the page being redirected to actually exists - if not os.path.exists(os.path.dirname(new_page_path)): - msg = 'Redirect does not exist for path: {}'.format(new_page_path) - if config.get('strict', False): - raise Exception(msg) - else: - log.warn(msg) - new_page = '/{}.html'.format(new_page) - - # ensure the folder path exists, recursively for nested directories. - if not os.path.exists(os.path.dirname(old_page_path)): - os.makedirs(os.path.dirname(old_page_path)) - - # write an HTML file in the site_dir with a meta redirect to the new page - # note that it will prefix the path with `/` to be relative to the site root. - with open(old_page_path, 'w') as f: - f.write(textwrap.dedent(""" - - - - - - - - Redirecting... - - - """).format(url=new_page)) + # Determine if 'use_directory_urls' is set; our destination + use_directory_urls = config.get('use_directory_urls') + + for page_old, page_new in self.redirects.items(): + + if page_new.lower().startswith(('http://', 'https://')): + dest_path = page_new + + elif page_new in self.doc_pages: + dest_path = '/' + self.doc_pages[page_new].dest_path + if use_directory_urls: + dest_path = dest_path.split('index.html')[0] + + else: + log.warn("Redirect target '%s' does not exist!", page_new) + continue + + write_html(config, + get_html_path(page_old, use_directory_urls), + dest_path) From 79471718ce50ac0218e340f4854172eebc5aaa6e Mon Sep 17 00:00:00 2001 From: Zach Himsel Date: Thu, 15 Aug 2019 13:33:02 -0600 Subject: [PATCH 05/10] Improve logging output --- mkdocs_redirects/plugin.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/mkdocs_redirects/plugin.py b/mkdocs_redirects/plugin.py index 7ae80ac..1b63989 100644 --- a/mkdocs_redirects/plugin.py +++ b/mkdocs_redirects/plugin.py @@ -13,13 +13,14 @@ def write_html(config, old_path, new_path): """ Write an HTML file in the site_dir with a meta redirect to the new page """ old_path_abs = os.path.join(config['site_dir'], old_path) + old_dir = os.path.dirname(old_path) old_dir_abs = os.path.dirname(old_path_abs) if not os.path.exists(old_dir_abs): - log.debug("Creating directory '%s'", old_dir_abs) + log.debug("Creating directory '%s'", old_dir) os.makedirs(old_dir_abs) with open(old_path_abs, 'w') as f: - log.debug("Creating redirect file: '%s', pointing to '%s'", - old_path_abs, new_path) + log.debug("Creating redirect: '%s' -> '%s'", + old_path, new_path) f.write(textwrap.dedent( """ From 115b8d4b62403d47f110e1e5fb9bdc46d5acd40d Mon Sep 17 00:00:00 2001 From: Zach Himsel Date: Thu, 15 Aug 2019 13:52:47 -0600 Subject: [PATCH 06/10] Rewrite README according to recent changes --- README.md | 57 ++++++++++++++++++++++++++++++++++--------------------- 1 file changed, 35 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index f869758..3118e7e 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,10 @@ # mkdocs-redirects -Open source plugin for Mkdocs page redirects + +Plugin for [`mkdocs`](https://www.mkdocs.org/) to create page redirects (e.g. for moved/renamed pages). ## Installing -> **Note:** This package requires MkDocs version 1.0.4 or higher. +> **Note:** This package requires MkDocs version 1.0.4 or higher. Install with pip: @@ -11,43 +12,55 @@ Install with pip: pip install mkdocs-redirects ``` -Enable the plugin in your `mkdocs.yml`: +## Using + +To use this plugin, specify your desired redirects in the plugin's `redirect_maps` setting in your `mkdocs.yml`: ```yaml plugins: - - search - - redirects + - redirects: + redirect_maps: + 'old.md': 'new.md' + 'old/file.md': 'new/file.md' + 'some_file.md': 'http://external.url.com/foobar' ``` -## Using +_Note: don't forget that specifying the `plugins` setting will override the defaults if you didn't already have it set! See [this page](https://www.mkdocs.org/user-guide/configuration/#plugins) for more information._ -In your `mkdocs.yml`, add a `redirects` block that maps the old page location to the new location: +The redirects map should take the form of a key/value pair: -``` -redirects: - 'old': 'some/new_location' - 'something/before': 'another/moved/file' - 'external': 'http://google.com' -``` +- The key of each redirect is the original _markdown doc_ (relative to the `docs_dir` path). + - This plugin will handle the filename resolution during the `mkdocs build` process. + This should be set to what the original markdown doc's filename was (or what it _would be_ if it existed), not the final HTML file rendered by MkDocs +- The value is the _redirect target_. This can take the following forms: + - Path of the _markdown doc_ you wish to be redirected to (relative to `docs_dir`) + - This plugin will handle the filename resolution during the `mkdocs build` process. + This should be set to what the markdown doc's filename is, not the final HTML file rendered by MkDocs + - External URL (e.g. `http://example.com`) + +During the `mkdocs build` process, this plugin will create `.html` files in `site_dir` for each of the "old" file that redirects to the "new" path. +It will produce a warning if any problems are encountered or of the redirect target doesn't actually exist (useful if you have `strict: true` set). + +### `use_directory_urls` -Note that the `.html` extension should be omitted (and will be automatically appended). +If you have `use_directory_urls: true` set (which is the default), this plugin will modify the redirect targets to the _directory_ URL, not the _actual_ `index.html` filename. +owever, it will create the `index.html` file for each target in the correct place so URL resolution works. -The plugin will dynamically create `old.html`, `something/before.html`, and `external.html` in your configured `site_dir` with -HTML that will include a meta redirect to the new page location. +For example, a redirect map of `'old/dir/README.md': 'new/dir/README.md'` will result in an HTML file created at `$site_dir/old/dir/index.html` which redirects to `/new/dir/. -If the new location does not start with `http` or `HTTP` then it will also be appended with `.html` extension and is assumed to be relative to the root of the site. +Additionally, a redirect map of `'old/dir/doc_name.md': 'new/dir/doc_name.md'` will result in `$site_dir/old/dir/doc_name/index.html` redirecting to `/new/dir/doc_name/` -For nested subfolders, the plugin will automatically create these directories in the `site_dir`. +This mimcs the behavior of how MkDocs builds the site dir without this plugin. ## Contributing -- Pull requests are welcome. -- File bugs and suggestions in the Github Issues tracker. +- Pull Requests are welcome. +- File bugs and suggestions in the [Github Issues tracker](https://github.com/datarobot/mkdocs-redirects/issues). ## Releasing -``` - make release +```bash +make release ``` It will prompt you for your PyPI user and password. From 1a0c0528a4216a0542c73fae7e3cf58fe279e113 Mon Sep 17 00:00:00 2001 From: Zach Himsel Date: Mon, 12 Aug 2019 13:41:35 -0600 Subject: [PATCH 07/10] Bump version I'm bumping to version 1.0 because this release will contain backwards-incompatible changes. --- setup.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 4a8d758..0665bee 100644 --- a/setup.py +++ b/setup.py @@ -1,12 +1,14 @@ import os from setuptools import setup, find_packages + def read(fname): return open(os.path.join(os.path.dirname(__file__), fname)).read() + setup( name='mkdocs-redirects', - version='0.0.6', + version='1.0.0', description='A MkDocs plugin for dynamic page redirects to prevent broken links.', long_description=read('README.md'), long_description_content_type="text/markdown", From 89e5bb14235ee2c6f68bb5b59407eeaf9daab0c5 Mon Sep 17 00:00:00 2001 From: Zach Himsel Date: Thu, 15 Aug 2019 14:21:52 -0600 Subject: [PATCH 08/10] Add some better comments --- mkdocs_redirects/plugin.py | 35 ++++++++++++++++++++++++++++++----- 1 file changed, 30 insertions(+), 5 deletions(-) diff --git a/mkdocs_redirects/plugin.py b/mkdocs_redirects/plugin.py index 1b63989..197234b 100644 --- a/mkdocs_redirects/plugin.py +++ b/mkdocs_redirects/plugin.py @@ -10,14 +10,19 @@ log.addFilter(utils.warning_filter) -def write_html(config, old_path, new_path): +def write_html(site_dir, old_path, new_path): """ Write an HTML file in the site_dir with a meta redirect to the new page """ - old_path_abs = os.path.join(config['site_dir'], old_path) + # Determine all relevant paths + old_path_abs = os.path.join(site_dir, old_path) old_dir = os.path.dirname(old_path) old_dir_abs = os.path.dirname(old_path_abs) + + # Create parent directories if they don't exist if not os.path.exists(old_dir_abs): log.debug("Creating directory '%s'", old_dir) os.makedirs(old_dir_abs) + + # Write the HTML redirect file in place of the old file with open(old_path_abs, 'w') as f: log.debug("Creating redirect: '%s' -> '%s'", old_path, new_path) @@ -41,17 +46,28 @@ def get_html_path(path, use_directory_urls): """ Return the HTML file path for a given markdown file """ parent, filename = os.path.split(path) name_orig, ext = os.path.splitext(filename) + + # Directory URLs require some different logic. This mirrors mkdocs' internal logic. if use_directory_urls: + + # Both `index.md` and `README.md` files are normalized to `index.html` during build name = 'index' if name_orig.lower() in ('index', 'readme') else name_orig + + # If it's name is `index`, then that means it's the "homepage" of a directory, so should get placed in that dir if name == 'index': return os.path.join(parent, 'index.html') + + # Otherwise, it's a file within that folder, so it should go in its own directory to resolve properly else: return os.path.join(parent, name, 'index.html') + + # Just use the original name if Directory URLs aren't used else: return os.path.join(parent, (name_orig + '.html')) class RedirectPlugin(BasePlugin): + # Any options that this plugin supplies should go here. config_scheme = ( ('redirect_maps', config_options.Type(dict, default={})), # note the trailing comma ) @@ -65,30 +81,39 @@ def on_files(self, files, config, **kwargs): if not utils.is_markdown_file(page_old): log.warn("redirects plugin: '%s' is not a valid markdown file!", page_old) - # Build a dict of known document pages + # Build a dict of known document pages to validate against later self.doc_pages = {} for page in files.documentation_pages(): # object type: mkdocs.structure.files.File self.doc_pages[page.src_path] = page # Create HTML files for redirects after site dir has been built def on_post_build(self, config, **kwargs): - # Determine if 'use_directory_urls' is set; our destination + + # Determine if 'use_directory_urls' is set use_directory_urls = config.get('use_directory_urls') + # Walk through the redirect map and write their HTML files for page_old, page_new in self.redirects.items(): + # External redirect targets are easy, just use it as the target path if page_new.lower().startswith(('http://', 'https://')): dest_path = page_new + # Internal document targets require a leading '/' to resolve properly. elif page_new in self.doc_pages: dest_path = '/' + self.doc_pages[page_new].dest_path + + # If use_directory_urls is set, redirect to the directory, not the HTML file if use_directory_urls: dest_path = dest_path.split('index.html')[0] + # If the redirect target isn't external or a valid internal page, throw an error + # Note: we use 'warn' here specifically; mkdocs treats warnings specially when in strict mode else: log.warn("Redirect target '%s' does not exist!", page_new) continue - write_html(config, + # DO IT! + write_html(config['site_dir'], get_html_path(page_old, use_directory_urls), dest_path) From 6ea448ed04980e7b3fff835cb9f8059123089d69 Mon Sep 17 00:00:00 2001 From: Zach Himsel Date: Thu, 15 Aug 2019 14:28:49 -0600 Subject: [PATCH 09/10] Add shim code to warn users about config changes --- mkdocs_redirects/plugin.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/mkdocs_redirects/plugin.py b/mkdocs_redirects/plugin.py index 197234b..5a26dc1 100644 --- a/mkdocs_redirects/plugin.py +++ b/mkdocs_redirects/plugin.py @@ -76,6 +76,11 @@ class RedirectPlugin(BasePlugin): def on_files(self, files, config, **kwargs): self.redirects = self.config.get('redirect_maps', {}) + # SHIM! Produce a warning if the old root-level 'redirects' config is present + if config.get('redirects'): + log.warn("The root-level 'redirects:' setting is not valid and has been changed in version 1.0! " + "The plugin-level 'redirect-map' must be used instead. See https://git.io/fjdBN") + # Validate user-provided redirect "old files" for page_old in self.redirects.keys(): if not utils.is_markdown_file(page_old): From 61da76db682ecf50e8fcb2e26d7d76ccc4dbc60f Mon Sep 17 00:00:00 2001 From: Zach Himsel Date: Thu, 15 Aug 2019 14:43:02 -0600 Subject: [PATCH 10/10] Fix typo --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 3118e7e..62e8dc1 100644 --- a/README.md +++ b/README.md @@ -44,7 +44,7 @@ It will produce a warning if any problems are encountered or of the redirect tar ### `use_directory_urls` If you have `use_directory_urls: true` set (which is the default), this plugin will modify the redirect targets to the _directory_ URL, not the _actual_ `index.html` filename. -owever, it will create the `index.html` file for each target in the correct place so URL resolution works. +However, it will create the `index.html` file for each target in the correct place so URL resolution works. For example, a redirect map of `'old/dir/README.md': 'new/dir/README.md'` will result in an HTML file created at `$site_dir/old/dir/index.html` which redirects to `/new/dir/.