diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml
index 4df95ea2..2681345c 100644
--- a/.github/workflows/docs.yml
+++ b/.github/workflows/docs.yml
@@ -39,8 +39,6 @@ jobs:
           set: |
             *.cache-from=type=gha,scope=docs
             *.cache-to=type=gha,scope=docs,mode=max
-        env:
-          DOCS_BASEURL: ${{ steps.pages.outputs.base_path }}
       - name: Fix permissions
         run: |
           chmod -c -R +rX "./build/docs" | while read line; do
diff --git a/docker-bake.hcl b/docker-bake.hcl
index 540e4be4..9512b17f 100644
--- a/docker-bake.hcl
+++ b/docker-bake.hcl
@@ -91,15 +91,8 @@ target "image-all" {
   ]
 }
 
-variable "DOCS_BASEURL" {
-  default = null
-}
-
 target "_common_docs" {
   dockerfile = "./dockerfiles/docs.Dockerfile"
-  args = {
-    DOCS_BASEURL = DOCS_BASEURL
-  }
 }
 
 target "docs-export" {
diff --git a/dockerfiles/docs.Dockerfile b/dockerfiles/docs.Dockerfile
index 1660e0cc..3db6e0c6 100644
--- a/dockerfiles/docs.Dockerfile
+++ b/dockerfiles/docs.Dockerfile
@@ -16,9 +16,8 @@ COPY --from=hugo $GOPATH/bin/hugo /bin/hugo
 WORKDIR /src
 
 FROM build-base AS build
-ARG DOCS_BASEURL=/
 RUN --mount=type=bind,rw,source=docs,target=. \
-    hugo --gc --minify --destination /out -b $DOCS_BASEURL
+    hugo --gc --minify --destination /out
 
 FROM build-base AS server
 COPY docs .
@@ -29,8 +28,12 @@ FROM scratch AS out
 COPY --from=build /out /
 
 FROM wjdp/htmltest:v0.17.0 AS test
+# Copy the site to a public/distribution subdirectory
+# This is a workaround for a limitation in htmltest, see:
+# https://github.com/wjdp/htmltest/issues/45
+WORKDIR /test/public/distribution
+COPY --from=build /out .
 WORKDIR /test
-COPY --from=build /out ./public
 ADD docs/.htmltest.yml .htmltest.yml
 RUN --mount=type=cache,target=tmp/.htmltest \
     htmltest
diff --git a/docs/content/about/configuration.md b/docs/content/about/configuration.md
index 4107c0b3..41519e05 100644
--- a/docs/content/about/configuration.md
+++ b/docs/content/about/configuration.md
@@ -434,17 +434,17 @@ The `storage` option is **required** and defines which storage backend is in
 use. You must configure exactly one backend. If you configure more, the registry
 returns an error. You can choose any of these backend storage drivers:
 
-| Storage driver      | Description                                                                                                                                                                                                                                                                              |
-|---------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
-| `filesystem`        | Uses the local disk to store registry files. It is ideal for development and may be appropriate for some small-scale production applications. See the [driver's reference documentation](/storage-drivers/filesystem). |
-| `azure`             | Uses Microsoft Azure Blob Storage. See the [driver's reference documentation](/storage-drivers/azure).                                                                                                               |
-| `gcs`               | Uses Google Cloud Storage. See the [driver's reference documentation](/storage-drivers/gcs).                                                                                                                           |
-| `s3`                | Uses Amazon Simple Storage Service (S3) and compatible Storage Services. See the [driver's reference documentation](/storage-drivers/s3).                                                                            |
+| Storage driver | Description                                                                                                                                                                                                                 |
+| -------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| `filesystem`   | Uses the local disk to store registry files. It is ideal for development and may be appropriate for some small-scale production applications. See the [driver's reference documentation](../storage-drivers/filesystem.md). |
+| `azure`        | Uses Microsoft Azure Blob Storage. See the [driver's reference documentation](../storage-drivers/azure.md).                                                                                                                 |
+| `gcs`          | Uses Google Cloud Storage. See the [driver's reference documentation](../storage-drivers/gcs.md).                                                                                                                           |
+| `s3`           | Uses Amazon Simple Storage Service (S3) and compatible Storage Services. See the [driver's reference documentation](../storage-drivers/s3.md).                                                                              |
 
 For testing only, you can use the [`inmemory` storage
-driver](/storage-drivers/inmemory).
+driver](../storage-drivers/inmemory.md).
 If you would like to run a registry from volatile memory, use the
-[`filesystem` driver](/storage-drivers/filesystem)
+[`filesystem` driver](../storage-drivers/filesystem.md)
 on a ramdisk.
 
 If you are deploying a registry on Windows, a Windows volume mounted from the
@@ -593,7 +593,7 @@ security.
 
 
 For more information about Token based authentication configuration, see the
-[specification](/spec/auth/token).
+[specification](../spec/auth/token.md).
 
 ### `htpasswd`
 
@@ -1100,7 +1100,7 @@ proxy:
 
 The `proxy` structure allows a registry to be configured as a pull-through cache
 to Docker Hub. See
-[mirror](/recipes/mirror)
+[mirror](../recipes/mirror.md)
 for more information. Pushing to a registry configured as a pull-through cache
 is unsupported.
 
diff --git a/docs/content/about/deploying.md b/docs/content/about/deploying.md
index 1c022bc2..bfa35955 100644
--- a/docs/content/about/deploying.md
+++ b/docs/content/about/deploying.md
@@ -9,7 +9,7 @@ A registry is an instance of the `registry` image, and runs within Docker.
 
 This topic provides basic information about deploying and configuring a
 registry. For an exhaustive list of configuration options, see the
-[configuration reference](../configuration).
+[configuration reference](configuration.md).
 
 If you have an air-gapped datacenter, see
 [Considerations for air-gapped registries](#considerations-for-air-gapped-registries).
@@ -27,7 +27,7 @@ The registry is now ready to use.
 > **Warning**: These first few examples show registry configurations that are
 > only appropriate for testing. A production-ready registry must be protected by
 > TLS and should ideally use an access-control mechanism. Keep reading and then
-> continue to the [configuration guide](../configuration) to deploy a
+> continue to the [configuration guide](configuration.md) to deploy a
 > production-ready registry.
 
 ## Copy an image from Docker Hub to your registry
@@ -94,7 +94,7 @@ To configure the container, you can pass additional or modified options to the
 `docker run` command.
 
 The following sections provide basic guidelines for configuring your registry.
-For more details, see the [registry configuration reference](../configuration).
+For more details, see the [registry configuration reference](configuration.md).
 
 ### Start the registry automatically
 
@@ -166,8 +166,8 @@ $ docker run -d \
 By default, the registry stores its data on the local filesystem, whether you
 use a bind mount or a volume. You can store the registry data in an Amazon S3
 bucket, Google Cloud Platform, or on another storage back-end by using
-[storage drivers](/storage-drivers). For more information, see
-[storage configuration options](../configuration#storage).
+[storage drivers](../storage-drivers/_index.md). For more information, see
+[storage configuration options](configuration.md#storage).
 
 ## Run an externally-accessible registry
 
@@ -252,13 +252,13 @@ The registry supports using Let's Encrypt to automatically obtain a
 browser-trusted certificate. For more information on Let's Encrypt, see
 [https://letsencrypt.org/how-it-works/](https://letsencrypt.org/how-it-works/)
 and the relevant section of the
-[registry configuration](../configuration#letsencrypt).
+[registry configuration](configuration.md#letsencrypt).
 
 ### Use an insecure registry (testing only)
 
 It is possible to use a self-signed certificate, or to use our registry
 insecurely. Unless you have set up verification for your self-signed
-certificate, this is for testing only. See [run an insecure registry](../insecure).
+certificate, this is for testing only. See [run an insecure registry](insecure.md).
 
 ## Run the registry as a service
 
@@ -462,20 +462,20 @@ using htpasswd, all authentication attempts will fail.
 {{< hint type=note title="X509 errors" >}}
 X509 errors usually indicate that you are attempting to use
 a self-signed certificate without configuring the Docker daemon correctly.
-See [run an insecure registry](../insecure).
+See [run an insecure registry](insecure.md).
 {{< /hint >}}
 
 ### More advanced authentication
 
 You may want to leverage more advanced basic auth implementations by using a
-proxy in front of the registry. See the [recipes list](/recipes/).
+proxy in front of the registry. See the [recipes list](../recipes/_index.md).
 
 The registry also supports delegated authentication which redirects users to a
 specific trusted token server. This approach is more complicated to set up, and
 only makes sense if you need to fully configure ACLs and need more control over
 the registry's integration into your global authorization and authentication
-systems. Refer to the following [background information](/spec/auth/token) and
-[configuration information here](../configuration#auth).
+systems. Refer to the following [background information](../spec/auth/token.md) and
+[configuration information here](configuration.md#auth).
 
 This approach requires you to implement your own authentication system or
 leverage a third-party implementation.
@@ -572,9 +572,9 @@ artifacts.
 
 More specific and advanced information is available in the following sections:
 
-- [Configuration reference](../configuration)
-- [Working with notifications](../notifications)
-- [Advanced "recipes"](/recipes)
-- [Registry API](/spec/api)
-- [Storage driver model](/storage-drivers)
-- [Token authentication](/spec/auth/token)
+- [Configuration reference](configuration.md)
+- [Working with notifications](notifications.md)
+- [Advanced "recipes"](../recipes/_index.md)
+- [Registry API](../spec/api.md)
+- [Storage driver model](../storage-drivers/_index.md)
+- [Token authentication](../spec/auth/token.md)
diff --git a/docs/content/about/garbage-collection.md b/docs/content/about/garbage-collection.md
index dd1768d6..ee2d2fb2 100644
--- a/docs/content/about/garbage-collection.md
+++ b/docs/content/about/garbage-collection.md
@@ -21,15 +21,15 @@ that certain layers no longer exist on the filesystem.
 
 Filesystem layers are stored by their content address in the Registry. This
 has many advantages, one of which is that data is stored once and referred to by manifests.
-See [here](../compatibility#content-addressable-storage-cas) for more details.
+See [here](compatibility.md#content-addressable-storage-cas) for more details.
 
 Layers are therefore shared amongst manifests; each manifest maintains a reference
 to the layer. As long as a layer is referenced by one manifest, it cannot be garbage
 collected.
 
 Manifests and layers can be `deleted` with the registry API (refer to the API
-documentation [here](/spec/api#deleting-a-layer) and
-[here](/spec/api#deleting-an-image) for details). This API removes references
+documentation [here](../spec/api.md#deleting-a-layer) and
+[here](../spec/api.md#deleting-an-image) for details). This API removes references
 to the target and makes them eligible for garbage collection. It also makes them
 unable to be read via the API.
 
diff --git a/docs/content/about/insecure.md b/docs/content/about/insecure.md
index e9f55f15..c848f869 100644
--- a/docs/content/about/insecure.md
+++ b/docs/content/about/insecure.md
@@ -72,7 +72,7 @@ This is more secure than the insecure registry solution.
 
    Be sure to use the name `myregistry.domain.com` as a CN.
 
-2. Use the result to [start your registry with TLS enabled](../deploying#get-a-certificate).
+2. Use the result to [start your registry with TLS enabled](deploying.md#get-a-certificate).
 
 3. Instruct every Docker daemon to trust that certificate. The way to do this
    depends on your OS.
diff --git a/docs/content/about/notifications.md b/docs/content/about/notifications.md
index d5b11d5d..8e2b642e 100644
--- a/docs/content/about/notifications.md
+++ b/docs/content/about/notifications.md
@@ -10,7 +10,7 @@ pushes and pulls and layer pushes and pulls. These actions are serialized into
 events. The events are queued into a registry-internal broadcast system which
 queues and dispatches events to [_Endpoints_](#endpoints).
 
-![Workflow of registry notifications](../../images/notifications.png)
+![Workflow of registry notifications](/distribution/images/notifications.png)
 
 ## Endpoints
 
@@ -45,7 +45,7 @@ The above would configure the registry with an endpoint to send events to
 5 failures happen consecutively, the registry backs off for 1 second before
 trying again.
 
-For details on the fields, see the [configuration documentation](../configuration/#notifications).
+For details on the fields, see the [configuration documentation](configuration.md#notifications).
 
 A properly configured endpoint should lead to a log message from the registry
 upon startup:
diff --git a/docs/content/recipes/apache.md b/docs/content/recipes/apache.md
index 84639275..d118c5fe 100644
--- a/docs/content/recipes/apache.md
+++ b/docs/content/recipes/apache.md
@@ -12,7 +12,7 @@ Usually, that includes enterprise setups using LDAP/AD on the backend and a SSO
 
 ### Alternatives
 
-If you just want authentication for your registry, and are happy maintaining users access separately, you should really consider sticking with the native [basic auth registry feature](/about/deploying#native-basic-auth).
+If you just want authentication for your registry, and are happy maintaining users access separately, you should really consider sticking with the native [basic auth registry feature](../about/deploying.md#native-basic-auth).
 
 ### Solution
 
diff --git a/docs/content/recipes/mirror.md b/docs/content/recipes/mirror.md
index f11f9cba..d5ec06a5 100644
--- a/docs/content/recipes/mirror.md
+++ b/docs/content/recipes/mirror.md
@@ -107,7 +107,7 @@ proxy:
 
 > **Warning**: For the scheduler to clean up old entries, `delete` must
 > be enabled in the registry configuration. See
-> [Registry Configuration](/about/configuration) for more details.
+> [Registry Configuration](../about/configuration.md) for more details.
 
 ### Configure the Docker daemon
 
diff --git a/docs/content/recipes/nginx.md b/docs/content/recipes/nginx.md
index d2c4ff35..8db6e4f1 100644
--- a/docs/content/recipes/nginx.md
+++ b/docs/content/recipes/nginx.md
@@ -17,7 +17,7 @@ mechanism fronting their internal http portal.
 
 If you just want authentication for your registry, and are happy maintaining
 users access separately, you should really consider sticking with the native
-[basic auth registry feature](/about/deploying#native-basic-auth).
+[basic auth registry feature](../about/deploying.md#native-basic-auth).
 
 ### Solution
 
diff --git a/docs/content/spec/_index.md b/docs/content/spec/_index.md
index 5e9729b8..4f2d0e93 100644
--- a/docs/content/spec/_index.md
+++ b/docs/content/spec/_index.md
@@ -6,7 +6,7 @@ keywords: registry, service, images, repository,  json
 
 # Docker Registry Reference
 
-* [HTTP API V2](api)
-* [Storage Driver](/storage-drivers/)
-* [Token Authentication Specification](auth/token)
-* [Token Authentication Implementation](auth/jwt)
+* [HTTP API V2](api.md)
+* [Storage Driver](../storage-drivers/_index.md)
+* [Token Authentication Specification](auth/token.md)
+* [Token Authentication Implementation](auth/jwt.md)
diff --git a/docs/content/spec/api.md b/docs/content/spec/api.md
index 45d6c0d0..2bab67d3 100644
--- a/docs/content/spec/api.md
+++ b/docs/content/spec/api.md
@@ -416,7 +416,7 @@ reference may include a tag or digest.
 
 The client should include an Accept header indicating which manifest content
 types it supports. For more details on the manifest format and content types,
-see [Image Manifest Version 2, Schema 2](../manifest-v2-2).
+see [Image Manifest Version 2, Schema 2](manifest-v2-2.md).
 In a successful response, the Content-Type header will indicate which manifest type is being returned.
 
 A `404 Not Found` response will be returned if the image is unknown to the
@@ -840,7 +840,7 @@ Content-Type: <manifest media type>
 The `name` and `reference` fields of the response body must match those
 specified in the URL. The `reference` field may be a "tag" or a "digest". The
 content type should match the type of the manifest being uploaded, as specified
-in [Image Manifest Version 2, Schema 2](../manifest-v2-2).
+in [Image Manifest Version 2, Schema 2](manifest-v2-2.md).
 
 If there is a problem with pushing the manifest, a relevant 4xx response will
 be returned with a JSON error message. Please see the
@@ -1088,7 +1088,7 @@ response will be issued instead.
 
     Accept: application/vnd.docker.distribution.manifest.v2+json
 
-> for more details, see: [compatibility](/about/compatibility#content-addressable-storage-cas)
+> for more details, see: [compatibility](../about/compatibility.md#content-addressable-storage-cas)
 
 ## Detail
 
diff --git a/docs/content/spec/auth/oauth.md b/docs/content/spec/auth/oauth.md
index ca61d2ed..8e5f759d 100644
--- a/docs/content/spec/auth/oauth.md
+++ b/docs/content/spec/auth/oauth.md
@@ -12,7 +12,7 @@ reference for the protocol and HTTP endpoints described here.
 
 **Note**: Not all token servers implement oauth2. If the request to the endpoint
 returns `404` using the HTTP `POST` method, refer to
-[Token Documentation](../token) for using the HTTP `GET` method supported by all
+[Token Documentation](token.md) for using the HTTP `GET` method supported by all
 token servers.
 
 ## Refresh token format
diff --git a/docs/content/spec/auth/scope.md b/docs/content/spec/auth/scope.md
index a2236bb7..1f2059ad 100644
--- a/docs/content/spec/auth/scope.md
+++ b/docs/content/spec/auth/scope.md
@@ -144,7 +144,7 @@ Each JWT access token may only have a single subject and audience but multiple
 resource scopes. The subject and audience are put into standard JWT fields
 `sub` and `aud`. The resource scope is put into the `access` field. The
 structure of the access field can be seen in the
-[jwt documentation](../jwt).
+[jwt documentation](jwt.md).
 
 ## Refresh Tokens
 
diff --git a/docs/content/spec/auth/token.md b/docs/content/spec/auth/token.md
index cc9d940e..a878b328 100644
--- a/docs/content/spec/auth/token.md
+++ b/docs/content/spec/auth/token.md
@@ -8,7 +8,7 @@ keywords: registry, on-prem, images, tags, repository, distribution, Bearer auth
 
 This document outlines the v2 Distribution registry authentication scheme:
 
-![v2 registry auth](../../../images/v2-registry-auth.png)
+![v2 registry auth](/distribution/images/v2-registry-auth.png)
 
 1. Attempt to begin a push/pull operation with the registry.
 2. If the registry requires authorization it will return a `401 Unauthorized`
@@ -188,7 +188,7 @@ https://auth.docker.io/token?service=registry.docker.io&scope=repository:samalba
 
 The token server should first attempt to authenticate the client using any
 authentication credentials provided with the request. From Docker 1.11 the
-Docker engine supports both Basic Authentication and [OAuth2](../oauth) for
+Docker engine supports both Basic Authentication and [OAuth2](oauth.md) for
 getting tokens. Docker 1.10 and before, the registry client in the Docker Engine
 only supports Basic Authentication. If an attempt to authenticate to the token
 server fails, the token server should return a `401 Unauthorized` response
diff --git a/docs/content/spec/manifest-v2-2.md b/docs/content/spec/manifest-v2-2.md
index 1e85fad7..a4c7e929 100644
--- a/docs/content/spec/manifest-v2-2.md
+++ b/docs/content/spec/manifest-v2-2.md
@@ -71,7 +71,7 @@ image manifest based on the Content-Type returned in the HTTP response.
     - **`digest`** *string*
 
         The digest of the content, as defined by the
-        [Registry V2 HTTP API Specificiation](../api#digest-parameter).
+        [Registry V2 HTTP API Specification](api.md#digest-parameter).
 
     - **`platform`** *object*
 
@@ -187,7 +187,7 @@ image. It's the direct replacement for the schema-1 manifest.
     - **`digest`** *string*
 
         The digest of the content, as defined by the
-        [Registry V2 HTTP API Specificiation](../api#digest-parameter).
+        [Registry V2 HTTP API Specification](api.md#digest-parameter).
 
 - **`layers`** *array*
 
@@ -213,7 +213,7 @@ image. It's the direct replacement for the schema-1 manifest.
     - **`digest`** *string*
 
         The digest of the content, as defined by the
-        [Registry V2 HTTP API Specificiation](../api#digest-parameter).
+        [Registry V2 HTTP API Specification](api.md#digest-parameter).
 
     - **`urls`** *array*
 
diff --git a/docs/content/storage-drivers/inmemory.md b/docs/content/storage-drivers/inmemory.md
index ba9cd93e..81c49dfc 100644
--- a/docs/content/storage-drivers/inmemory.md
+++ b/docs/content/storage-drivers/inmemory.md
@@ -7,7 +7,7 @@ title: In-memory storage driver (testing only)
 For purely tests purposes, you can use the `inmemory` storage driver. This
 driver is an implementation of the `storagedriver.StorageDriver` interface which
 uses local memory for object storage. If you would like to run a registry from
-volatile memory, use the [`filesystem` driver](../filesystem) on a ramdisk.
+volatile memory, use the [`filesystem` driver](filesystem.md) on a ramdisk.
 
 {{< hint type=important >}}
 This storage driver *does not* persist data across runs. This is why it is only suitable for testing. *Never* use this driver in production.
diff --git a/docs/hugo.yaml b/docs/hugo.yaml
index 4d5c75c9..b98cdd15 100644
--- a/docs/hugo.yaml
+++ b/docs/hugo.yaml
@@ -1,4 +1,4 @@
-baseURL: /
+baseURL: https://distribution.github.io/distribution
 languageCode: en-us
 title: CNCF Distribution
 theme: hugo-geekdoc
@@ -22,3 +22,4 @@ disablePathToLower: true
 params:
   geekdocRepo: "https://github.com/distribution/distribution"
   geekdocEditPath: edit/main/docs
+  geekdocOverwriteHTMLBase: true
diff --git a/docs/layouts/_default/_markup/render-link.html b/docs/layouts/_default/_markup/render-link.html
new file mode 100644
index 00000000..01ef350c
--- /dev/null
+++ b/docs/layouts/_default/_markup/render-link.html
@@ -0,0 +1,5 @@
+{{- if (strings.HasPrefix .Destination "http") -}}
+  <a href="{{ safe.URL .Destination }}" target="_blank">{{ safe.HTML .Text }}</a>
+{{- else -}}
+  <a href="{{ ref .Page .Destination | safe.URL }}">{{ safe.HTML .Text }}</a>
+{{- end -}}