diff --git a/pygitweb/README.md b/pygitweb/README.md
index 169a7e2..27bc110 100644
--- a/pygitweb/README.md
+++ b/pygitweb/README.md
@@ -62,10 +62,11 @@ See [ROUTES.md](ROUTES.md) for more detail.
 - `GET /{project}?a=blobdiff&h=...&hb=...&f=...&fp=...` — blob diff (two versions of a file) rendered with diff2html
 - `GET /{project}?a=blobpatch&h=...&hb=...&f=...&fp=...` — blob diff as plain unified diff
 - `GET /{project}?a=remotes` — list repo remotes
+- `GET /{project}?a=object&h=...` — show object by type (commit, tree, tag, or blob)
 
 **Project-scoped actions (stub only)**
 
-These actions are accepted but return a minimal placeholder page. Add handlers in `main.py` to implement: `blame`, `blame_incremental`, `blame_data`, `rss`, `atom`, `search`, `search_help`, `snapshot`, `object`.
+These actions are accepted but return a minimal placeholder page. Add handlers in `main.py` to implement: `blame`, `blame_incremental`, `blame_data`, `rss`, `atom`, `search`, `search_help`.
 
 ## Query parameter short names (CGI mapping)
 
diff --git a/pygitweb/main.py b/pygitweb/main.py
index ea63702..dc5314f 100644
--- a/pygitweb/main.py
+++ b/pygitweb/main.py
@@ -497,6 +497,8 @@ def dispatch(
         return git_commitdiff(proj, hash_param)
     if action == "remotes":
         return git_remotes(proj)
+    if action == "object":
+        return git_object(proj, hash_param)
     # Stub others with minimal response
     pre = PREAMBLE.render(title=f"{esc_html(action)} - {esc_html(proj)}", site_name=config.SITE_NAME)
     return HTMLResponse(
@@ -509,6 +511,51 @@ def _object_type(project: str, ref: str) -> str | None:
     return git_get_type(project, ref)
 
 
+def git_object(project: str, h: str | None) -> Response:
+    """Show object by type: commit, tree, tag, or blob. Dispatches to the appropriate view."""
+    if not h:
+        raise HTTPException(status_code=400, detail="Object hash (h) required")
+    if not is_valid_ref_format(h):
+        raise HTTPException(status_code=400, detail="Invalid ref or hash")
+    obj_type = _object_type(project, h)
+    if not obj_type:
+        raise HTTPException(status_code=404, detail="Object not found")
+    if obj_type == "commit":
+        return git_commit(project, h)
+    if obj_type == "tree":
+        return git_tree(project, h, None)
+    if obj_type == "tag":
+        return git_tag(project, h)
+    if obj_type == "blob":
+        try:
+            from pygitweb.git_helpers import open_repo
+            repo = open_repo(project)
+            obj = repo.revparse_single(h)
+        except (KeyError, pygit2.GitError, OSError):
+            raise HTTPException(status_code=404, detail="Object not found")
+        if not isinstance(obj, pygit2.Blob):
+            raise HTTPException(status_code=404, detail="Not a blob")
+        data = obj.data
+        text = to_utf8(data) or ""
+        body = esc_html(sanitize(text) or "") or ""
+        lines = text.split("\n")
+        num_lines = max(1, len(lines))
+        line_nums = "\n".join(str(i) for i in range(1, num_lines + 1))
+        oid_short = str(obj.id)[:7]
+        blob_html = (
+            f"<div class=\"blob-view\">"
+            f"<div class=\"blob-line-nums\" aria-hidden=\"true\">{esc_html(line_nums)}</div>"
+            f"<pre class=\"blob-content\"><code class=\"hljs\">{body}</code></pre>"
+            f"</div>"
+        )
+        pre = PREAMBLE.render(
+            title=f"Blob {oid_short} - {esc_html(project)}",
+            site_name=config.SITE_NAME,
+        )
+        return HTMLResponse(f"{pre}<h1>Blob {esc_html(oid_short)}</h1><p>Project: <a href='/{project}'>{esc_html(project)}</a></p>{blob_html}{POSTAMBLE}")
+    raise HTTPException(status_code=404, detail="Unknown object type")
+
+
 # Common README filenames to look for (order matters: prefer README.md)
 README_CANDIDATES = ["README.md", "README", "readme.md", "readme", "Readme.md"]
 
@@ -1068,6 +1115,12 @@ def git_commit(project: str, h: str | None) -> HTMLResponse:
         ["Date", esc_html(author_date)],
         ["tree", f'<a href="{tree_link}">{esc_html(tree_oid[:7])}</a>'],
     ]
+    for i, parent_id in enumerate(commit.parent_ids):
+        parent_oid = str(parent_id)
+        parent_short = parent_oid[:7]
+        parent_link = f"/{project}?a=commit&h={quote(parent_oid, safe='')}"
+        label = "parent" if len(commit.parent_ids) == 1 else f"parent ({i + 1})"
+        table_rows.append([label, f'<a href="{parent_link}">{esc_html(parent_short)}</a>'])
     if message:
         table_rows.append(["", f"<pre class='commit-message'>{esc_html(message)}</pre>"])
     table_rows.append(["", f'<a href="{diff_link}">diff</a> · <a href="{patch_link}">patch</a>'])
