Repository: mkdocs/mkdocs

Issue:
Improve edit_uri flexability
Hi guys, 
According to the documentation, I should be able to only add the link to my bitbucket repository to get edit links. 
I added it like this: repo_url: https://bitbucket.org/josephkobti/testdocs/
but the links I'm getting are not working
i.e. https://bitbucket.org/josephkobti/testdocs/src/default/docs/index.md

I think maybe I should add the repo_url in another way, any suggestions? 


Discussion:
What URL are you expecting to get?

IIRC, Bitbucket doesn't have an "edit" URL for documents in your repo. We can only point to the document, then the user needs to click on the "edit" button which uses a script on Bitbucket's site to switch to edit mode (there is no change to the URL). In other words, this is a limit of Bitbucket which we have no control over.

If, on the other hand, Bitbucket does offer an "edit" URL for documents, we would be happy to update MkDocs to support it. Please let us know what that URL scheme is and we'll add support.
Closing this due to a lack of information to act on.
@jkobti In my case I set `edit_uri` value to `src/master/docs` and that should point the users directly to the documentation file inside the Bitbucket repository source code. Therefore they can click on "edit" from the Bitbucket UI to make changes to the documentation .md file.

@waylan If that helps, this is how an edit URL in Bitbucket would open directly the file from the source code editor: https://bitbucket.org/elvismdev/my-project/src/master/docs/index.md?mode=edit&spa=0


@elvismdev thank you for providing Bitbiucket's edit URL format.

It would seem that MkDocs' current implementation is inadequate for general use. As it currently stands, an "edit URL" is composed by joining the following three parts:

    { repo_url }/{ edit_uri }/{ page.url }

However, Bitbucket would need:

    { repo_url }/src/{ branch }/{ page.url }?mode=edit&spa=0

And based on this recent report in #1437, a GitHub wiki would need:

    { repo_url }/wiki/{ page.title }/_edit

Given the above, it seems that rather than having a setting for `edit_uri` it would be more effective to have a setting for a `edit_url_template` which the user could define. Then MkDocs could use Python's `string.format()` to pass in the page object. I suppose it could accept the `repo_url` as well, but that shouldn't be necessary as the user could just include that in the `edit_url_template`.
Note that since #1224 we have supported query string based formats used by some hosts. However, this works because, like GitHub, the page path comes after the `edit_uri`. It's just that the page path is in a query string. All #1224 did was account for an edit_uri ending with the beginning of a query string and joined the page path appropriately.

What I'm proposing here would work in all cases with the various parts in any order. Users just need to be careful that to properly include or exclude slashes in the template. Under the hood it drastically simplifies things as we no longer need to worry about that. The problem is that the burden is now on the end user to do things correctly. 

There could also be an issue where a value included in a query string would need to be escaped differently that when part of the path. Not sure how to address that. Although we currently don't address it, so maybe its not necessary.
See also [some thoughts][1] on a plugin for handling this.

[1]: https://github.com/mkdocs/mkdocs/pull/1435#issuecomment-373219377
Hi,
It might be more appropriate to open a separate issue (or two) on this, but it appears that the "edit URL" is being incorrectly forced to lower-case.

I spotted this with my documentation (http://spinicist.github.io/QUIT) because I had only the `repo_url` specified and not `edit_uri`, and my `docs_dir` is "Docs" not "docs" (to fit my pre-existing naming convention before I started using `mkdocs`). The generated link was all lower-case, which resulted in a 404 on github.

I have now specified `edit_uri` to be "", which then makes `repo_url` work as a link to the repo front-page, which is actually what I wanted in the first place.

So:
1 - Unexpected behaviour that I had to set `edit_uri` to "" to get the desired behaviour
2 - `edit_uri` links appear to be incorrectly forced to lower-case.

Thanks for all the hard work, this is first problem I've had with `mkdocs` and it is very minor.
For GitHub, the following seems to work:

```yaml
repo_url: https://github.com/<owner>/<repo>
edit_uri: https://github.com/<owner>/<repo>/edit/<branch>/<docs-root>
```

e.g.

```yaml
repo_url: https://github.com/jacebrowning/datafiles
edit_uri: https://github.com/jacebrowning/datafiles/edit/develop/docs
```
#2212 highlights that even '/' is not a given before filename
For GitLab's Static Site Editor (from #2212), this would be something like:

`{{ repo_url }}/-/sse/{{ branch }}/docs%2F{{ page.file.src_path | urlencode }}`

Or optionally with a redirect back to the site in the query:

`{{ repo_url }}/-/sse/{{ branch }}/docs%2F{{ page.file.src_path | urlencode }}?return_url={{ page.canonical_url | urlencode }}`

Both the path and `return_url` would need to be URL-encoded. I don't think `config.docs_dir` can be used as it is the absolute path on the filesystem. `branch` would be the default branch from which the GitLab Pages are deployed (master/main etc). I guess this can be hardcoded by the project (edit: most likely needs to be URL-encoded as well, in case someone uses something like `master/pages` etc).

This makes me think @waylan that at least your idea of `edit_url_template` is necessary, but is some post-processing feasible with this (e.g. gitlab really needs these paths url-encoded). or would this be more feasible with plugins?

I'd be interested in adding this, although for now I managed to get GitLab's SSE to work with a small jinja hack in the `base.html` template.

For reference gitlab's middleman example template https://gitlab.com/gitlab-org/project-templates/static-site-editor-middleman/-/blob/master/source/layouts/layout.erb#L16-18 and helper: https://gitlab.com/gitlab-org/project-templates/static-site-editor-middleman/-/blob/master/helpers/custom_helpers.rb#L3
> This makes me think @waylan that at least your idea of `edit_url_template` is necessary, but is some post-processing feasible with this (e.g. gitlab really needs these paths url-encoded). or would this be more feasible with plugins?

If we go with an `edit_uri_template` my expectation is that we would pass in the parts as-is (no processing). That is why a plugin would be preferable. Any third party could create a plugin for any given service which meets the specific needs of that service. And you don't need to wait for us to get around to adding support for that service and then wait some more for us to make our next release, etc.

This should be easy for a plugin to address now. Each `Page` instance includes an `edit_url` attribute which simply contains the string representation of the URL which was build from the `repo_url`, `edit_uri` and `src_path`. If the `repo_url` and/or `edit_uri` are not available, the value is `None` (see [mkdocs/structure/pages.py#L107][2]).  A plugin could replace that value with a value it derives from plugin specific config options (using the [on_pre_page event][3]). The value of this approach is that each plugin can define its own config options, so those options can be whatever is needed for the supported service. The plugin could also easily include additional processing such as url-encoding.

The one issue that a plugin may struggle with is providing an icon for the service. This was discussed [here][1]. We may need to provide a mechanism for the plugin to provide the icon and make it available for the theme.

[1]: https://github.com/mkdocs/mkdocs/pull/1435#issuecomment-373219377
[2]: https://github.com/mkdocs/mkdocs/blob/master/mkdocs/structure/pages.py#L107
[3]: https://www.mkdocs.org/user-guide/plugins/#on_pre_page
Thanks for the quick feedback @waylan. Based on your info I put together a quick thing here: https://gitlab.com/nejch1/mkdocs-gitlab-static-site-editor, which does the job for me (initial PR [here](https://gitlab.com/nejch1/mkdocs-gitlab-static-site-editor/-/merge_requests/1/diffs) if you have an account to comment).

Does this makes sense? If so I might do some cleanup/polishing and publish this on pypi. Still unsure if it's overkill to have a plugin specifically for this, or if it should be a more generic plugin that can handle other use cases/platforms, but I can't think of any at the moment.

Hmm I wasn't even thinking about icons (some don't use repo-specific ones for Edit, see https://github.com/squidfunk/mkdocs-material/blob/094819e1e41b7a54aa2b5ed2a413cdc22fd5afd9/material/base.html#L161). But I see now the plugin should probably at least set `repo_name` to gitlab for the user even with the current setup, for on-prem instances.

Yes @nejch that’s the general idea. Whether you want to only support one service or multiple is up to you. 

<sub>Sent with <a href="http://githawk.com">GitHawk</a></sub>
I have created a plugin now for GitHub Wiki that changes the link to the correct format... For those needing GitHub Wiki support, give it a try?
https://pypi.org/project/mkdocs-github-wiki-edit-url/
For GitLab support too this would be useful. GitLab provides two ways to edit files:

1) a simple file editor - `https://gitlab.com/group/repo/-/edit/main/docs/index.md`
2) [Web IDE](https://docs.gitlab.com/ee/user/project/web_ide/) (more features) - `https://gitlab.com/-/ide/project/group/repo/edit/main/-/docs/index.md`

GitLab defaults to 2) Web IDE but mkdocs doesn't provide a way to construct that kind of URL using the edit_uri field (only 1) works)

Resolving patch:
diff --git a/mkdocs/config/config_options.py b/mkdocs/config/config_options.py
--- a/mkdocs/config/config_options.py
+++ b/mkdocs/config/config_options.py
@@ -2,10 +2,14 @@
 
 import ipaddress
 import os
+import string
 import sys
 import traceback
 import typing as t
+import warnings
+from collections import UserString
 from typing import NamedTuple
+from urllib.parse import quote as urlquote
 from urllib.parse import urlsplit, urlunsplit
 
 import markdown
@@ -347,12 +351,11 @@ def run_validation(self, value):
 
 
 class RepoURL(URL):
-    """
-    Repo URL Config Option
-
-    A small extension to the URL config that sets the repo_name and edit_uri,
-    based on the url if they haven't already been provided.
-    """
+    def __init__(self, *args, **kwargs):
+        warnings.warn(
+            "RepoURL is no longer used in MkDocs and will be removed.", DeprecationWarning
+        )
+        super().__init__(*args, **kwargs)
 
     def post_validation(self, config, key_name):
         repo_host = urlsplit(config['repo_url']).netloc.lower()
@@ -385,6 +388,88 @@ def post_validation(self, config, key_name):
         config['edit_uri'] = edit_uri
 
 
+class EditURI(Type):
+    def __init__(self, repo_url_key):
+        super().__init__(str)
+        self.repo_url_key = repo_url_key
+
+    def post_validation(self, config, key_name):
+        edit_uri = config.get(key_name)
+        repo_url = config.get(self.repo_url_key)
+
+        if edit_uri is None and repo_url is not None:
+            repo_host = urlsplit(repo_url).netloc.lower()
+            if repo_host == 'github.com' or repo_host == 'gitlab.com':
+                edit_uri = 'edit/master/docs/'
+            elif repo_host == 'bitbucket.org':
+                edit_uri = 'src/default/docs/'
+
+        # ensure a well-formed edit_uri
+        if edit_uri and not edit_uri.endswith('/'):
+            edit_uri += '/'
+
+        config[key_name] = edit_uri
+
+
+class EditURITemplate(OptionallyRequired):
+    class Formatter(string.Formatter):
+        def convert_field(self, value, conversion):
+            if conversion == 'q':
+                return urlquote(value, safe='')
+            return super().convert_field(value, conversion)
+
+    class Template(UserString):
+        def __init__(self, formatter, data):
+            super().__init__(data)
+            self.formatter = formatter
+            try:
+                self.format('', '')
+            except KeyError as e:
+                raise ValueError(f"Unknown template substitute: {e}")
+
+        def format(self, path, path_noext):
+            return self.formatter.format(self.data, path=path, path_noext=path_noext)
+
+    def __init__(self, edit_uri_key=None):
+        super().__init__()
+        self.edit_uri_key = edit_uri_key
+
+    def run_validation(self, value):
+        try:
+            return self.Template(self.Formatter(), value)
+        except Exception as e:
+            raise ValidationError(e)
+
+    def post_validation(self, config, key_name):
+        if self.edit_uri_key and config.get(key_name) and config.get(self.edit_uri_key):
+            self.warnings.append(
+                f"The option '{self.edit_uri_key}' has no effect when '{key_name}' is set."
+            )
+
+
+class RepoName(Type):
+    def __init__(self, repo_url_key):
+        super().__init__(str)
+        self.repo_url_key = repo_url_key
+
+    def post_validation(self, config, key_name):
+        repo_name = config.get(key_name)
+        repo_url = config.get(self.repo_url_key)
+
+        # derive repo_name from repo_url if unset
+        if repo_url is not None and repo_name is None:
+            repo_host = urlsplit(config['repo_url']).netloc.lower()
+            if repo_host == 'github.com':
+                repo_name = 'GitHub'
+            elif repo_host == 'bitbucket.org':
+                repo_name = 'Bitbucket'
+            elif repo_host == 'gitlab.com':
+                repo_name = 'GitLab'
+            else:
+                repo_name = repo_host.split('.')[0].title()
+            config[key_name] = repo_name
+
+
 class FilesystemObject(Type):
     """
     Base class for options that point to filesystem objects.
diff --git a/mkdocs/config/defaults.py b/mkdocs/config/defaults.py
--- a/mkdocs/config/defaults.py
+++ b/mkdocs/config/defaults.py
@@ -63,17 +63,18 @@ class _MkDocsConfig:
     True generates nicer URLs, but False is useful if browsing the output on
     a filesystem."""
 
-    repo_url = config_options.RepoURL()
+    repo_url = config_options.URL()
     """Specify a link to the project source repo to be included
     in the documentation pages."""
 
-    repo_name = config_options.Type(str)
+    repo_name = config_options.RepoName('repo_url')
     """A name to use for the link to the project source repo.
     Default, If repo_url is unset then None, otherwise
     "GitHub", "Bitbucket" or "GitLab" for known url or Hostname
     for unknown urls."""
 
-    edit_uri = config_options.Type(str)
+    edit_uri_template = config_options.EditURITemplate('edit_uri')
+    edit_uri = config_options.EditURI('repo_url')
     """Specify a URI to the docs dir in the project source repo, relative to the
     repo_url. When set, a link directly to the page in the source repo will
     be added to the generated HTML. If repo_url is not set also, this option
diff --git a/mkdocs/structure/pages.py b/mkdocs/structure/pages.py
--- a/mkdocs/structure/pages.py
+++ b/mkdocs/structure/pages.py
@@ -41,7 +41,9 @@ def __init__(self, title: Optional[str], file: File, config: Config) -> None:
         self.update_date = get_build_date()
 
         self._set_canonical_url(config.get('site_url', None))
-        self._set_edit_url(config.get('repo_url', None), config.get('edit_uri', None))
+        self._set_edit_url(
+            config.get('repo_url', None), config.get('edit_uri'), config.get('edit_uri_template')
+        )
 
         # Placeholders to be filled in later in the build process.
         self.markdown = None
@@ -172,10 +174,20 @@ def _set_canonical_url(self, base: Optional[str]) -> None:
             self.canonical_url = None
             self.abs_url = None
 
-    def _set_edit_url(self, repo_url: Optional[str], edit_uri: Optional[str]) -> None:
-        if edit_uri:
+    def _set_edit_url(
+        self,
+        repo_url: Optional[str],
+        edit_uri: Optional[str] = None,
+        edit_uri_template: Optional[str] = None,
+    ) -> None:
+        if edit_uri or edit_uri_template:
             src_uri = self.file.src_uri
-            edit_uri += src_uri
+            if edit_uri_template:
+                noext = posixpath.splitext(src_uri)[0]
+                edit_uri = edit_uri_template.format(path=src_uri, path_noext=noext)
+            else:
+                assert edit_uri is not None and edit_uri.endswith('/')
+                edit_uri += src_uri
             if repo_url:
                 # Ensure urljoin behavior is correct
                 if not edit_uri.startswith(('?', '#')) and not repo_url.endswith('/'):


Test patch:
diff --git a/mkdocs/tests/config/config_options_tests.py b/mkdocs/tests/config/config_options_tests.py
--- a/mkdocs/tests/config/config_options_tests.py
+++ b/mkdocs/tests/config/config_options_tests.py
@@ -417,11 +417,12 @@ class Schema:
             self.get_config(Schema, {'option': 1})
 
 
-class RepoURLTest(TestCase):
+class EditURITest(TestCase):
     class Schema:
-        repo_url = config_options.RepoURL()
-        repo_name = config_options.Type(str)
-        edit_uri = config_options.Type(str)
+        repo_url = config_options.URL()
+        repo_name = config_options.RepoName('repo_url')
+        edit_uri_template = config_options.EditURITemplate('edit_uri')
+        edit_uri = config_options.EditURI('repo_url')
 
     def test_repo_name_github(self):
         config = self.get_config(
@@ -479,7 +480,7 @@ def test_edit_uri_custom(self):
             self.Schema,
             {'repo_url': "https://launchpad.net/python-tuskarclient"},
         )
-        self.assertEqual(config.get('edit_uri'), '')
+        self.assertEqual(config.get('edit_uri'), None)
         self.assertEqual(config['repo_url'], "https://launchpad.net/python-tuskarclient")
 
     def test_repo_name_custom_and_empty_edit_uri(self):
@@ -489,6 +490,60 @@ def test_repo_name_custom_and_empty_edit_uri(self):
         )
         self.assertEqual(config.get('edit_uri'), 'edit/master/docs/')
 
+    def test_edit_uri_template_ok(self):
+        config = self.get_config(
+            self.Schema,
+            {
+                'repo_url': "https://github.com/mkdocs/mkdocs",
+                'edit_uri_template': 'edit/foo/docs/{path}',
+            },
+        )
+        self.assertEqual(config['edit_uri_template'], 'edit/foo/docs/{path}')
+
+    def test_edit_uri_template_errors(self):
+        with self.expect_error(
+            edit_uri_template=re.compile(r'.*[{}].*')  # Complains about unclosed '{' or missing '}'
+        ):
+            self.get_config(
+                self.Schema,
+                {
+                    'repo_url': "https://github.com/mkdocs/mkdocs",
+                    'edit_uri_template': 'edit/master/{path',
+                },
+            )
+
+        with self.expect_error(edit_uri_template=re.compile(r'.*\bz\b.*')):
+            self.get_config(
+                self.Schema,
+                {
+                    'repo_url': "https://github.com/mkdocs/mkdocs",
+                    'edit_uri_template': 'edit/master/{path!z}',
+                },
+            )
+
+        with self.expect_error(edit_uri_template="Unknown template substitute: 'foo'"):
+            self.get_config(
+                self.Schema,
+                {
+                    'repo_url': "https://github.com/mkdocs/mkdocs",
+                    'edit_uri_template': 'edit/master/{foo}',
+                },
+            )
+
+    def test_edit_uri_template_warning(self):
+        config = self.get_config(
+            self.Schema,
+            {
+                'repo_url': "https://github.com/mkdocs/mkdocs",
+                'edit_uri': 'edit',
+                'edit_uri_template': 'edit/master/{path}',
+            },
+            warnings=dict(
+                edit_uri_template="The option 'edit_uri' has no effect when 'edit_uri_template' is set."
+            ),
+        )
+        self.assertEqual(config['edit_uri_template'], 'edit/master/{path}')
+
 
 class ListOfItemsTest(TestCase):
     def test_int_type(self):
diff --git a/mkdocs/tests/structure/page_tests.py b/mkdocs/tests/structure/page_tests.py
--- a/mkdocs/tests/structure/page_tests.py
+++ b/mkdocs/tests/structure/page_tests.py
@@ -429,156 +429,139 @@ def test_BOM(self, docs_dir):
         self.assertEqual(pg.markdown, md_src)
         self.assertEqual(pg.meta, {})
 
-    def test_page_edit_url(self):
+    def test_page_edit_url(
+        self, paths={'testing.md': 'testing/', 'sub1/non-index.md': 'sub1/non-index/'}
+    ):
         for case in [
             dict(
                 config={'repo_url': 'http://github.com/mkdocs/mkdocs'},
                 edit_url='http://github.com/mkdocs/mkdocs/edit/master/docs/testing.md',
+                edit_url2='http://github.com/mkdocs/mkdocs/edit/master/docs/sub1/non-index.md',
             ),
             dict(
                 config={'repo_url': 'https://github.com/mkdocs/mkdocs/'},
                 edit_url='https://github.com/mkdocs/mkdocs/edit/master/docs/testing.md',
+                edit_url2='https://github.com/mkdocs/mkdocs/edit/master/docs/sub1/non-index.md',
             ),
             dict(
                 config={'repo_url': 'http://example.com'},
                 edit_url=None,
+                edit_url2=None,
             ),
             dict(
                 config={'repo_url': 'http://example.com', 'edit_uri': 'edit/master'},
                 edit_url='http://example.com/edit/master/testing.md',
+                edit_url2='http://example.com/edit/master/sub1/non-index.md',
             ),
             dict(
                 config={'repo_url': 'http://example.com', 'edit_uri': '/edit/master'},
                 edit_url='http://example.com/edit/master/testing.md',
+                edit_url2='http://example.com/edit/master/sub1/non-index.md',
             ),
             dict(
                 config={'repo_url': 'http://example.com/foo/', 'edit_uri': '/edit/master/'},
                 edit_url='http://example.com/edit/master/testing.md',
+                edit_url2='http://example.com/edit/master/sub1/non-index.md',
             ),
             dict(
                 config={'repo_url': 'http://example.com/foo', 'edit_uri': '/edit/master/'},
                 edit_url='http://example.com/edit/master/testing.md',
+                edit_url2='http://example.com/edit/master/sub1/non-index.md',
             ),
             dict(
                 config={'repo_url': 'http://example.com/foo/', 'edit_uri': '/edit/master'},
                 edit_url='http://example.com/edit/master/testing.md',
+                edit_url2='http://example.com/edit/master/sub1/non-index.md',
             ),
             dict(
                 config={'repo_url': 'http://example.com/foo/', 'edit_uri': 'edit/master/'},
                 edit_url='http://example.com/foo/edit/master/testing.md',
+                edit_url2='http://example.com/foo/edit/master/sub1/non-index.md',
             ),
             dict(
                 config={'repo_url': 'http://example.com/foo', 'edit_uri': 'edit/master/'},
                 edit_url='http://example.com/foo/edit/master/testing.md',
+                edit_url2='http://example.com/foo/edit/master/sub1/non-index.md',
             ),
             dict(
                 config={'repo_url': 'http://example.com', 'edit_uri': '?query=edit/master'},
                 edit_url='http://example.com?query=edit/master/testing.md',
+                edit_url2='http://example.com?query=edit/master/sub1/non-index.md',
             ),
             dict(
                 config={'repo_url': 'http://example.com/', 'edit_uri': '?query=edit/master/'},
                 edit_url='http://example.com/?query=edit/master/testing.md',
+                edit_url2='http://example.com/?query=edit/master/sub1/non-index.md',
             ),
             dict(
                 config={'repo_url': 'http://example.com', 'edit_uri': '#edit/master'},
                 edit_url='http://example.com#edit/master/testing.md',
+                edit_url2='http://example.com#edit/master/sub1/non-index.md',
             ),
             dict(
                 config={'repo_url': 'http://example.com/', 'edit_uri': '#edit/master/'},
                 edit_url='http://example.com/#edit/master/testing.md',
+                edit_url2='http://example.com/#edit/master/sub1/non-index.md',
             ),
             dict(
                 config={'edit_uri': 'http://example.com/edit/master'},
                 edit_url='http://example.com/edit/master/testing.md',
+                edit_url2='http://example.com/edit/master/sub1/non-index.md',
             ),
             dict(
-                config={'repo_url': 'http://example.com', 'edit_uri': ''},  # Set to blank value
-                edit_url=None,
-            ),
-            dict(config={}, edit_url=None),  # Nothing defined
-        ]:
-            with self.subTest(case['config']):
-                cfg = load_config(**case['config'])
-                fl = File('testing.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls'])
-                pg = Page('Foo', fl, cfg)
-                self.assertEqual(pg.url, 'testing/')
-                self.assertEqual(pg.edit_url, case['edit_url'])
-
-    def test_nested_page_edit_url(self, file_src_path='sub1/non-index.md'):
-        for case in [
-            dict(
-                config={'repo_url': 'http://github.com/mkdocs/mkdocs'},
-                edit_url='http://github.com/mkdocs/mkdocs/edit/master/docs/sub1/non-index.md',
-            ),
-            dict(
-                config={'repo_url': 'https://github.com/mkdocs/mkdocs/'},
-                edit_url='https://github.com/mkdocs/mkdocs/edit/master/docs/sub1/non-index.md',
-            ),
-            dict(
-                config={'repo_url': 'http://example.com'},
-                edit_url=None,
-            ),
-            dict(
-                config={'repo_url': 'http://example.com', 'edit_uri': 'edit/master'},
-                edit_url='http://example.com/edit/master/sub1/non-index.md',
-            ),
-            dict(
-                config={'repo_url': 'http://example.com', 'edit_uri': '/edit/master'},
-                edit_url='http://example.com/edit/master/sub1/non-index.md',
-            ),
-            dict(
-                config={'repo_url': 'http://example.com/foo/', 'edit_uri': '/edit/master/'},
-                edit_url='http://example.com/edit/master/sub1/non-index.md',
-            ),
-            dict(
-                config={'repo_url': 'http://example.com/foo', 'edit_uri': '/edit/master/'},
-                edit_url='http://example.com/edit/master/sub1/non-index.md',
-            ),
-            dict(
-                config={'repo_url': 'http://example.com/foo/', 'edit_uri': '/edit/master'},
-                edit_url='http://example.com/edit/master/sub1/non-index.md',
-            ),
-            dict(
-                config={'repo_url': 'http://example.com/foo/', 'edit_uri': 'edit/master/'},
-                edit_url='http://example.com/foo/edit/master/sub1/non-index.md',
-            ),
-            dict(
-                config={'repo_url': 'http://example.com/foo', 'edit_uri': 'edit/master/'},
-                edit_url='http://example.com/foo/edit/master/sub1/non-index.md',
-            ),
-            dict(
-                config={'repo_url': 'http://example.com', 'edit_uri': '?query=edit/master'},
-                edit_url='http://example.com?query=edit/master/sub1/non-index.md',
-            ),
-            dict(
-                config={'repo_url': 'http://example.com/', 'edit_uri': '?query=edit/master/'},
-                edit_url='http://example.com/?query=edit/master/sub1/non-index.md',
+                config={'edit_uri_template': 'https://github.com/project/repo/wiki/{path_noext}'},
+                edit_url='https://github.com/project/repo/wiki/testing',
+                edit_url2='https://github.com/project/repo/wiki/sub1/non-index',
             ),
             dict(
-                config={'repo_url': 'http://example.com', 'edit_uri': '#edit/master'},
-                edit_url='http://example.com#edit/master/sub1/non-index.md',
+                config={
+                    'repo_url': 'https://github.com/project/repo/wiki',
+                    'edit_uri_template': '{path_noext}/_edit',
+                },
+                edit_url='https://github.com/project/repo/wiki/testing/_edit',
+                edit_url2='https://github.com/project/repo/wiki/sub1/non-index/_edit',
             ),
             dict(
-                config={'repo_url': 'http://example.com/', 'edit_uri': '#edit/master/'},
-                edit_url='http://example.com/#edit/master/sub1/non-index.md',
+                config={
+                    'repo_url': 'https://gitlab.com/project/repo',
+                    'edit_uri_template': '-/sse/master/docs%2F{path!q}',
+                },
+                edit_url='https://gitlab.com/project/repo/-/sse/master/docs%2Ftesting.md',
+                edit_url2='https://gitlab.com/project/repo/-/sse/master/docs%2Fsub1%2Fnon-index.md',
             ),
             dict(
-                config={'edit_uri': 'http://example.com/edit/master'},
-                edit_url='http://example.com/edit/master/sub1/non-index.md',
+                config={
+                    'repo_url': 'https://bitbucket.org/project/repo/',
+                    'edit_uri_template': 'src/master/docs/{path}?mode=edit',
+                },
+                edit_url='https://bitbucket.org/project/repo/src/master/docs/testing.md?mode=edit',
+                edit_url2='https://bitbucket.org/project/repo/src/master/docs/sub1/non-index.md?mode=edit',
             ),
             dict(
-                config={'repo_url': 'http://example.com', 'edit_uri': ''},  # Set to blank value
+                config={
+                    'repo_url': 'http://example.com',
+                    'edit_uri': '',
+                    'edit_uri_template': '',
+                },  # Set to blank value
                 edit_url=None,
+                edit_url2=None,
             ),
+            dict(config={}, edit_url=None, edit_url2=None),  # Nothing defined
         ]:
-            with self.subTest(case['config']):
-                cfg = load_config(**case['config'], docs_dir=self.DOCS_DIR)
-                fl = File(
-                    file_src_path, cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls']
-                )
-                pg = Page('Foo', fl, cfg)
-                self.assertEqual(pg.url, 'sub1/non-index/')
-                self.assertEqual(pg.edit_url, case['edit_url'])
+            for i, path in enumerate(paths, 1):
+                edit_url_key = f'edit_url{i}' if i > 1 else 'edit_url'
+                with self.subTest(case['config'], path=path):
+                    cfg = load_config(**case['config'])
+                    fl = File(path, cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls'])
+                    pg = Page('Foo', fl, cfg)
+                    self.assertEqual(pg.url, paths[path])
+                    self.assertEqual(pg.edit_url, case[edit_url_key])
+
+    @unittest.skipUnless(sys.platform.startswith("win"), "requires Windows")
+    def test_page_edit_url_windows(self):
+        self.test_page_edit_url(
+            paths={'testing.md': 'testing/', 'sub1\\non-index.md': 'sub1/non-index/'}
+        )
 
     def test_page_edit_url_warning(self):
         for case in [
@@ -600,10 +583,6 @@ def test_page_edit_url_warning(self):
                 self.assertEqual(pg.edit_url, case['edit_url'])
                 self.assertEqual(cm.output, [case['warning']])
 
-    @unittest.skipUnless(sys.platform.startswith("win"), "requires Windows")
-    def test_nested_page_edit_url_windows(self):
-        self.test_nested_page_edit_url(file_src_path='sub1\\non-index.md')
-
     def test_page_render(self):
         cfg = load_config()
         fl = File('testing.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls'])