diff --git a/PKG-INFO b/PKG-INFO
index fdec0535197b444fdd81adc7545efc0e228c7d79..f842a1e09d25d1698abd70dec84f5affa2a9fcfe 100644
--- a/PKG-INFO
+++ b/PKG-INFO
@@ -1,6 +1,6 @@
 Metadata-Version: 2.1
 Name: swh.model
-Version: 2.4.0
+Version: 2.4.1
 Summary: Software Heritage data model
 Home-page: https://forge.softwareheritage.org/diffusion/DMOD/
 Author: Software Heritage developers
diff --git a/docs/cli.rst b/docs/cli.rst
index 654111f660fdde7ad129b2658cbeed8633dfe796..0826a01d164a5fbe3df0f210f55da32b488e7b32 100644
--- a/docs/cli.rst
+++ b/docs/cli.rst
@@ -2,5 +2,5 @@ Command-line interface
 ======================
 
 .. click:: swh.model.cli:identify
-  :prog: swh identify
-  :show-nested:
+   :prog: swh identify
+   :show-nested:
diff --git a/docs/persistent-identifiers.rst b/docs/persistent-identifiers.rst
index 2f20cf18b51dc5e05ab4f18e9c67d40f38330e18..2cb27e5b6fb21be17810c141e329afa19caeda50 100644
--- a/docs/persistent-identifiers.rst
+++ b/docs/persistent-identifiers.rst
@@ -257,34 +257,30 @@ Identifiers with qualifiers
 ---------------------------
 
 * The following `SWHID
-  <https://archive.softwareheritage.org/swh:1:cnt:4d99d2d18326621ccdd70f5ea66c2e2ac236ad8b;origin=https://gitorious.org/ocamlp3l/ocamlp3l_cvs.git;visit=swh:1:snp:d7f1b9eb7ccb596c2622c4780febaa02549830f9;anchor=swh:1:rev:2db189928c94d62a3b4757b3eec68f0a4d4113f0;path=/Examples/SimpleFarm/simplefarm.ml;lines=9-15>`_
+  <https://archive.softwareheritage.org/swh:1:cnt:4d99d2d18326621ccdd70f5ea66c2e2ac236ad8b;origin=https://gitorious.org/ocamlp3l/ocamlp3l_cvs.git;visit=swh:1:snp:d7f1b9eb7ccb596c2622c4780febaa02549830f9;anchor=swh:1:rev:2db189928c94d62a3b4757b3eec68f0a4d4113f0;path=/Examples/SimpleFarm/simplefarm.ml;lines=9-15>`__
   denotes the lines 9 to 15 of a file content that can be found at absolute
   path ``/Examples/SimpleFarm/simplefarm.ml`` from the root directory of the
   revision ``swh:1:rev:2db189928c94d62a3b4757b3eec68f0a4d4113f0`` that is
   contained in the snapshot
   ``swh:1:snp:d7f1b9eb7ccb596c2622c4780febaa02549830f9`` taken from the origin
-  ``https://gitorious.org/ocamlp3l/ocamlp3l_cvs.git``:
+  ``https://gitorious.org/ocamlp3l/ocamlp3l_cvs.git``::
 
-.. code-block:: url
-
-  swh:1:cnt:4d99d2d18326621ccdd70f5ea66c2e2ac236ad8b;
-    origin=https://gitorious.org/ocamlp3l/ocamlp3l_cvs.git;
-    visit=swh:1:snp:d7f1b9eb7ccb596c2622c4780febaa02549830f9;
-    anchor=swh:1:rev:2db189928c94d62a3b4757b3eec68f0a4d4113f0;
-    path=/Examples/SimpleFarm/simplefarm.ml;
-    lines=9-15
+    swh:1:cnt:4d99d2d18326621ccdd70f5ea66c2e2ac236ad8b;
+      origin=https://gitorious.org/ocamlp3l/ocamlp3l_cvs.git;
+      visit=swh:1:snp:d7f1b9eb7ccb596c2622c4780febaa02549830f9;
+      anchor=swh:1:rev:2db189928c94d62a3b4757b3eec68f0a4d4113f0;
+      path=/Examples/SimpleFarm/simplefarm.ml;
+      lines=9-15
 
 * Here is an example of a `SWHID
-  <https://archive.softwareheritage.org/swh:1:cnt:f10371aa7b8ccabca8479196d6cd640676fd4a04;origin=https://github.com/web-platform-tests/wpt;visit=swh:1:snp:b37d435721bbd450624165f334724e3585346499;anchor=swh:1:rev:259d0612af038d14f2cd889a14a3adb6c9e96d96;path=/html/semantics/document-metadata/the-meta-element/pragma-directives/attr-meta-http-equiv-refresh/support/x%3Burl=foo/>`_
-  with a file path that requires percent-escaping:
-
-.. code-block:: url
-
-  swh:1:cnt:f10371aa7b8ccabca8479196d6cd640676fd4a04;
-    origin=https://github.com/web-platform-tests/wpt;
-    visit=swh:1:snp:b37d435721bbd450624165f334724e3585346499;
-    anchor=swh:1:rev:259d0612af038d14f2cd889a14a3adb6c9e96d96;
-    path=/html/semantics/document-metadata/the-meta-element/pragma-directives/attr-meta-http-equiv-refresh/support/x%3Burl=foo/
+  <https://archive.softwareheritage.org/swh:1:cnt:f10371aa7b8ccabca8479196d6cd640676fd4a04;origin=https://github.com/web-platform-tests/wpt;visit=swh:1:snp:b37d435721bbd450624165f334724e3585346499;anchor=swh:1:rev:259d0612af038d14f2cd889a14a3adb6c9e96d96;path=/html/semantics/document-metadata/the-meta-element/pragma-directives/attr-meta-http-equiv-refresh/support/x%3Burl=foo/>`__
+  with a file path that requires percent-escaping::
+
+    swh:1:cnt:f10371aa7b8ccabca8479196d6cd640676fd4a04;
+      origin=https://github.com/web-platform-tests/wpt;
+      visit=swh:1:snp:b37d435721bbd450624165f334724e3585346499;
+      anchor=swh:1:rev:259d0612af038d14f2cd889a14a3adb6c9e96d96;
+      path=/html/semantics/document-metadata/the-meta-element/pragma-directives/attr-meta-http-equiv-refresh/support/x%3Burl=foo/
 
 
 Implementation
diff --git a/swh.model.egg-info/PKG-INFO b/swh.model.egg-info/PKG-INFO
index fdec0535197b444fdd81adc7545efc0e228c7d79..f842a1e09d25d1698abd70dec84f5affa2a9fcfe 100644
--- a/swh.model.egg-info/PKG-INFO
+++ b/swh.model.egg-info/PKG-INFO
@@ -1,6 +1,6 @@
 Metadata-Version: 2.1
 Name: swh.model
-Version: 2.4.0
+Version: 2.4.1
 Summary: Software Heritage data model
 Home-page: https://forge.softwareheritage.org/diffusion/DMOD/
 Author: Software Heritage developers
diff --git a/swh/model/cli.py b/swh/model/cli.py
index effdf8a86334046840917a96a3debb954360f580..091f8abadb150b3ef168c271e087dafbf62f6334 100644
--- a/swh/model/cli.py
+++ b/swh/model/cli.py
@@ -218,7 +218,7 @@ def identify_object(obj_type, follow_symlinks, exclude_patterns, obj) -> str:
     metavar="PATTERN",
     multiple=True,
     help="Exclude directories using glob patterns \
-    (e.g., '*.git' to exclude all .git directories)",
+    (e.g., ``*.git`` to exclude all .git directories)",
 )
 @click.option(
     "--verify",
@@ -242,7 +242,7 @@ def identify(
     Tip: you can pass "-" to identify the content of standard input.
 
     \b
-    Examples:
+    Examples::
 
     \b
       $ swh identify fork.c kmod.c sched/deadline.c
diff --git a/swh/model/identifiers.py b/swh/model/identifiers.py
index 563da9e6944f40a0db8909d69b62ef30c93f9b40..daa92e757cfc9618a5d073cdeb8151001371fdec 100644
--- a/swh/model/identifiers.py
+++ b/swh/model/identifiers.py
@@ -47,7 +47,7 @@ class ObjectType(enum.Enum):
 class ExtendedObjectType(enum.Enum):
     """Possible object types of an ExtendedSWHID.
 
-    The variants are a superset of :cls:`ObjectType`'s"""
+    The variants are a superset of :class:`ObjectType`'s"""
 
     SNAPSHOT = "snp"
     REVISION = "rev"
@@ -729,24 +729,22 @@ def raw_extrinsic_metadata_identifier(metadata: Dict[str, Any]) -> str:
 
     A raw_extrinsic_metadata identifier is a salted sha1 (using the git
     hashing algorithm with the ``raw_extrinsic_metadata`` object type) of
-    a manifest following the format:
-
-    ```
-    target $ExtendedSwhid
-    discovery_date $Timestamp
-    authority $StrWithoutSpaces $IRI
-    fetcher $Str $Version
-    format $StrWithoutSpaces
-    origin $IRI                         <- optional
-    visit $IntInDecimal                 <- optional
-    snapshot $CoreSwhid                 <- optional
-    release $CoreSwhid                  <- optional
-    revision $CoreSwhid                 <- optional
-    path $Bytes                         <- optional
-    directory $CoreSwhid                <- optional
-
-    $MetadataBytes
-    ```
+    a manifest following the format::
+
+        target $ExtendedSwhid
+        discovery_date $Timestamp
+        authority $StrWithoutSpaces $IRI
+        fetcher $Str $Version
+        format $StrWithoutSpaces
+        origin $IRI                         <- optional
+        visit $IntInDecimal                 <- optional
+        snapshot $CoreSwhid                 <- optional
+        release $CoreSwhid                  <- optional
+        revision $CoreSwhid                 <- optional
+        path $Bytes                         <- optional
+        directory $CoreSwhid                <- optional
+
+        $MetadataBytes
 
     $IRI must be RFC 3987 IRIs (so they may contain newlines, that are escaped as
     described below)
@@ -769,7 +767,7 @@ def raw_extrinsic_metadata_identifier(metadata: Dict[str, Any]) -> str:
     ie. by adding a space after them.
 
     Returns:
-      str: the intrinsic identifier for `metadata`
+      str: the intrinsic identifier for ``metadata``
 
     """
     # equivalent to using math.floor(dt.timestamp()) to round down,
diff --git a/swh/model/merkle.py b/swh/model/merkle.py
index e84ef9d9823a3ffb3444a3c5586abae16f4b6772..098c8723086b74ef397e5055123ef99910cbdf21 100644
--- a/swh/model/merkle.py
+++ b/swh/model/merkle.py
@@ -7,7 +7,7 @@
 
 import abc
 from collections.abc import Mapping
-from typing import Iterator, List, Set
+from typing import Dict, Iterator, List, Set
 
 
 def deep_update(left, right):
@@ -101,16 +101,18 @@ class MerkleNode(dict, metaclass=abc.ABCMeta):
     The collection of updated data from the tree is implemented through the
     :func:`collect` function and associated helpers.
 
-    Attributes:
-      data (dict): data associated to the current node
-      parents (list): known parents of the current node
-      collected (bool): whether the current node has been collected
-
     """
 
     __slots__ = ["parents", "data", "__hash", "collected"]
 
-    """Type of the current node (used as a classifier for :func:`collect`)"""
+    data: Dict
+    """data associated to the current node"""
+
+    parents: List
+    """known parents of the current node"""
+
+    collected: bool
+    """whether the current node has been collected"""
 
     def __init__(self, data=None):
         super().__init__()
diff --git a/swh/model/tests/swh_model_data.py b/swh/model/tests/swh_model_data.py
index cd0ef4321ce58b1ac5a8b45476bd378ea0386f05..7166522ba57f5fe14077b8f58dc95feee0d33dfb 100644
--- a/swh/model/tests/swh_model_data.py
+++ b/swh/model/tests/swh_model_data.py
@@ -104,7 +104,7 @@ DATES = [
 
 REVISIONS = [
     Revision(
-        id=hash_to_bytes("4ca486e65eb68e4986aeef8227d2db1d56ce51b3"),
+        id=hash_to_bytes("66c7c1cd9673275037140f2abff7b7b11fc9439c"),
         message=b"hello",
         date=DATES[0],
         committer=COMMITTERS[0],
@@ -114,10 +114,13 @@ REVISIONS = [
         directory=b"\x01" * 20,
         synthetic=False,
         metadata=None,
-        parents=(),
+        parents=(
+            hash_to_bytes("9b918dd063cec85c2bc63cc7f167e29f5894dcbc"),
+            hash_to_bytes("757f38bdcd8473aaa12df55357f5e2f1a318e672"),
+        ),
     ),
     Revision(
-        id=hash_to_bytes("677063f5c405d6fc1781fc56379c9a9adf43d3a0"),
+        id=hash_to_bytes("c7f96242d73c267adc77c2908e64e0c1cb6a4431"),
         message=b"hello again",
         date=DATES[1],
         committer=COMMITTERS[1],
@@ -258,7 +261,7 @@ ORIGIN_VISIT_STATUSES = [
 DIRECTORIES = [
     Directory(id=hash_to_bytes("4b825dc642cb6eb9a060e54bf8d69288fbee4904"), entries=()),
     Directory(
-        id=hash_to_bytes("21416d920e0ebf0df4a7888bed432873ed5cb3a7"),
+        id=hash_to_bytes("87b339104f7dc2a8163dec988445e3987995545f"),
         entries=(
             DirectoryEntry(
                 name=b"file1.ext",
@@ -282,7 +285,7 @@ DIRECTORIES = [
 
 SNAPSHOTS = [
     Snapshot(
-        id=hash_to_bytes("17d0066a4a80aba4a0e913532ee8ff2014f006a9"),
+        id=hash_to_bytes("9e78d7105c5e0f886487511e2a92377b4ee4c32a"),
         branches={
             b"master": SnapshotBranch(
                 target_type=TargetType.REVISION, target=REVISIONS[0].id
@@ -290,7 +293,7 @@ SNAPSHOTS = [
         },
     ),
     Snapshot(
-        id=hash_to_bytes("8ce268b87faf03850693673c3eb5c9bb66e1ca38"),
+        id=hash_to_bytes("09efffaaad8d1f9c7f9402db0266dbe28082853f"),
         branches={
             b"target/revision": SnapshotBranch(
                 target_type=TargetType.REVISION, target=REVISIONS[0].id,
diff --git a/swh/model/tests/test_swh_model_data.py b/swh/model/tests/test_swh_model_data.py
index 7b50e60e54d348d50c88b607aeab6f005239a5f1..3b6cd5cb62521f65ac8949d8c510edfad70ce69c 100644
--- a/swh/model/tests/test_swh_model_data.py
+++ b/swh/model/tests/test_swh_model_data.py
@@ -18,6 +18,16 @@ def test_swh_model_data(object_type, objects):
         attr.validate(obj)
 
 
+@pytest.mark.parametrize(
+    "object_type", ("directory", "revision", "release", "snapshot"),
+)
+def test_swh_model_data_hash(object_type):
+    for obj in TEST_OBJECTS[object_type]:
+        assert (
+            obj.compute_hash() == obj.id
+        ), f"{obj.compute_hash().hex()} != {obj.id.hex()}"
+
+
 def test_ensure_visit_visit_status_date_consistency():
     """ensure origin-visit-status dates are more recent than their visit counterpart
 
diff --git a/tox.ini b/tox.ini
index 25baa11db70079f1d41f3123066b763f44ed0024..a8df51a4618fc6cdaa1ac2f5fc6caac3030a3f6f 100644
--- a/tox.ini
+++ b/tox.ini
@@ -42,3 +42,41 @@ deps =
   mypy
 commands =
   mypy swh
+
+# build documentation outside swh-environment using the current
+# git HEAD of swh-docs, is executed on CI for each diff to prevent
+# breaking doc build
+[testenv:sphinx]
+whitelist_externals = make
+usedevelop = true
+extras =
+  testing
+deps =
+  # fetch and install swh-docs in develop mode
+  -e git+https://forge.softwareheritage.org/source/swh-docs#egg=swh.docs
+
+setenv =
+  SWH_PACKAGE_DOC_TOX_BUILD = 1
+  # turn warnings into errors
+  SPHINXOPTS = -W
+commands =
+  make -I ../.tox/sphinx/src/swh-docs/swh/ -C docs
+
+
+# build documentation only inside swh-environment using local state
+# of swh-docs package
+[testenv:sphinx-dev]
+whitelist_externals = make
+usedevelop = true
+extras =
+  testing
+deps =
+  # install swh-docs in develop mode
+  -e ../swh-docs
+
+setenv =
+  SWH_PACKAGE_DOC_TOX_BUILD = 1
+  # turn warnings into errors
+  SPHINXOPTS = -W
+commands =
+  make -I ../.tox/sphinx-dev/src/swh-docs/swh/ -C docs