diff --git a/.gitignore b/.gitignore
index 39315f4..9d22b7a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -18,3 +18,6 @@
# Downloaded dependency archives
/libs/*
!/libs/README.md
+
+# Docs
+doc/build
diff --git a/.readthedocs.yaml b/.readthedocs.yaml
index 93665c8..269d20d 100644
--- a/.readthedocs.yaml
+++ b/.readthedocs.yaml
@@ -1,9 +1,9 @@
version: 2
build:
- os: ubuntu-20.04
+ os: ubuntu-24.04
tools:
- python: '3.9'
+ python: '3.14'
sphinx:
configuration: doc/source/conf.py
diff --git a/doc/README.md b/doc/README.md
new file mode 100644
index 0000000..446a057
--- /dev/null
+++ b/doc/README.md
@@ -0,0 +1,154 @@
+# Documentation and Read the Docs
+
+This project publishes its documentation on Read the Docs (RTD):
+
+- Public site:
+- RTD config: [../.readthedocs.yaml](../.readthedocs.yaml)
+- Sphinx config: [source/conf.py](source/conf.py)
+- Docs source: [source/](source/)
+
+## How Read the Docs works in this repo
+
+Read the Docs does not usually require you to upload files manually.
+Instead, it watches the GitHub repository and builds the docs from the latest pushed commit.
+
+For this repository, RTD is configured to:
+
+- use config version `2`
+- build on `ubuntu-24.04`
+- use Python `3.14`
+- build with Sphinx using `doc/source/conf.py`
+- install the project itself with `pip install .`
+- install extra docs dependencies from `doc/source/requirements.txt`
+
+That means every successful RTD build uses the repository contents plus the settings in `.readthedocs.yaml`.
+
+## Before you publish
+
+If you changed the docs, or changed code that affects autodoc output, validate locally first.
+
+### 1. Create and activate a virtual environment
+
+From the repository root:
+
+```bash
+python3 -m venv .venv-docs
+source .venv-docs/bin/activate
+python -m pip install --upgrade pip
+```
+
+Use Python `3.12+` for local docs builds. The docs dependencies are pinned to current releases, including Sphinx `9.1.0`, which no longer supports older Python versions.
+
+### 2. Install the package and doc dependencies
+
+For a minimal local docs build, install the docs dependencies:
+
+```bash
+python -m pip install -r doc/source/requirements.txt
+```
+
+If you want autodoc to import `xmlsec` and render the API pages without import warnings, also install the package itself:
+
+```bash
+python -m pip install .
+```
+
+Note: `xmlsec` depends on native libraries. If `pip install .` fails, install the system dependencies first.
+
+Examples:
+
+- Debian/Ubuntu:
+
+```bash
+sudo apt-get install pkg-config libxml2-dev libxmlsec1-dev libxmlsec1-openssl
+```
+
+- macOS with Homebrew:
+
+```bash
+brew install libxml2 libxmlsec1 pkg-config
+```
+
+### 3. Build the docs locally
+
+```bash
+make -C doc html
+```
+
+Built HTML will be placed in:
+
+```text
+doc/build/html/
+```
+
+Open `doc/build/html/index.html` in a browser and check the pages you changed.
+
+## How to publish the latest docs
+
+### Normal flow
+
+If the RTD project is already connected to GitHub, this is the normal deployment path:
+
+1. Edit the docs or code.
+2. Commit the changes.
+3. Push the branch to GitHub.
+4. RTD receives the webhook event.
+5. RTD rebuilds the matching version and publishes it.
+
+For this repository, the current Git branch is `master`, and `latest` on RTD commonly tracks the repository default branch.
+
+Example:
+
+```bash
+git add doc/source .readthedocs.yaml README.md
+git commit -m "Update documentation"
+git push origin master
+```
+
+If you changed files outside those paths, add the correct files instead of using the sample `git add` command above.
+
+### Manual rebuild from the RTD dashboard
+
+If you already pushed your changes but the site did not update:
+
+1. Open the Read the Docs project for `xmlsec`.
+2. Go to the build/version page.
+3. Trigger a build for the version you want, usually `latest`.
+4. Wait for the build to finish and review the logs if it fails.
+
+Typical reasons a manual rebuild is needed:
+
+- GitHub integration/webhook is missing or broken
+- the target branch/version is inactive on RTD
+- a previous build failed and you want to rebuild after fixing the branch
+
+## First-time setup in Read the Docs
+
+If RTD has not been connected yet, an admin needs to set it up once:
+
+1. Sign in to Read the Docs with a GitHub account that has access to `xmlsec/python-xmlsec`.
+2. Import the repository.
+3. Confirm RTD is using the repository root `.readthedocs.yaml`.
+4. Make sure the GitHub integration/webhook is enabled.
+5. Make sure the `latest` version is active.
+
+After that, pushes to GitHub should trigger builds automatically.
+
+## Troubleshooting
+
+### Build fails locally on `pip install .`
+
+The Python package requires native `xmlsec` and `libxml2` dependencies. Install the OS packages first, then retry.
+
+### RTD build fails after a push
+
+Check:
+
+- the build logs in RTD
+- whether `.readthedocs.yaml` is valid
+- whether `doc/source/conf.py` still builds cleanly
+- whether the target branch/version is active
+
+### RTD is building the wrong version
+
+Read the Docs manages versions from Git branches and tags. Verify which branch `latest` points to, and whether `stable` is mapped to a branch or tag you expect.
diff --git a/doc/source/conf.py b/doc/source/conf.py
index 900b79d..aa59392 100644
--- a/doc/source/conf.py
+++ b/doc/source/conf.py
@@ -20,7 +20,7 @@
master_doc = 'index'
project = 'python-xmlsec'
-copyright = '2020, Oleg Hoefling '
+copyright = '2026, Amin Solhizadeh '
author = 'Bulat Gaifullin '
release = importlib.metadata.version('xmlsec')
parsed: Version = parse(release)
diff --git a/doc/source/requirements.txt b/doc/source/requirements.txt
index 9c05a5c..662649f 100644
--- a/doc/source/requirements.txt
+++ b/doc/source/requirements.txt
@@ -1,5 +1,4 @@
lxml==6.1.0
-importlib_metadata;python_version < '3.8'
-packaging
-Sphinx>=3
-furo>=2021.4.11b34
+packaging==26.1
+Sphinx==9.1.0
+furo==2025.12.19
diff --git a/src/template.c b/src/template.c
index 4609783..c6864c2 100644
--- a/src/template.c
+++ b/src/template.c
@@ -605,7 +605,7 @@ static PyObject* PyXmlSec_TemplateAddEncryptedKey(PyObject* self, PyObject *args
static char PyXmlSec_TemplateCreateEncryptedData__doc__[] = \
"encrypted_data_create(node, method, id = None, type = None, mime_type = None, encoding = None, ns = None) -> lxml.etree._Element\n"
- "Creates new :xml:`<{ns}:EncryptedData />` node for encryption template.\n\n"
+ "Creates new ``<{ns}:EncryptedData />`` node for encryption template.\n\n"
":param node: the pointer to signature node\n"
":type node: :class:`lxml.etree._Element`\n"
":param method: the encryption method\n"
@@ -662,7 +662,7 @@ static PyObject* PyXmlSec_TemplateCreateEncryptedData(PyObject* self, PyObject *
static char PyXmlSec_TemplateEncryptedDataEnsureKeyInfo__doc__[] = \
"encrypted_data_ensure_key_info(node, id = None, ns = None) -> lxml.etree._Element\n"
- "Adds :xml:`<{ns}:KeyInfo/>` to the :xml:`` node of ``node``.\n\n"
+ "Adds ``<{ns}:KeyInfo/>`` to the :xml:`` node of ``node``.\n\n"
":param node: the pointer to :xml:`` node\n"
":type node: :class:`lxml.etree._Element`\n"
":param id: the ``\"Id\"`` attribute (optional)\n"