diff --git a/swh/web/browse/browseurls.py b/swh/web/browse/browseurls.py
--- a/swh/web/browse/browseurls.py
+++ b/swh/web/browse/browseurls.py
@@ -1,8 +1,10 @@
-# Copyright (C) 2017-2019  The Software Heritage developers
+# Copyright (C) 2017-2022  The Software Heritage developers
 # See the AUTHORS file at the top-level directory of this distribution
 # License: GNU Affero General Public License version 3, or any later version
 # See top-level LICENSE file for more information
 
+from typing import List, Optional
+
 from swh.web.common.urlsindex import UrlsIndex
 
 
@@ -14,7 +16,11 @@
     scope = "browse"
 
 
-def browse_route(*url_patterns, view_name=None, checksum_args=None):
+def browse_route(
+    *url_patterns: str,
+    view_name: Optional[str] = None,
+    checksum_args: Optional[List[str]] = None,
+):
     """
     Decorator to ease the registration of a swh-web browse endpoint
 
@@ -24,7 +30,7 @@
         view_name: the name of the Django view associated to the routes used
             to reverse the url
     """
-    url_patterns = ["^" + url_pattern + "$" for url_pattern in url_patterns]
+    url_patterns = tuple("^" + url_pattern + "$" for url_pattern in url_patterns)
     view_name = view_name
 
     def decorator(f):
diff --git a/swh/web/browse/identifiers.py b/swh/web/browse/identifiers.py
--- a/swh/web/browse/identifiers.py
+++ b/swh/web/browse/identifiers.py
@@ -1,19 +1,20 @@
-# Copyright (C) 2017-2020  The Software Heritage developers
+# Copyright (C) 2017-2022  The Software Heritage developers
 # See the AUTHORS file at the top-level directory of this distribution
 # License: GNU Affero General Public License version 3, or any later version
 # See top-level LICENSE file for more information
 
+from django.http import HttpRequest, HttpResponse
 from django.shortcuts import redirect
 
 from swh.web.common.identifiers import resolve_swhid
 
 
-def swhid_browse(request, swhid):
+def swhid_browse(request: HttpRequest, swhid: str) -> HttpResponse:
     """
     Django view enabling to browse the archive using :ref:`persistent-identifiers`.
 
     The url that points to it is :http:get:`/(swhid)/`.
     """
     swhid_resolved = resolve_swhid(swhid, query_params=request.GET)
-
+    assert swhid_resolved["browse_url"]
     return redirect(swhid_resolved["browse_url"])
diff --git a/swh/web/browse/snapshot_context.py b/swh/web/browse/snapshot_context.py
--- a/swh/web/browse/snapshot_context.py
+++ b/swh/web/browse/snapshot_context.py
@@ -8,6 +8,7 @@
 from collections import defaultdict
 from typing import Any, Dict, List, Optional, Tuple
 
+from django.http import HttpRequest, HttpResponse
 from django.shortcuts import render
 from django.utils.html import escape
 
@@ -48,7 +49,9 @@
 _empty_snapshot_id = Snapshot(branches={}).id.hex()
 
 
-def _get_branch(branches, branch_name, snapshot_id):
+def _get_branch(
+    branches: List[SnapshotBranchInfo], branch_name: str, snapshot_id: str
+) -> Optional[SnapshotBranchInfo]:
     """
     Utility function to get a specific branch from a snapshot.
     Returns None if the branch cannot be found.
@@ -71,9 +74,12 @@
         if snp_branch and snp_branch[0]["name"] == branch_name:
             branches.append(snp_branch[0])
             return snp_branch[0]
+    return None
 
 
-def _get_release(releases, release_name, snapshot_id):
+def _get_release(
+    releases: List[SnapshotReleaseInfo], release_name: Optional[str], snapshot_id: str
+) -> Optional[SnapshotReleaseInfo]:
     """
     Utility function to get a specific release from a snapshot.
     Returns None if the release cannot be found.
@@ -81,7 +87,7 @@
     filtered_releases = [r for r in releases if r["name"] == release_name]
     if filtered_releases:
         return filtered_releases[0]
-    else:
+    elif release_name:
         # case where a large branches list has been truncated
         try:
             # git origins have specific branches for releases
@@ -102,11 +108,18 @@
         if snp_release and snp_release[0]["name"] == release_name:
             releases.append(snp_release[0])
             return snp_release[0]
+    return None
 
 
 def _branch_not_found(
-    branch_type, branch, snapshot_id, snapshot_sizes, origin_info, timestamp, visit_id
-):
+    branch_type: str,
+    branch: str,
+    snapshot_id: str,
+    snapshot_sizes: Dict[str, int],
+    origin_info: Optional[OriginInfo],
+    timestamp: Optional[str],
+    visit_id: Optional[int],
+) -> None:
     """
     Utility function to raise an exception when a specified branch/release
     can not be found.
@@ -131,25 +144,25 @@
             branch,
             snapshot_id,
         )
-    elif visit_id and snapshot_sizes[target_type] == 0:
+    elif visit_id and snapshot_sizes[target_type] == 0 and origin_info:
         msg = (
             "Origin with url %s"
             " for visit with id %s has an empty list"
             " of %s!" % (origin_info["url"], visit_id, branch_type_plural)
         )
-    elif visit_id:
+    elif visit_id and origin_info:
         msg = (
             "%s %s associated to visit with"
             " id %s for origin with url %s"
             " not found!" % (branch_type, branch, visit_id, origin_info["url"])
         )
-    elif snapshot_sizes[target_type] == 0:
+    elif snapshot_sizes[target_type] == 0 and origin_info and timestamp:
         msg = (
             "Origin with url %s"
             " for visit with timestamp %s has an empty list"
             " of %s!" % (origin_info["url"], timestamp, branch_type_plural)
         )
-    else:
+    elif origin_info and timestamp:
         msg = (
             "%s %s associated to visit with"
             " timestamp %s for origin with "
@@ -569,13 +582,24 @@
                 # HEAD alias targets a release
                 release_name = archive.lookup_release(head["target"])["name"]
                 head_rel = _get_release(releases, release_name, snapshot_id)
-                if head_rel["target_type"] == "revision":
+                if head_rel is None:
+                    _branch_not_found(
+                        "release",
+                        str(release_name),
+                        snapshot_id,
+                        snapshot_sizes,
+                        origin_info,
+                        timestamp,
+                        visit_id,
+                    )
+                elif head_rel["target_type"] == "revision":
                     revision = archive.lookup_revision(head_rel["target"])
                     root_directory = revision["directory"]
                     revision_id = head_rel["target"]
                 elif head_rel["target_type"] == "directory":
                     root_directory = head_rel["target"]
-                release_id = head_rel["id"]
+                if head_rel is not None:
+                    release_id = head_rel["id"]
         elif branches:
             # fallback to browse first branch otherwise
             branch = branches[0]
@@ -658,12 +682,16 @@
     )
 
     if revision_info:
-        revision_info["revision_url"] = gen_revision_url(revision_id, snapshot_context)
+        revision_info["revision_url"] = gen_revision_url(
+            revision_info["id"], snapshot_context
+        )
 
     return snapshot_context
 
 
-def _build_breadcrumbs(snapshot_context: SnapshotContext, path: str):
+def _build_breadcrumbs(
+    snapshot_context: SnapshotContext, path: Optional[str]
+) -> List[Dict[str, str]]:
     origin_info = snapshot_context["origin_info"]
     url_args = snapshot_context["url_args"]
     query_params = dict(snapshot_context["query_params"])
@@ -700,14 +728,18 @@
     return breadcrumbs
 
 
-def _check_origin_url(snapshot_id, origin_url):
+def _check_origin_url(snapshot_id: Optional[str], origin_url: Optional[str]) -> None:
     if snapshot_id is None and origin_url is None:
         raise BadInputExc("An origin URL must be provided as query parameter.")
 
 
 def browse_snapshot_directory(
-    request, snapshot_id=None, origin_url=None, timestamp=None, path=None
-):
+    request: HttpRequest,
+    snapshot_id: Optional[str] = None,
+    origin_url: Optional[str] = None,
+    timestamp: Optional[str] = None,
+    path: Optional[str] = None,
+) -> HttpResponse:
     """
     Django view implementation for browsing a directory in a snapshot context.
     """
@@ -728,7 +760,7 @@
 
     root_directory = snapshot_context["root_directory"]
     sha1_git = root_directory
-    error_info = {
+    error_info: Dict[str, Any] = {
         "status_code": 200,
         "description": None,
     }
@@ -813,7 +845,7 @@
         dir_path = "/" + path
 
     swh_objects = []
-    vault_cooking = {
+    vault_cooking: Dict[str, Any] = {
         "directory_context": False,
         "directory_swhid": None,
         "revision_context": False,
@@ -932,7 +964,12 @@
 PER_PAGE = 100
 
 
-def browse_snapshot_log(request, snapshot_id=None, origin_url=None, timestamp=None):
+def browse_snapshot_log(
+    request: HttpRequest,
+    snapshot_id: Optional[str] = None,
+    origin_url: Optional[str] = None,
+    timestamp: Optional[str] = None,
+) -> HttpResponse:
     """
     Django view implementation for browsing a revision history in a
     snapshot context.
@@ -991,9 +1028,10 @@
     query_params = snapshot_context["query_params"]
     snapshot_id = snapshot_context["snapshot_id"]
 
-    query_params["per_page"] = per_page
+    query_params["per_page"] = str(per_page)
     revs_ordering = request.GET.get("revs_ordering", "")
-    query_params["revs_ordering"] = revs_ordering or None
+    if revs_ordering:
+        query_params["revs_ordering"] = revs_ordering
 
     if origin_info:
         browse_view_name = "browse-origin-log"
@@ -1002,14 +1040,14 @@
 
     prev_log_url = None
     if len(rev_log) > offset + per_page:
-        query_params["offset"] = offset + per_page
+        query_params["offset"] = str(offset + per_page)
         prev_log_url = reverse(
             browse_view_name, url_args=url_args, query_params=query_params
         )
 
     next_log_url = None
     if offset != 0:
-        query_params["offset"] = offset - per_page
+        query_params["offset"] = str(offset - per_page)
         next_log_url = reverse(
             browse_view_name, url_args=url_args, query_params=query_params
         )
@@ -1029,7 +1067,7 @@
         "snapshot": snapshot_id,
     }
 
-    if origin_info:
+    if origin_info and visit_info:
         revision_metadata["origin url"] = origin_info["url"]
         revision_metadata["origin visit date"] = format_utc_iso_date(visit_info["date"])
         revision_metadata["origin visit type"] = visit_info["type"]
@@ -1077,8 +1115,12 @@
 
 
 def browse_snapshot_branches(
-    request, snapshot_id=None, origin_url=None, timestamp=None, branch_name_include=None
-):
+    request: HttpRequest,
+    snapshot_id: Optional[str] = None,
+    origin_url: Optional[str] = None,
+    timestamp: Optional[str] = None,
+    branch_name_include: Optional[str] = None,
+) -> HttpResponse:
     """
     Django view implementation for browsing a list of branches in a snapshot
     context.
@@ -1093,8 +1135,8 @@
         visit_id=visit_id or None,
     )
 
-    branches_bc = request.GET.get("branches_breadcrumbs", "")
-    branches_bc = branches_bc.split(",") if branches_bc else []
+    branches_bc_str = request.GET.get("branches_breadcrumbs", "")
+    branches_bc = branches_bc_str.split(",") if branches_bc_str else []
     branches_from = branches_bc[-1] if branches_bc else ""
 
     origin_info = snapshot_context["origin_info"]
@@ -1113,9 +1155,10 @@
         target_types=["revision", "alias"],
         branch_name_include_substring=branch_name_include,
     )
-    displayed_branches = []
+    displayed_branches: List[Dict[str, Any]] = []
     if snapshot:
-        displayed_branches, _, _ = process_snapshot_branches(snapshot)
+        branches, _, _ = process_snapshot_branches(snapshot)
+        displayed_branches = [dict(branch) for branch in branches]
 
     for branch in displayed_branches:
         rev_query_params = {}
@@ -1190,11 +1233,11 @@
 
 
 def browse_snapshot_releases(
-    request,
-    snapshot_id=None,
-    origin_url=None,
-    timestamp=None,
-    release_name_include=None,
+    request: HttpRequest,
+    snapshot_id: Optional[str] = None,
+    origin_url: Optional[str] = None,
+    timestamp: Optional[str] = None,
+    release_name_include: Optional[str] = None,
 ):
     """
     Django view implementation for browsing a list of releases in a snapshot
@@ -1210,8 +1253,8 @@
         visit_id=visit_id or None,
     )
 
-    rel_bc = request.GET.get("releases_breadcrumbs", "")
-    rel_bc = rel_bc.split(",") if rel_bc else []
+    rel_bc_str = request.GET.get("releases_breadcrumbs", "")
+    rel_bc = rel_bc_str.split(",") if rel_bc_str else []
     rel_from = rel_bc[-1] if rel_bc else ""
 
     origin_info = snapshot_context["origin_info"]
@@ -1225,9 +1268,10 @@
         target_types=["release", "alias"],
         branch_name_include_substring=release_name_include,
     )
-    displayed_releases = []
+    displayed_releases: List[Dict[str, Any]] = []
     if snapshot:
-        _, displayed_releases, _ = process_snapshot_branches(snapshot)
+        _, releases, _ = process_snapshot_branches(snapshot)
+        displayed_releases = [dict(release) for release in releases]
 
     for release in displayed_releases:
         query_params_tgt = {"snapshot": snapshot_id, "release": release["name"]}
diff --git a/swh/web/browse/urls.py b/swh/web/browse/urls.py
--- a/swh/web/browse/urls.py
+++ b/swh/web/browse/urls.py
@@ -1,9 +1,10 @@
-# Copyright (C) 2017-2021  The Software Heritage developers
+# Copyright (C) 2017-2022  The Software Heritage developers
 # See the AUTHORS file at the top-level directory of this distribution
 # License: GNU Affero General Public License version 3, or any later version
 # See top-level LICENSE file for more information
 
 from django.conf.urls import url
+from django.http import HttpRequest, HttpResponse
 from django.shortcuts import redirect, render
 
 from swh.web.browse.browseurls import BrowseUrls
@@ -17,13 +18,13 @@
 from swh.web.common.utils import origin_visit_types, reverse
 
 
-def _browse_help_view(request):
+def _browse_help_view(request: HttpRequest) -> HttpResponse:
     return render(
         request, "browse/help.html", {"heading": "How to browse the archive ?"}
     )
 
 
-def _browse_search_view(request):
+def _browse_search_view(request: HttpRequest) -> HttpResponse:
     return render(
         request,
         "browse/search.html",
@@ -34,7 +35,7 @@
     )
 
 
-def _browse_vault_view(request):
+def _browse_vault_view(request: HttpRequest) -> HttpResponse:
     return render(
         request,
         "browse/vault-ui.html",
@@ -42,7 +43,7 @@
     )
 
 
-def _browse_origin_save_view(request):
+def _browse_origin_save_view(request: HttpRequest) -> HttpResponse:
     return redirect(reverse("origin-save"))
 
 
diff --git a/swh/web/browse/utils.py b/swh/web/browse/utils.py
--- a/swh/web/browse/utils.py
+++ b/swh/web/browse/utils.py
@@ -6,7 +6,7 @@
 import base64
 import stat
 import textwrap
-from typing import Tuple
+from typing import Any, Dict, Iterator, List, Optional, Tuple, Union
 
 import chardet
 import magic
@@ -16,6 +16,7 @@
 
 from swh.web.common import archive, highlightjs
 from swh.web.common.exc import NotFoundExc, sentry_capture_exception
+from swh.web.common.typing import SnapshotContext
 from swh.web.common.utils import (
     browsers_supported_image_mimes,
     django_cache,
@@ -27,7 +28,9 @@
 
 
 @django_cache()
-def get_directory_entries(sha1_git):
+def get_directory_entries(
+    sha1_git: str,
+) -> Tuple[List[Dict[str, Any]], List[Dict[str, Any]]]:
     """Function that retrieves the content of a directory
     from the archive.
 
@@ -44,7 +47,7 @@
     Raises:
         NotFoundExc if the directory is not found
     """
-    entries = list(archive.lookup_directory(sha1_git))
+    entries: List[Dict[str, Any]] = list(archive.lookup_directory(sha1_git))
     for e in entries:
         e["perms"] = stat.filemode(e["perms"])
         if e["type"] == "rev":
@@ -61,12 +64,12 @@
     return dirs, files
 
 
-def get_mimetype_and_encoding_for_content(content):
+def get_mimetype_and_encoding_for_content(content: bytes) -> Tuple[str, str]:
     """Function that returns the mime type and the encoding associated to
     a content buffer using the magic module under the hood.
 
     Args:
-        content (bytes): a content buffer
+        content: a content buffer
 
     Returns:
         A tuple (mimetype, encoding), for instance ('text/plain', 'us-ascii'),
@@ -131,10 +134,10 @@
 
 
 def request_content(
-    query_string,
-    max_size=content_display_max_size,
-    re_encode=True,
-):
+    query_string: str,
+    max_size: Optional[int] = content_display_max_size,
+    re_encode: bool = True,
+) -> Dict[str, Any]:
     """Function that retrieves a content from the archive.
 
     Raw bytes content is first retrieved, then the content mime type.
@@ -150,8 +153,7 @@
             no size limit if None)
 
     Returns:
-        A tuple whose first member corresponds to the content raw bytes
-        and second member the content mime type
+        A dict filled with content info.
 
     Raises:
         NotFoundExc if the content is not found
@@ -209,7 +211,9 @@
     return content_data
 
 
-def prepare_content_for_display(content_data, mime_type, path):
+def prepare_content_for_display(
+    content_data: bytes, mime_type: str, path: Optional[str]
+) -> Dict[str, Any]:
     """Function that prepares a content for HTML display.
 
     The function tries to associate a programming language to a
@@ -221,9 +225,9 @@
     for displaying the image.
 
     Args:
-        content_data (bytes): raw bytes of the content
-        mime_type (string): mime type of the content
-        path (string): path of the content including filename
+        content_data: raw bytes of the content
+        mime_type: mime type of the content
+        path: path of the content including filename
 
     Returns:
         A dict containing the content bytes (possibly different from the one
@@ -242,29 +246,39 @@
     if language is None:
         language = "plaintext"
 
+    processed_content: Union[bytes, str] = content_data
+
     if mime_type.startswith("image/"):
         if mime_type in browsers_supported_image_mimes:
-            content_data = base64.b64encode(content_data).decode("ascii")
+            processed_content = base64.b64encode(content_data).decode("ascii")
 
     if mime_type.startswith("image/svg"):
         mime_type = "image/svg+xml"
 
     if mime_type.startswith("text/") or mime_type.startswith("application/"):
-        content_data = content_data.decode("utf-8", errors="replace")
+        processed_content = content_data.decode("utf-8", errors="replace")
 
-    return {"content_data": content_data, "language": language, "mimetype": mime_type}
+    return {
+        "content_data": processed_content,
+        "language": language,
+        "mimetype": mime_type,
+    }
 
 
-def gen_link(url, link_text=None, link_attrs=None):
+def gen_link(
+    url: str,
+    link_text: Optional[str] = None,
+    link_attrs: Optional[Dict[str, str]] = None,
+) -> str:
     """
     Utility function for generating an HTML link to insert
     in Django templates.
 
     Args:
-        url (str): an url
-        link_text (str): optional text for the produced link,
+        url: an url
+        link_text: optional text for the produced link,
             if not provided the url will be used
-        link_attrs (dict): optional attributes (e.g. class)
+        link_attrs: optional attributes (e.g. class)
             to add to the link
 
     Returns:
@@ -281,8 +295,10 @@
     return mark_safe(link)
 
 
-def _snapshot_context_query_params(snapshot_context):
-    query_params = {}
+def _snapshot_context_query_params(
+    snapshot_context: Optional[SnapshotContext],
+) -> Dict[str, str]:
+    query_params: Dict[str, str] = {}
     if not snapshot_context:
         return query_params
     if snapshot_context and snapshot_context["origin_info"]:
@@ -290,11 +306,11 @@
         snp_query_params = snapshot_context["query_params"]
         query_params = {"origin_url": origin_info["url"]}
         if "timestamp" in snp_query_params:
-            query_params["timestamp"] = snp_query_params["timestamp"]
+            query_params["timestamp"] = str(snp_query_params["timestamp"])
         if "visit_id" in snp_query_params:
-            query_params["visit_id"] = snp_query_params["visit_id"]
+            query_params["visit_id"] = str(snp_query_params["visit_id"])
         if "snapshot" in snp_query_params and "visit_id" not in query_params:
-            query_params["snapshot"] = snp_query_params["snapshot"]
+            query_params["snapshot"] = str(snp_query_params["snapshot"])
     elif snapshot_context:
         query_params = {"snapshot": snapshot_context["snapshot_id"]}
 
@@ -310,13 +326,15 @@
     return query_params
 
 
-def gen_revision_url(revision_id, snapshot_context=None):
+def gen_revision_url(
+    revision_id: str, snapshot_context: Optional[SnapshotContext] = None
+) -> str:
     """
     Utility function for generating an url to a revision.
 
     Args:
-        revision_id (str): a revision id
-        snapshot_context (dict): if provided, generate snapshot-dependent
+        revision_id: a revision id
+        snapshot_context: if provided, generate snapshot-dependent
             browsing url
 
     Returns:
@@ -334,25 +352,28 @@
 
 
 def gen_revision_link(
-    revision_id,
-    shorten_id=False,
-    snapshot_context=None,
-    link_text="Browse",
-    link_attrs={"class": "btn btn-default btn-sm", "role": "button"},
-):
+    revision_id: str,
+    shorten_id: bool = False,
+    snapshot_context: Optional[SnapshotContext] = None,
+    link_text: Optional[str] = "Browse",
+    link_attrs: Optional[Dict[str, str]] = {
+        "class": "btn btn-default btn-sm",
+        "role": "button",
+    },
+) -> Optional[str]:
     """
     Utility function for generating a link to a revision HTML view
     to insert in Django templates.
 
     Args:
-        revision_id (str): a revision id
-        shorten_id (boolean): whether to shorten the revision id to 7
+        revision_id: a revision id
+        shorten_id: whether to shorten the revision id to 7
             characters for the link text
-        snapshot_context (dict): if provided, generate snapshot-dependent
+        snapshot_context: if provided, generate snapshot-dependent
             browsing link
-        link_text (str): optional text for the generated link
+        link_text: optional text for the generated link
             (the revision id will be used by default)
-        link_attrs (dict): optional attributes (e.g. class)
+        link_attrs: optional attributes (e.g. class)
             to add to the link
 
     Returns:
@@ -373,20 +394,23 @@
 
 
 def gen_directory_link(
-    sha1_git,
-    snapshot_context=None,
-    link_text="Browse",
-    link_attrs={"class": "btn btn-default btn-sm", "role": "button"},
-):
+    sha1_git: str,
+    snapshot_context: Optional[SnapshotContext] = None,
+    link_text: Optional[str] = "Browse",
+    link_attrs: Optional[Dict[str, str]] = {
+        "class": "btn btn-default btn-sm",
+        "role": "button",
+    },
+) -> Optional[str]:
     """
     Utility function for generating a link to a directory HTML view
     to insert in Django templates.
 
     Args:
-        sha1_git (str): directory identifier
-        link_text (str): optional text for the generated link
+        sha1_git: directory identifier
+        link_text: optional text for the generated link
             (the directory id will be used by default)
-        link_attrs (dict): optional attributes (e.g. class)
+        link_attrs: optional attributes (e.g. class)
             to add to the link
 
     Returns:
@@ -408,20 +432,23 @@
 
 
 def gen_snapshot_link(
-    snapshot_id,
-    snapshot_context=None,
-    link_text="Browse",
-    link_attrs={"class": "btn btn-default btn-sm", "role": "button"},
-):
+    snapshot_id: str,
+    snapshot_context: Optional[SnapshotContext] = None,
+    link_text: Optional[str] = "Browse",
+    link_attrs: Optional[Dict[str, str]] = {
+        "class": "btn btn-default btn-sm",
+        "role": "button",
+    },
+) -> str:
     """
     Utility function for generating a link to a snapshot HTML view
     to insert in Django templates.
 
     Args:
-        snapshot_id (str): snapshot identifier
-        link_text (str): optional text for the generated link
+        snapshot_id: snapshot identifier
+        link_text: optional text for the generated link
             (the snapshot id will be used by default)
-        link_attrs (dict): optional attributes (e.g. class)
+        link_attrs: optional attributes (e.g. class)
             to add to the link
 
     Returns:
@@ -442,20 +469,23 @@
 
 
 def gen_content_link(
-    sha1_git,
-    snapshot_context=None,
-    link_text="Browse",
-    link_attrs={"class": "btn btn-default btn-sm", "role": "button"},
-):
+    sha1_git: str,
+    snapshot_context: Optional[SnapshotContext] = None,
+    link_text: Optional[str] = "Browse",
+    link_attrs: Optional[Dict[str, str]] = {
+        "class": "btn btn-default btn-sm",
+        "role": "button",
+    },
+) -> Optional[str]:
     """
     Utility function for generating a link to a content HTML view
     to insert in Django templates.
 
     Args:
-        sha1_git (str): content identifier
-        link_text (str): optional text for the generated link
+        sha1_git: content identifier
+        link_text: optional text for the generated link
             (the content sha1_git will be used by default)
-        link_attrs (dict): optional attributes (e.g. class)
+        link_attrs: optional attributes (e.g. class)
             to add to the link
 
     Returns:
@@ -477,14 +507,16 @@
     return gen_link(content_url, link_text, link_attrs)
 
 
-def get_revision_log_url(revision_id, snapshot_context=None):
+def get_revision_log_url(
+    revision_id: str, snapshot_context: Optional[SnapshotContext] = None
+) -> str:
     """
     Utility function for getting the URL for a revision log HTML view
     (possibly in the context of an origin).
 
     Args:
-        revision_id (str): revision identifier the history heads to
-        snapshot_context (dict): if provided, generate snapshot-dependent
+        revision_id: revision identifier the history heads to
+        snapshot_context: if provided, generate snapshot-dependent
             browsing link
     Returns:
         The revision log view URL
@@ -510,22 +542,25 @@
 
 
 def gen_revision_log_link(
-    revision_id,
-    snapshot_context=None,
-    link_text="Browse",
-    link_attrs={"class": "btn btn-default btn-sm", "role": "button"},
-):
+    revision_id: str,
+    snapshot_context: Optional[SnapshotContext] = None,
+    link_text: Optional[str] = "Browse",
+    link_attrs: Optional[Dict[str, str]] = {
+        "class": "btn btn-default btn-sm",
+        "role": "button",
+    },
+) -> Optional[str]:
     """
     Utility function for generating a link to a revision log HTML view
     (possibly in the context of an origin) to insert in Django templates.
 
     Args:
-        revision_id (str): revision identifier the history heads to
-        snapshot_context (dict): if provided, generate snapshot-dependent
+        revision_id: revision identifier the history heads to
+        snapshot_context: if provided, generate snapshot-dependent
             browsing link
-        link_text (str): optional text to use for the generated link
+        link_text: optional text to use for the generated link
             (the revision id will be used by default)
-        link_attrs (dict): optional attributes (e.g. class)
+        link_attrs: optional attributes (e.g. class)
             to add to the link
 
     Returns:
@@ -542,15 +577,17 @@
     return gen_link(revision_log_url, link_text, link_attrs)
 
 
-def gen_person_mail_link(person, link_text=None):
+def gen_person_mail_link(
+    person: Dict[str, Any], link_text: Optional[str] = None
+) -> str:
     """
     Utility function for generating a mail link to a person to insert
     in Django templates.
 
     Args:
-        person (dict): dictionary containing person data
+        person: dictionary containing person data
             (*name*, *email*, *fullname*)
-        link_text (str): optional text to use for the generated mail link
+        link_text: optional text to use for the generated mail link
             (the person name will be used by default)
 
     Returns:
@@ -570,20 +607,23 @@
 
 
 def gen_release_link(
-    sha1_git,
-    snapshot_context=None,
-    link_text="Browse",
-    link_attrs={"class": "btn btn-default btn-sm", "role": "button"},
-):
+    sha1_git: str,
+    snapshot_context: Optional[SnapshotContext] = None,
+    link_text: Optional[str] = "Browse",
+    link_attrs: Optional[Dict[str, str]] = {
+        "class": "btn btn-default btn-sm",
+        "role": "button",
+    },
+) -> str:
     """
     Utility function for generating a link to a release HTML view
     to insert in Django templates.
 
     Args:
-        sha1_git (str): release identifier
-        link_text (str): optional text for the generated link
+        sha1_git: release identifier
+        link_text: optional text for the generated link
             (the release id will be used by default)
-        link_attrs (dict): optional attributes (e.g. class)
+        link_attrs: optional attributes (e.g. class)
             to add to the link
 
     Returns:
@@ -601,7 +641,11 @@
     return gen_link(release_url, link_text, link_attrs)
 
 
-def format_log_entries(revision_log, per_page, snapshot_context=None):
+def format_log_entries(
+    revision_log: Iterator[Optional[Dict[str, Any]]],
+    per_page: int,
+    snapshot_context: Optional[SnapshotContext] = None,
+) -> List[Dict[str, str]]:
     """
     Utility functions that process raw revision log data for HTML display.
     Its purpose is to:
@@ -611,15 +655,17 @@
         * truncate the message log
 
     Args:
-        revision_log (list): raw revision log as returned by the swh-web api
-        per_page (int): number of log entries per page
-        snapshot_context (dict): if provided, generate snapshot-dependent
+        revision_log: raw revision log as returned by the swh-web api
+        per_page: number of log entries per page
+        snapshot_context: if provided, generate snapshot-dependent
             browsing link
 
 
     """
     revision_log_data = []
     for i, rev in enumerate(revision_log):
+        if rev is None:
+            continue
         if i == per_page:
             break
         author_name = "None"
@@ -666,17 +712,19 @@
 ]
 
 
-def get_readme_to_display(readmes):
+def get_readme_to_display(
+    readmes: Dict[str, str]
+) -> Tuple[Optional[str], Optional[str], Optional[str]]:
     """
     Process a list of readme files found in a directory
     in order to find the adequate one to display.
 
     Args:
-        readmes: a list of dict where keys are readme file names and values
+        readmes: a dict where keys are readme file names and values
             are readme sha1s
 
     Returns:
-        A tuple (readme_name, readme_sha1)
+        A tuple (readme_name, readme_url, readme_html)
     """
     readme_name = None
     readme_url = None
diff --git a/swh/web/browse/views/content.py b/swh/web/browse/views/content.py
--- a/swh/web/browse/views/content.py
+++ b/swh/web/browse/views/content.py
@@ -5,8 +5,9 @@
 
 import difflib
 from distutils.util import strtobool
+from typing import Any, Dict, Optional
 
-from django.http import HttpResponse, JsonResponse
+from django.http import HttpRequest, HttpResponse, JsonResponse
 from django.shortcuts import redirect, render
 
 from swh.model.hashutil import hash_to_hex
@@ -32,11 +33,11 @@
 
 
 @browse_route(
-    r"content/(?P<query_string>[0-9a-z_:]*[0-9a-f]+.)/raw/",
+    r"content/(?P<query_string>[0-9a-z_:]*[0-9a-f]+)/raw/",
     view_name="browse-content-raw",
     checksum_args=["query_string"],
 )
-def content_raw(request, query_string):
+def content_raw(request: HttpRequest, query_string: str) -> HttpResponse:
     """Django view that produces a raw display of a content identified
     by its hash value.
 
@@ -70,10 +71,12 @@
 
 
 @browse_route(
-    r"content/(?P<from_query_string>.*)/diff/(?P<to_query_string>.*)/",
+    r"content/(?P<from_query_string>.+)/diff/(?P<to_query_string>.+)/",
     view_name="diff-contents",
 )
-def _contents_diff(request, from_query_string, to_query_string):
+def _contents_diff(
+    request: HttpRequest, from_query_string: str, to_query_string: str
+) -> HttpResponse:
     """
     Browse endpoint used to compute unified diffs between two contents.
 
@@ -102,11 +105,11 @@
     content_to_size = 0
     content_from_lines = []
     content_to_lines = []
-    force = request.GET.get("force", "false")
+    force_str = request.GET.get("force", "false")
     path = request.GET.get("path", None)
     language = "plaintext"
 
-    force = bool(strtobool(force))
+    force = bool(strtobool(force_str))
 
     if from_query_string == to_query_string:
         diff_str = "File renamed without changes"
@@ -173,7 +176,7 @@
     return JsonResponse(diff_data)
 
 
-def _get_content_from_request(request):
+def _get_content_from_request(request: HttpRequest) -> Dict[str, Any]:
     path = request.GET.get("path")
     if path is None:
         raise BadInputExc("The path query parameter must be provided.")
@@ -196,16 +199,19 @@
         browse_context="content",
     )
     root_directory = snapshot_context["root_directory"]
+    assert root_directory is not None  # to keep mypy happy
     return archive.lookup_directory_with_path(root_directory, path)
 
 
 @browse_route(
-    r"content/(?P<query_string>[0-9a-z_:]*[0-9a-f]+.)/",
+    r"content/(?P<query_string>[0-9a-z_:]*[0-9a-f]+)/",
     r"content/",
     view_name="browse-content",
     checksum_args=["query_string"],
 )
-def content_display(request, query_string=None):
+def content_display(
+    request: HttpRequest, query_string: Optional[str] = None
+) -> HttpResponse:
     """Django view that produces an HTML display of a content identified
     by its hash value.
 
@@ -215,11 +221,11 @@
     """
     if query_string is None:
         # this case happens when redirected from origin/content or snapshot/content
-        content = _get_content_from_request(request)
+        content_data = _get_content_from_request(request)
         return redirect(
             reverse(
                 "browse-content",
-                url_args={"query_string": f"sha1_git:{content['target']}"},
+                url_args={"query_string": f"sha1_git:{content_data['target']}"},
                 query_params=request.GET,
             ),
         )
@@ -233,7 +239,7 @@
     snapshot_id = request.GET.get("snapshot") or request.GET.get("snapshot_id")
     path = request.GET.get("path")
     content_data = {}
-    error_info = {"status_code": 200, "description": None}
+    error_info: Dict[str, Any] = {"status_code": 200, "description": None}
     try:
         content_data = request_content(query_string)
     except NotFoundExc as e:
@@ -256,7 +262,7 @@
                 browse_context="content",
             )
         except NotFoundExc as e:
-            if str(e).startswith("Origin"):
+            if str(e).startswith("Origin") and origin_url is not None:
                 raw_cnt_url = reverse(
                     "browse-content", url_args={"query_string": query_string}
                 )
@@ -327,9 +333,9 @@
                     query_params=query_params,
                 )
                 breadcrumbs.append({"name": pi["name"], "url": dir_url})
-        breadcrumbs.append({"name": filename, "url": None})
+        breadcrumbs.append({"name": filename, "url": ""})
 
-    if path and root_dir != path:
+    if path and root_dir is not None and root_dir != path:
         dir_info = archive.lookup_directory_with_path(root_dir, path)
         directory_id = dir_info["target"]
     elif root_dir != path:
@@ -360,10 +366,10 @@
         sha256=content_checksums.get("sha256"),
         blake2s256=content_checksums.get("blake2s256"),
         content_url=content_url,
-        mimetype=content_data.get("mimetype"),
-        encoding=content_data.get("encoding"),
+        mimetype=content_data.get("mimetype", ""),
+        encoding=content_data.get("encoding", ""),
         size=content_data.get("length", 0),
-        language=content_data.get("language"),
+        language=content_data.get("language", ""),
         root_directory=root_dir,
         path=f"/{path}" if path else None,
         filename=filename or "",
@@ -418,7 +424,7 @@
 
     heading = "Content - %s" % content_checksums.get("sha1_git")
     if breadcrumbs:
-        content_path = "/".join([bc["name"] for bc in breadcrumbs])
+        content_path = "/".join(bc["name"] for bc in breadcrumbs)
         heading += " - %s" % content_path
 
     return render(
diff --git a/swh/web/browse/views/directory.py b/swh/web/browse/views/directory.py
--- a/swh/web/browse/views/directory.py
+++ b/swh/web/browse/views/directory.py
@@ -4,8 +4,9 @@
 # See top-level LICENSE file for more information
 
 import os
+from typing import Any, Dict, Optional
 
-from django.http import HttpResponse
+from django.http import HttpRequest, HttpResponse
 from django.shortcuts import redirect, render
 
 from swh.model.swhids import ObjectType
@@ -23,21 +24,24 @@
 from swh.web.common.utils import gen_path_info, reverse, swh_object_icons
 
 
-def _directory_browse(request, sha1_git, path=None):
+def _directory_browse(
+    request: HttpRequest, sha1_git: str, path: Optional[str] = None
+) -> HttpResponse:
     root_sha1_git = sha1_git
-    error_info = {"status_code": 200, "description": None}
+    dir_sha1_git: Optional[str] = sha1_git
+    error_info: Dict[str, Any] = {"status_code": 200, "description": None}
     if path:
         try:
             dir_info = archive.lookup_directory_with_path(sha1_git, path)
-            sha1_git = dir_info["target"]
+            dir_sha1_git = dir_info["target"]
         except NotFoundExc as e:
             error_info["status_code"] = 404
             error_info["description"] = f"NotFoundExc: {str(e)}"
-            sha1_git = None
+            dir_sha1_git = None
 
     dirs, files = [], []
-    if sha1_git is not None:
-        dirs, files = get_directory_entries(sha1_git)
+    if dir_sha1_git is not None:
+        dirs, files = get_directory_entries(dir_sha1_git)
     origin_url = request.GET.get("origin_url")
     if not origin_url:
         origin_url = request.GET.get("origin")
@@ -54,9 +58,9 @@
                 path=path,
             )
         except NotFoundExc as e:
-            if str(e).startswith("Origin"):
+            if str(e).startswith("Origin") and origin_url is not None:
                 raw_dir_url = reverse(
-                    "browse-directory", url_args={"sha1_git": sha1_git}
+                    "browse-directory", url_args={"sha1_git": dir_sha1_git}
                 )
                 error_message = (
                     "The Software Heritage archive has a directory "
@@ -144,7 +148,7 @@
 
     dir_metadata = DirectoryMetadata(
         object_type=ObjectType.DIRECTORY,
-        object_id=sha1_git,
+        object_id=dir_sha1_git,
         directory=root_sha1_git,
         nb_files=len(files),
         nb_dirs=len(dirs),
@@ -159,12 +163,14 @@
 
     vault_cooking = {
         "directory_context": True,
-        "directory_swhid": f"swh:1:dir:{sha1_git}",
+        "directory_swhid": f"swh:1:dir:{dir_sha1_git}",
         "revision_context": False,
         "revision_swhid": None,
     }
 
-    swh_objects = [SWHObjectInfo(object_type=ObjectType.DIRECTORY, object_id=sha1_git)]
+    swh_objects = [
+        SWHObjectInfo(object_type=ObjectType.DIRECTORY, object_id=dir_sha1_git)
+    ]
 
     if snapshot_context:
         if snapshot_context["revision_id"]:
@@ -190,7 +196,7 @@
 
     swhids_info = get_swhids_info(swh_objects, snapshot_context, dir_metadata)
 
-    heading = "Directory - %s" % sha1_git
+    heading = "Directory - %s" % dir_sha1_git
     if breadcrumbs:
         dir_path = "/".join([bc["name"] for bc in breadcrumbs]) + "/"
         heading += " - %s" % dir_path
@@ -244,7 +250,7 @@
     view_name="browse-directory",
     checksum_args=["sha1_git"],
 )
-def directory_browse(request, sha1_git):
+def directory_browse(request: HttpRequest, sha1_git: str) -> HttpResponse:
     """Django view for browsing the content of a directory identified
     by its sha1_git value.
 
@@ -259,7 +265,9 @@
     view_name="browse-directory-legacy",
     checksum_args=["sha1_git"],
 )
-def directory_browse_legacy(request, sha1_git, path):
+def directory_browse_legacy(
+    request: HttpRequest, sha1_git: str, path: str
+) -> HttpResponse:
     """Django view for browsing the content of a directory identified
     by its sha1_git value.
 
@@ -274,13 +282,15 @@
     view_name="browse-directory-resolve-content-path",
     checksum_args=["sha1_git"],
 )
-def _directory_resolve_content_path(request, sha1_git):
+def _directory_resolve_content_path(
+    request: HttpRequest, sha1_git: str
+) -> HttpResponse:
     """
     Internal endpoint redirecting to data url for a specific file path
     relative to a root directory.
     """
     try:
-        path = os.path.normpath(request.GET.get("path"))
+        path = os.path.normpath(request.GET.get("path", ""))
         if not path.startswith("../"):
             dir_info = archive.lookup_directory_with_path(sha1_git, path)
             if dir_info["type"] == "file":
diff --git a/swh/web/browse/views/origin.py b/swh/web/browse/views/origin.py
--- a/swh/web/browse/views/origin.py
+++ b/swh/web/browse/views/origin.py
@@ -1,8 +1,11 @@
-# Copyright (C) 2021 The Software Heritage developers
+# Copyright (C) 2021-2022 The Software Heritage developers
 # See the AUTHORS file at the top-level directory of this distribution
 # License: GNU Affero General Public License version 3, or any later version
 # See top-level LICENSE file for more information
 
+from typing import Any, Dict, List, Optional, cast
+
+from django.http import HttpRequest, HttpResponse
 from django.shortcuts import redirect, render
 
 from swh.web.browse.browseurls import browse_route
@@ -25,7 +28,7 @@
     r"origin/directory/",
     view_name="browse-origin-directory",
 )
-def origin_directory_browse(request):
+def origin_directory_browse(request: HttpRequest) -> HttpResponse:
     """Django view for browsing the content of a directory associated
     to an origin for a given visit.
 
@@ -47,7 +50,12 @@
     r"origin/(?P<origin_url>.+)/directory/",
     view_name="browse-origin-directory-legacy",
 )
-def origin_directory_browse_legacy(request, origin_url, timestamp=None, path=None):
+def origin_directory_browse_legacy(
+    request: HttpRequest,
+    origin_url: str,
+    timestamp: Optional[str] = None,
+    path: Optional[str] = None,
+) -> HttpResponse:
     """Django view for browsing the content of a directory associated
     to an origin for a given visit.
 
@@ -68,7 +76,7 @@
     r"origin/content/",
     view_name="browse-origin-content",
 )
-def origin_content_browse(request):
+def origin_content_browse(request: HttpRequest) -> HttpResponse:
     """
     This route is deprecated; use http:get:`/browse/content` instead
 
@@ -87,7 +95,12 @@
     r"origin/(?P<origin_url>.+)/content/",
     view_name="browse-origin-content-legacy",
 )
-def origin_content_browse_legacy(request, origin_url, path=None, timestamp=None):
+def origin_content_browse_legacy(
+    request: HttpRequest,
+    origin_url: str,
+    path: Optional[str] = None,
+    timestamp: Optional[str] = None,
+) -> HttpResponse:
     """
     This route is deprecated; use http:get:`/browse/content` instead
 
@@ -106,7 +119,7 @@
     r"origin/log/",
     view_name="browse-origin-log",
 )
-def origin_log_browse(request):
+def origin_log_browse(request: HttpRequest) -> HttpResponse:
     """
     This route is deprecated; use http:get:`/browse/snapshot/log` instead
 
@@ -123,7 +136,9 @@
     r"origin/(?P<origin_url>.+)/log/",
     view_name="browse-origin-log-legacy",
 )
-def origin_log_browse_legacy(request, origin_url, timestamp=None):
+def origin_log_browse_legacy(
+    request: HttpRequest, origin_url: str, timestamp: Optional[str] = None
+) -> HttpResponse:
     """
     This route is deprecated; use http:get:`/browse/snapshot/log` instead
 
@@ -145,7 +160,7 @@
     r"origin/branches/",
     view_name="browse-origin-branches",
 )
-def origin_branches_browse(request):
+def origin_branches_browse(request: HttpRequest) -> HttpResponse:
     """
     This route is deprecated; use http:get:`/browse/snapshot/branches` instead
 
@@ -163,7 +178,9 @@
     r"origin/(?P<origin_url>.+)/branches/",
     view_name="browse-origin-branches-legacy",
 )
-def origin_branches_browse_legacy(request, origin_url, timestamp=None):
+def origin_branches_browse_legacy(
+    request: HttpRequest, origin_url: str, timestamp: Optional[str] = None
+) -> HttpResponse:
     """
     This route is deprecated; use http:get:`/browse/snapshot/branches` instead
 
@@ -182,7 +199,7 @@
     r"origin/releases/",
     view_name="browse-origin-releases",
 )
-def origin_releases_browse(request):
+def origin_releases_browse(request: HttpRequest) -> HttpResponse:
     """
     This route is deprecated; use http:get:`/browse/snapshot/releases` instead
 
@@ -200,7 +217,9 @@
     r"origin/(?P<origin_url>.+)/releases/",
     view_name="browse-origin-releases-legacy",
 )
-def origin_releases_browse_legacy(request, origin_url, timestamp=None):
+def origin_releases_browse_legacy(
+    request: HttpRequest, origin_url: str, timestamp: Optional[str] = None
+) -> HttpResponse:
     """
     This route is deprecated; use http:get:`/browse/snapshot/releases` instead
 
@@ -215,12 +234,15 @@
     return redirect_to_new_route(request, "browse-snapshot-releases")
 
 
-def _origin_visits_browse(request, origin_url):
+def _origin_visits_browse(
+    request: HttpRequest, origin_url: Optional[str]
+) -> HttpResponse:
     if origin_url is None:
         raise BadInputExc("An origin URL must be provided as query parameter.")
 
     origin_info = archive.lookup_origin({"url": origin_url})
-    origin_visits = get_origin_visits(origin_info)
+    origin_visits = cast(List[Dict[str, Any]], get_origin_visits(origin_info))
+
     snapshot_context = get_snapshot_context(origin_url=origin_url)
 
     for i, visit in enumerate(origin_visits):
@@ -263,7 +285,7 @@
 
 
 @browse_route(r"origin/visits/", view_name="browse-origin-visits")
-def origin_visits_browse(request):
+def origin_visits_browse(request: HttpRequest) -> HttpResponse:
     """Django view that produces an HTML display of visits reporting
     for a given origin.
 
@@ -276,7 +298,7 @@
 @browse_route(
     r"origin/(?P<origin_url>.+)/visits/", view_name="browse-origin-visits-legacy"
 )
-def origin_visits_browse_legacy(request, origin_url):
+def origin_visits_browse_legacy(request: HttpRequest, origin_url: str) -> HttpResponse:
     """Django view that produces an HTML display of visits reporting
     for a given origin.
 
@@ -287,7 +309,7 @@
 
 
 @browse_route(r"origin/", view_name="browse-origin")
-def origin_browse(request):
+def origin_browse(request: HttpRequest) -> HttpResponse:
     """Django view that redirects to the display of the latest archived
     snapshot for a given software origin.
     """
@@ -299,7 +321,7 @@
 
 
 @browse_route(r"origin/(?P<origin_url>.+)/", view_name="browse-origin-legacy")
-def origin_browse_legacy(request, origin_url):
+def origin_browse_legacy(request: HttpRequest, origin_url: str) -> HttpResponse:
     """Django view that redirects to the display of the latest archived
     snapshot for a given software origin.
     """
diff --git a/swh/web/browse/views/release.py b/swh/web/browse/views/release.py
--- a/swh/web/browse/views/release.py
+++ b/swh/web/browse/views/release.py
@@ -3,6 +3,9 @@
 # License: GNU Affero General Public License version 3, or any later version
 # See top-level LICENSE file for more information
 
+from typing import Optional
+
+from django.http import HttpRequest, HttpResponse
 from django.shortcuts import render
 
 from swh.model.swhids import ObjectType
@@ -19,7 +22,7 @@
 from swh.web.common import archive
 from swh.web.common.exc import NotFoundExc, sentry_capture_exception
 from swh.web.common.identifiers import get_swhids_info
-from swh.web.common.typing import ReleaseMetadata, SWHObjectInfo
+from swh.web.common.typing import ReleaseMetadata, SnapshotContext, SWHObjectInfo
 from swh.web.common.utils import format_utc_iso_date, reverse
 
 
@@ -28,7 +31,7 @@
     view_name="browse-release",
     checksum_args=["sha1_git"],
 )
-def release_browse(request, sha1_git):
+def release_browse(request: HttpRequest, sha1_git: str) -> HttpResponse:
     """
     Django view that produces an HTML display of a release
     identified by its id.
@@ -36,7 +39,7 @@
     The url that points to it is :http:get:`/browse/release/(sha1_git)/`.
     """
     release = archive.lookup_release(sha1_git)
-    snapshot_context = {}
+    snapshot_context: Optional[SnapshotContext] = None
     origin_info = None
     snapshot_id = request.GET.get("snapshot_id")
     if not snapshot_id:
@@ -76,7 +79,8 @@
             snapshot_id, release_name=release["name"]
         )
 
-    snapshot_id = snapshot_context.get("snapshot_id", None)
+    if snapshot_context is not None:
+        snapshot_id = snapshot_context.get("snapshot_id", None)
 
     release_metadata = ReleaseMetadata(
         object_type=ObjectType.RELEASE,
diff --git a/swh/web/browse/views/revision.py b/swh/web/browse/views/revision.py
--- a/swh/web/browse/views/revision.py
+++ b/swh/web/browse/views/revision.py
@@ -6,8 +6,9 @@
 import hashlib
 import json
 import textwrap
+from typing import Any, Dict, List, Optional
 
-from django.http import JsonResponse
+from django.http import HttpRequest, HttpResponse, JsonResponse
 from django.shortcuts import render
 from django.utils.safestring import mark_safe
 
@@ -30,7 +31,7 @@
 from swh.web.common import archive
 from swh.web.common.exc import NotFoundExc, http_status_code_message
 from swh.web.common.identifiers import get_swhids_info
-from swh.web.common.typing import RevisionMetadata, SWHObjectInfo
+from swh.web.common.typing import RevisionMetadata, SnapshotContext, SWHObjectInfo
 from swh.web.common.utils import (
     format_utc_iso_date,
     gen_path_info,
@@ -39,7 +40,12 @@
 )
 
 
-def _gen_content_url(revision, query_string, path, snapshot_context):
+def _gen_content_url(
+    revision: Dict[str, Any],
+    query_string: str,
+    path: str,
+    snapshot_context: Optional[SnapshotContext],
+) -> str:
     if snapshot_context:
         query_params = snapshot_context["query_params"]
         query_params["path"] = path
@@ -55,7 +61,7 @@
     return content_url
 
 
-def _gen_diff_link(idx, diff_anchor, link_text):
+def _gen_diff_link(idx: int, diff_anchor: str, link_text: str) -> str:
     if idx < _max_displayed_file_diffs:
         return gen_link(diff_anchor, link_text)
     else:
@@ -66,7 +72,11 @@
 _max_displayed_file_diffs = 1000
 
 
-def _gen_revision_changes_list(revision, changes, snapshot_context):
+def _gen_revision_changes_list(
+    revision: Dict[str, Any],
+    changes: List[Dict[str, Any]],
+    snapshot_context: Optional[SnapshotContext],
+) -> str:
     """
     Returns a HTML string describing the file changes
     introduced in a revision.
@@ -151,7 +161,7 @@
     view_name="diff-revision",
     checksum_args=["sha1_git"],
 )
-def _revision_diff(request, sha1_git):
+def _revision_diff(request: HttpRequest, sha1_git: str) -> HttpResponse:
     """
     Browse internal endpoint to compute revision diff
     """
@@ -161,7 +171,8 @@
     if not origin_url:
         origin_url = request.GET.get("origin", None)
     timestamp = request.GET.get("timestamp", None)
-    visit_id = request.GET.get("visit_id", None)
+    visit_id_str = request.GET.get("visit_id", None)
+    visit_id = int(visit_id_str) if visit_id_str is not None else None
     if origin_url:
         snapshot_context = get_snapshot_context(
             origin_url=origin_url, timestamp=timestamp, visit_id=visit_id
@@ -186,7 +197,7 @@
     view_name="browse-revision-log",
     checksum_args=["sha1_git"],
 )
-def revision_log_browse(request, sha1_git):
+def revision_log_browse(request: HttpRequest, sha1_git: str) -> HttpResponse:
     """
     Django view that produces an HTML display of the history
     log for a revision identified by its id.
@@ -296,7 +307,7 @@
     view_name="browse-revision",
     checksum_args=["sha1_git"],
 )
-def revision_browse(request, sha1_git):
+def revision_browse(request: HttpRequest, sha1_git: str) -> HttpResponse:
     """
     Django view that produces an HTML display of a revision
     identified by its id.
@@ -350,7 +361,7 @@
     elif snapshot_id:
         snapshot_context = get_snapshot_context(snapshot_id)
 
-    error_info = {"status_code": 200, "description": None}
+    error_info: Dict[str, Any] = {"status_code": 200, "description": None}
 
     if path:
         try:
diff --git a/swh/web/browse/views/snapshot.py b/swh/web/browse/views/snapshot.py
--- a/swh/web/browse/views/snapshot.py
+++ b/swh/web/browse/views/snapshot.py
@@ -1,9 +1,11 @@
-# Copyright (C) 2018-2019  The Software Heritage developers
+# Copyright (C) 2018-2022  The Software Heritage developers
 # See the AUTHORS file at the top-level directory of this distribution
 # License: GNU Affero General Public License version 3, or any later version
 # See top-level LICENSE file for more information
 
+from typing import Optional
 
+from django.http import HttpRequest, HttpResponse
 from django.shortcuts import redirect
 
 from swh.web.browse.browseurls import browse_route
@@ -18,7 +20,7 @@
 from swh.web.common.utils import redirect_to_new_route, reverse
 
 
-def get_snapshot_from_request(request):
+def get_snapshot_from_request(request: HttpRequest) -> str:
     snapshot = request.GET.get("snapshot")
     if snapshot:
         return snapshot
@@ -34,7 +36,7 @@
     view_name="browse-snapshot",
     checksum_args=["snapshot_id"],
 )
-def snapshot_browse(request, snapshot_id):
+def snapshot_browse(request: HttpRequest, snapshot_id: str) -> HttpResponse:
     """Django view for browsing the content of a snapshot.
 
     The url that points to it is :http:get:`/browse/snapshot/(snapshot_id)/`
@@ -52,7 +54,7 @@
     view_name="browse-snapshot-directory",
     checksum_args=["snapshot_id"],
 )
-def snapshot_directory_browse(request, snapshot_id):
+def snapshot_directory_browse(request: HttpRequest, snapshot_id: str) -> HttpResponse:
     """Django view for browsing the content of a directory collected
     in a snapshot.
 
@@ -71,7 +73,9 @@
     view_name="browse-snapshot-directory-legacy",
     checksum_args=["snapshot_id"],
 )
-def snapshot_directory_browse_legacy(request, snapshot_id, path=None):
+def snapshot_directory_browse_legacy(
+    request: HttpRequest, snapshot_id: str, path: Optional[str] = None
+) -> HttpResponse:
     """Django view for browsing the content of a directory collected
     in a snapshot.
 
@@ -91,7 +95,7 @@
     view_name="browse-snapshot-content",
     checksum_args=["snapshot_id"],
 )
-def snapshot_content_browse(request, snapshot_id):
+def snapshot_content_browse(request: HttpRequest, snapshot_id: str) -> HttpResponse:
     """
     This route is deprecated; use http:get:`/browse/content` instead
 
@@ -109,7 +113,9 @@
     view_name="browse-snapshot-content-legacy",
     checksum_args=["snapshot_id"],
 )
-def snapshot_content_browse_legacy(request, snapshot_id, path):
+def snapshot_content_browse_legacy(
+    request: HttpRequest, snapshot_id: str, path: str
+) -> HttpResponse:
     """
     This route is deprecated; use http:get:`/browse/content` instead
 
@@ -128,7 +134,9 @@
     view_name="browse-snapshot-log",
     checksum_args=["snapshot_id"],
 )
-def snapshot_log_browse(request, snapshot_id=None):
+def snapshot_log_browse(
+    request: HttpRequest, snapshot_id: Optional[str] = None
+) -> HttpResponse:
     """Django view that produces an HTML display of revisions history (aka
     the commit log) collected in a snapshot.
 
@@ -161,7 +169,9 @@
     view_name="browse-snapshot-branches",
     checksum_args=["snapshot_id"],
 )
-def snapshot_branches_browse(request, snapshot_id=None):
+def snapshot_branches_browse(
+    request: HttpRequest, snapshot_id: Optional[str] = None
+) -> HttpResponse:
     """Django view that produces an HTML display of the list of branches
     collected in a snapshot.
 
@@ -196,7 +206,9 @@
     view_name="browse-snapshot-releases",
     checksum_args=["snapshot_id"],
 )
-def snapshot_releases_browse(request, snapshot_id=None):
+def snapshot_releases_browse(
+    request: HttpRequest, snapshot_id: Optional[str] = None
+) -> HttpResponse:
     """Django view that produces an HTML display of the list of releases
     collected in a snapshot.
 
diff --git a/swh/web/common/identifiers.py b/swh/web/common/identifiers.py
--- a/swh/web/common/identifiers.py
+++ b/swh/web/common/identifiers.py
@@ -1,9 +1,9 @@
-# Copyright (C) 2020-2021  The Software Heritage developers
+# Copyright (C) 2020-2022  The Software Heritage developers
 # See the AUTHORS file at the top-level directory of this distribution
 # License: GNU Affero General Public License version 3, or any later version
 # See top-level LICENSE file for more information
 
-from typing import Any, Dict, Iterable, List, Optional
+from typing import Any, Dict, Iterable, List, Mapping, Optional
 from urllib.parse import quote, unquote
 
 from typing_extensions import TypedDict
@@ -274,7 +274,7 @@
 def get_swhids_info(
     swh_objects: Iterable[SWHObjectInfo],
     snapshot_context: Optional[SnapshotContext] = None,
-    extra_context: Optional[Dict[str, Any]] = None,
+    extra_context: Optional[Mapping[str, Any]] = None,
 ) -> List[SWHIDInfo]:
     """
     Returns a list of dict containing info related to SWHIDs of objects.
diff --git a/swh/web/common/typing.py b/swh/web/common/typing.py
--- a/swh/web/common/typing.py
+++ b/swh/web/common/typing.py
@@ -137,7 +137,7 @@
 
 class SWHObjectInfo(TypedDict):
     object_type: ObjectType
-    object_id: str
+    object_id: Optional[str]
 
 
 class SWHIDContext(TypedDict, total=False):
@@ -182,12 +182,12 @@
 
 
 class DirectoryMetadata(SWHObjectInfo, SWHObjectInfoMetadata):
-    directory: str
-    nb_files: int
-    nb_dirs: int
-    sum_file_sizes: int
+    directory: Optional[str]
+    nb_files: Optional[int]
+    nb_dirs: Optional[int]
+    sum_file_sizes: Optional[int]
     root_directory: Optional[str]
-    path: str
+    path: Optional[str]
     revision: Optional[str]
     revision_found: Optional[bool]
     release: Optional[str]