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/README.md b/README.md index f869758..62e8dc1 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. +However, 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. diff --git a/mkdocs_redirects/plugin.py b/mkdocs_redirects/plugin.py index 7ecb641..5a26dc1 100644 --- a/mkdocs_redirects/plugin.py +++ b/mkdocs_redirects/plugin.py @@ -2,49 +2,123 @@ import os import textwrap -from mkdocs import utils as mkdocs_utils -from mkdocs.config import config_options, Config +from mkdocs import utils +from mkdocs.config import config_options 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) + + +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 """ + # 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) + 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) + + # 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 + ) + # Build a list of redirects on file generation + 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): + log.warn("redirects plugin: '%s' is not a valid markdown file!", page_old) + + # 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): - redirects = config.get('redirects', {}) - - 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 + 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 + + # DO IT! + write_html(config['site_dir'], + get_html_path(page_old, use_directory_urls), + dest_path) 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",