diff --git a/.gitignore b/.gitignore
index 7d3643bb2266a33fe80a9c660b10e5489ce3c712..81c6f2a1b9b1969cc40c9bb39ef213aa4609a16c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -16,6 +16,7 @@ __pycache__/
 
 # documentation outputs
 /docs/_build/
-/docs/api/
+/docs/api/*.rst
+!/docs/api/gwdatafind.rst
 !/docs/api/gwdatafind.utils.rst
 *.automodapi
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index da51b1af33935e62f59520ea1d539f149c8b7f95..110494cddc23d72389b2401b09838e4d29251fd3 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -50,6 +50,7 @@ include:
 
   - component: $CI_SERVER_FQDN/computing/gitlab/components/sphinx/build@1
     inputs:
+      apt_packages: graphviz
       requirements: ".[docs]"
 
 
diff --git a/README.md b/README.md
index acb327048aa1f554a15d9c5ab792473f3e89c24a..2d4ddfdab65026e21d22bede69155c97e13b1744 100644
--- a/README.md
+++ b/README.md
@@ -7,8 +7,7 @@ Gravitational-Wave Frame (GWF) files containing data from the current
 gravitational-wave detectors.
 
 [![PyPI version](https://badge.fury.io/py/gwdatafind.svg)](http://badge.fury.io/py/gwdatafind)
-[![Linux status](https://git.ligo.org/lscsoft/gwdatafind/badges/main/pipeline.svg)](https://git.ligo.org/lscsoft/gwdatafind/commits/main)
-[![Windows status](https://ci.appveyor.com/api/projects/status/js6gql8960qa9pkl?svg=true)](https://ci.appveyor.com/project/duncanmmacleod/gwdatafind)
+[![Conda version](https://img.shields.io/conda/vn/conda-forge/gwdatafind.svg)](https://anaconda.org/conda-forge/gwdatafind/)
 [![License](https://img.shields.io/pypi/l/gwdatafind.svg)](https://choosealicense.com/licenses/gpl-3.0/)
 [![Documentation status](https://readthedocs.org/projects/gwdatafind/badge/?version=latest)](https://gwdatafind.readthedocs.io/en/latest/?badge=latest)
 
diff --git a/docs/api.rst b/docs/api.rst
deleted file mode 100644
index 7df04ae3d88d0f15e71de3fec41d9479d1a57243..0000000000000000000000000000000000000000
--- a/docs/api.rst
+++ /dev/null
@@ -1,18 +0,0 @@
-===================
- API documentation
-===================
-
-.. automodapi:: gwdatafind
-   :no-inheritance-diagram:
-   :no-heading:
-   :no-main-docstr:
-   :headings: =-
-   :skip: Session
-
-
-Submodules
-----------
-
-.. toctree::
-
-   api/gwdatafind.utils
diff --git a/docs/api/gwdatafind.rst b/docs/api/gwdatafind.rst
new file mode 100644
index 0000000000000000000000000000000000000000..68157d822daa0aa69ff20244e1bc04b5c85db48c
--- /dev/null
+++ b/docs/api/gwdatafind.rst
@@ -0,0 +1,7 @@
+############
+`gwdatafind`
+############
+
+.. automodapi:: gwdatafind
+   :no-heading:
+   :headings: =-
diff --git a/docs/api/gwdatafind.utils.rst b/docs/api/gwdatafind.utils.rst
index 4ad0d5273207a0325c525aab445677446d6770a0..ae5bdd2e0a55bf99f51865ecf7a867ab49b40878 100644
--- a/docs/api/gwdatafind.utils.rst
+++ b/docs/api/gwdatafind.utils.rst
@@ -1 +1,6 @@
+##################
+`gwdatafind.utils`
+##################
+
 .. automodapi:: gwdatafind.utils
+    :no-heading:
diff --git a/docs/auth.rst b/docs/auth.rst
index b51a920f4bfb4c3a333c2286f0ccb062743c0adb..049495452b5094281f1bce93a15fc74b5ebe84d5 100644
--- a/docs/auth.rst
+++ b/docs/auth.rst
@@ -23,8 +23,6 @@ for the `GW Open Science Center (GWOSC) <https://www.gwosc.org/>`__:
     https://datafind.gwosc.org
 
 
-.. _scitokens:
-
 =========
 SciTokens
 =========
@@ -33,7 +31,7 @@ GWDataFind servers may be operated with support for
 `SciTokens <https://scitokens.org>`__, an implementation of
 JSON Web Tokens designed for distributed scientific computing.
 
-When using the :doc:`API <api>`, the following keyword arguments
+When using :doc:`api/gwdatafind`, the following keyword arguments
 can be used with all functions to control the use of SciTokens:
 
 ``token``
@@ -83,7 +81,7 @@ proxies as authorisation credentials.
 This requires the X.509 credential _subject_ to be known to the server
 ahead of time.
 
-When using the :doc:`API <api>`, the following keyword arguments
+When using :doc:`api/gwdatafind`, the following keyword arguments
 can be used to control the use of X.509 credentials:
 
 ``cert``
diff --git a/docs/commandline.rst b/docs/commandline.rst
new file mode 100644
index 0000000000000000000000000000000000000000..5231c5ecb5c18090dd238df47442816c413734df
--- /dev/null
+++ b/docs/commandline.rst
@@ -0,0 +1,13 @@
+================
+``gw_data_find``
+================
+
+GWDataFind queries can be made using the command-line interface
+accessible via module execution (``python -m gwdatafind``) or
+the ``gw_data_find`` entry point script:
+
+.. argparse::
+    :module: gwdatafind.__main__
+    :func: command_line
+    :prog: gw_data_find
+    :nodescription:
diff --git a/docs/conf.py b/docs/conf.py
index a0ebf607c39389ddcccd1a8bf4ef57389d334eaf..4396419cfa9f7e4c53bfd6b3f653798c68718401 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -1,20 +1,38 @@
-# gwdatafind documentation build configuration file
+# Copyright (C) 2025 Cardiff University
+#
+# This file is part of GWDataFind.
+#
+# GWDataFind is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# GWDataFind is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with GWDataFind.  If not, see <http://www.gnu.org/licenses/>.
+
+"""GWDataFind documentation configuration file."""
+
+from datetime import (
+    datetime,
+    timezone,
+)
 
-import inspect
-import re
-import sys
-from os import getenv
-from pathlib import Path
+import sphinx_github_style
 
 import gwdatafind
 
 # -- metadata
 
 project = "gwdatafind"
-copyright = "2018-2025, Cardiff University"
+copyright = f"{datetime.now(tz=timezone.utc).date().year}, Cardiff University"
 author = "Duncan Macleod"
 release = gwdatafind.__version__
-version = re.split(r"[\w-]", gwdatafind.__version__)[0]
+version = release.split("+", 1)[0]
 
 # -- config
 
@@ -26,6 +44,7 @@ default_role = "obj"
 # -- theme
 
 html_theme = "furo"
+html_title = f"{project} {version}"
 
 html_theme_options = {
     "footer_icons": [
@@ -52,6 +71,7 @@ extensions = [
     "sphinx.ext.intersphinx",
     "sphinx.ext.napoleon",
     "sphinx.ext.linkcode",
+    "sphinxarg.ext",
     "sphinx_automodapi.automodapi",
     "sphinx_copybutton",
     "sphinxarg.ext",
@@ -60,99 +80,33 @@ extensions = [
 # automodapi
 automodapi_inherited_members = False
 
+# copybutton
+copybutton_prompt_text = " |".join((  # noqa: FLY002
+    ">>>",
+    r"\.\.\.",
+    r"\$"
+    r"In \[\d*\]:",
+    r" {2,5}\.\.\.:",
+    " {5,8}: ",
+))
+copybutton_prompt_is_regexp = True
+copybutton_line_continuation_character = "\\"
+
 # intersphinx
-intersphinx_mapping = {
-    "igwn-auth-utils": (
-        "https://igwn-auth-utils.readthedocs.io/en/stable/",
-        None,
-    ),
-    "igwn-segments": (
-        "https://igwn-segments.readthedocs.io/en/stable/",
-        None,
-    ),
-    "python": (
-        "https://docs.python.org/",
-        None,
-    ),
-    "requests": (
-        "https://requests.readthedocs.io/en/stable/",
-        None,
-    ),
-    "scitokens": (
-        "https://scitokens.readthedocs.io/en/stable/",
-        None,
-    ),
-}
+intersphinx_mapping = {key: (value, None) for key, value in {
+    "igwn-auth-utils": "https://igwn-auth-utils.readthedocs.io/en/stable/",
+    "igwn-segments": "https://igwn-segments.readthedocs.io/en/stable/",
+    "python": "https://docs.python.org/",
+    "requests": "https://requests.readthedocs.io/en/stable/",
+    "scitokens": "https://scitokens.readthedocs.io/en/stable/",
+}.items()}
+
+# linkcode
+linkcode_url = sphinx_github_style.get_linkcode_url(
+    blob=sphinx_github_style.get_linkcode_revision("head"),
+    url="https://git.ligo.org/computing/gwdatafind/client",
+)
+linkcode_resolve = sphinx_github_style.get_linkcode_resolve(linkcode_url)
 
 # napoleon
 napoleon_use_rtype = False
-
-
-# -- linkcode
-
-def _project_git_ref(version, prefix="v"):
-    """Returns the git reference for the given full release version.
-    """
-    # handle builds in CI
-    if getenv("GITLAB_CI"):
-        return getenv("CI_COMMIT_REF")
-    if getenv("GITHUB_ACTIONS"):
-        return getenv("GITHUB_SHA")
-    # otherwise use the project metadata
-    _setuptools_scm_version_regex = re.compile(
-        r"\+g(\w+)(?:\Z|\.)",
-    )
-    if match := _setuptools_scm_version_regex.search(version):
-        return match.groups()[0]
-    return f"{prefix}{version}"
-
-
-PROJECT_GIT_REF = _project_git_ref(release, prefix="")
-PROJECT_PATH = Path(gwdatafind.__file__).parent
-PROJECT_URL = getenv(
-    "CI_PROJECT_URL",
-    "https://git.ligo.org/computing/gwdatafind/client",
-)
-PROJECT_BLOB_URL = f"{PROJECT_URL}/blob/{PROJECT_GIT_REF}/{PROJECT_PATH.name}"
-
-
-def linkcode_resolve(domain, info):
-    """Determine the URL corresponding to Python object.
-    """
-    if domain != "py" or not info["module"]:
-        return None
-
-    def find_source(module, fullname):
-        """Construct a source file reference for an object reference.
-        """
-        # resolve object
-        obj = sys.modules[module]
-        for part in fullname.split("."):
-            obj = getattr(obj, part)
-        # get filename relative to project
-        filename = Path(
-            inspect.getsourcefile(obj),  # type: ignore [arg-type]
-        ).relative_to(PROJECT_PATH).as_posix()
-        # get line numbers of this object
-        lines, lineno = inspect.findsource(obj)
-        if lineno:
-            start = lineno + 1  # 0-index
-            end = lineno + len(inspect.getblock(lines[lineno:]))
-        else:
-            start = end = 0
-        return filename, start, end
-
-    try:
-        path, start, end = find_source(info["module"], info["fullname"])
-    except (
-        AttributeError,  # object not found
-        OSError,  # file not found
-        TypeError,  # source for object not found
-        ValueError,  # file not from this project
-    ):
-        return None
-
-    url = f"{PROJECT_BLOB_URL}/{path}"
-    if start:
-        url += f"#L{start}-L{end}"
-    return url
diff --git a/docs/index.rst b/docs/index.rst
index 1981e3e7161dd4ed7ba6a8cfab2084bf2612f15e..b9d62eadd87c1f9cec0541b408d8eee0d0adef43 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -1,14 +1,32 @@
-
 ##########
 GWDataFind
 ##########
 
-.. toctree::
-    :hidden:
+The client library for the GWDataFind service.
 
-    GWDatafind <self>
+.. image:: https://badge.fury.io/py/gwdatafind.svg
+    :target: https://badge.fury.io/py/gwdatafind
+    :alt: gwdatafind PyPI release badge
 
-The client library for the GWDataFind service.
+.. image:: https://img.shields.io/conda/vn/conda-forge/gwdatafind.svg
+    :target: https://anaconda.org/conda-forge/gwdatafind/
+    :alt: gwdatafind conda-forge release badge
+
+.. image:: https://img.shields.io/pypi/l/gwdatafind.svg
+    :target: https://choosealicense.com/licenses/gpl-3.0/
+    :alt: gwdatafind license
+
+.. raw:: html
+
+    <br>
+
+.. image:: https://img.shields.io/gitlab/issues/open/computing%2Fgwdatafind%2Fclient?gitlab_url=https%3A%2F%2Fgit.ligo.org
+    :target: https://git.ligo.org/computing/gwdatafind/client/issues/
+    :alt: GitLab Issues
+
+.. image:: https://img.shields.io/gitlab/merge-requests/open/computing%2Fgwdatafind%2Fclient?gitlab_url=https%3A%2F%2Fgit.ligo.org
+    :target: https://git.ligo.org/computing/gwdatafind/client/merge_requests/
+    :alt: GitLab Merge Requests
 
 .. toctree::
     :caption: Documentation
@@ -21,6 +39,13 @@ The client library for the GWDataFind service.
 
 .. toctree::
     :caption: Reference
-    :maxdepth: 2
+    :maxdepth: 1
+
+    api/gwdatafind
+    api/gwdatafind.utils
+
+.. toctree::
+    :caption: Command-line interface
+    :maxdepth: 1
 
-    api
+    commandline
diff --git a/docs/install.rst b/docs/install.rst
index 082d7b702684d6b675a424c240f1a5f1c5d5a148..4e4e27efcb7bed856b56fbf0a4e7de6b824cb3fa 100644
--- a/docs/install.rst
+++ b/docs/install.rst
@@ -4,8 +4,6 @@
 Installing GWDataFind
 #####################
 
-.. _conda:
-
 =====
 Conda
 =====
@@ -20,8 +18,6 @@ The recommended method of installing GWDataFind is with
 
     conda install -c conda-forge gwdatafind
 
-.. _debian:
-
 ======
 Debian
 ======
@@ -45,8 +41,6 @@ To install the Python 3 library only (and not any command-line entry points):
 
     apt-get install python3-gwdatafind
 
-.. _pip:
-
 ===
 Pip
 ===
@@ -70,10 +64,9 @@ RedHat / CentOS / Scientific / Rocky Linux
     dnf install gwdatafind
 
 See the IGWN Computing Guide software repositories entries for
-`Scientific Linux 7
-<https://computing.docs.ligo.org/guide/software/sl7/>`__
-or
 `Rocky Linux 8 <https://computing.docs.ligo.org/guide/software/rl8/>`__
+or
+`Rocky Linux 9 <https://computing.docs.ligo.org/guide/software/rl9/>`__
 for instructions on how to configure the required IGWN Yum repositories.
 
 To install the Python 3 library only (and not any command-line entry points):
diff --git a/docs/intro.rst b/docs/intro.rst
index 1184c932836713a86f9796f5089ee3860a262dc0..626442604bfd7aec45cf158ffa6090e1917fa4de 100644
--- a/docs/intro.rst
+++ b/docs/intro.rst
@@ -63,10 +63,4 @@ Command-line interface
 
 GWDataFind also provides a command-line interface accessible via
 module execution (``python -m gwdatafind``) or the ``gw_data_find``
-entry point script:
-
-.. argparse::
-    :module: gwdatafind.__main__
-    :func: command_line
-    :prog: gw_data_find
-    :nodescription:
+entry point script. See :doc:`commandline`.
diff --git a/gwdatafind/ui.py b/gwdatafind/ui.py
index a95164e5d6571862f1654c04f091d1b676ba18d1..c4bc38eece69011c078e0ecd81314a65da050c46 100644
--- a/gwdatafind/ui.py
+++ b/gwdatafind/ui.py
@@ -212,7 +212,7 @@ def find_observatories(
 
     Raises
     ------
-    requests.RequestsException
+    requests.RequestException
         if the request fails for any reason
 
     Examples
@@ -280,7 +280,7 @@ def find_types(
 
     Raises
     ------
-    requests.RequestsException
+    requests.RequestException
         if the request fails for any reason
 
     Examples
@@ -372,7 +372,7 @@ def find_times(
 
     Raises
     ------
-    requests.RequestsException
+    requests.RequestException
         if the request fails for any reason
     """  # noqa: E501
     qurl = _url(
@@ -459,7 +459,7 @@ def find_url(
 
     Raises
     ------
-    requests.RequestsException
+    requests.RequestException
         if the request fails for any reason
 
     RuntimeError
@@ -536,7 +536,7 @@ def find_latest(
 
     Raises
     ------
-    requests.RequestsException
+    requests.RequestException
         if the request fails for any reason
 
     RuntimeError
@@ -627,7 +627,7 @@ def find_urls(
 
     Raises
     ------
-    requests.RequestsException
+    requests.RequestException
         if the request fails for any reason
 
     RuntimeError
diff --git a/pyproject.toml b/pyproject.toml
index e10dd7395798f421d403f06d825a5ce1695bee16..a04fa11d3604fbe889ec155496d54133c18472a6 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -45,11 +45,11 @@ dynamic = [
 [project.optional-dependencies]
 docs = [
   "furo",
-  "numpydoc",
-  "Sphinx >= 4.4.0",
+  "Sphinx >=4.4.0",
   "sphinx-argparse",
-  "sphinx_automodapi",
+  "sphinx-automodapi",
   "sphinx-copybutton",
+  "sphinx-github-style",
 ]
 test = [
   "pytest >= 3.9.3",