diff --git a/pygitweb/git_helpers.py b/pygitweb/git_helpers.py
index 097837e..5beb131 100644
--- a/pygitweb/git_helpers.py
+++ b/pygitweb/git_helpers.py
@@ -306,10 +306,36 @@ def git_get_heads_list(project: str) -> list[tuple[str, str, str]]:
     return [(ref.replace("refs/heads/", ""), ref, oid) for ref, oid in refs]
 
 
+def _tag_timestamp(repo: pygit2.Repository, oid: str) -> int:
+    """Return tagger time for an annotated tag, or committer time for a lightweight tag (commit)."""
+    try:
+        obj = repo.revparse_single(oid)
+        if isinstance(obj, pygit2.Tag) and obj.tagger:
+            return obj.tagger.time
+        if isinstance(obj, pygit2.Commit):
+            return obj.committer.time
+    except (KeyError, pygit2.GitError):
+        pass
+    return 0
+
+
 def git_get_tags_list(project: str) -> list[tuple[str, str, str]]:
-    """List (name, ref, oid) for tags. Port of git_get_tags_list."""
-    refs = git_get_references(project, "refs/tags")
-    return [(ref.replace("refs/tags/", ""), ref, oid) for ref, oid in refs]
+    """List (name, ref, oid) for tags using pygit2 references.iterator(GIT_REFERENCES_TAGS).
+    Sorted by tag creation time descending (newest first); lightweight tags use commit time.
+    """
+    try:
+        repo = open_repo(project)
+        result = []
+        for ref in repo.references.iterator(pygit2.GIT_REFERENCES_TAGS):
+            r = ref.resolve() if isinstance(ref.target, str) else ref
+            oid = str(r.target)
+            name = ref.name.replace("refs/tags/", "")
+            ts = _tag_timestamp(repo, oid)
+            result.append((name, ref.name, oid, ts))
+        result.sort(key=lambda x: (x[3], x[0]), reverse=True)  # by time desc, then name desc
+        return [(name, ref_name, oid) for name, ref_name, oid, _ in result]
+    except (pygit2.GitError, OSError):
+        return []
 
 
 def git_get_remotes_list(project: str) -> list[str]:
@@ -373,11 +399,13 @@ def get_commit_history(
     ref: str | None = None,
     path: str | None = None,
     max_count: int = 100,
+    skip: int = 0,
 ) -> list[dict[str, Any]]:
     """
     Get commit history for a project, optionally filtered by path.
     Returns list of commit dicts with oid and parsed commit data.
     Port of git log functionality.
+    skip: number of matching commits to skip (for pagination).
     """
     try:
         repo = open_repo(project)
@@ -400,6 +428,7 @@ def get_commit_history(
             return []
         
         commits = []
+        skipped = 0
         walker = repo.walk(start_oid, pygit2.GIT_SORT_TIME | pygit2.GIT_SORT_TOPOLOGICAL)
         
         if path:
@@ -445,6 +474,9 @@ def get_commit_history(
                         path_changed = entry is not None
                     
                     if path_changed or entry:
+                        if skipped < skip:
+                            skipped += 1
+                            continue
                         commit_data = parse_commit(project, str(commit.id))
                         commit_data["oid"] = str(commit.id)
                         commits.append(commit_data)
@@ -454,6 +486,9 @@ def get_commit_history(
         else:
             # No path filter, get all commits
             for commit in walker:
+                if skipped < skip:
+                    skipped += 1
+                    continue
                 if len(commits) >= max_count:
                     break
                 commit_data = parse_commit(project, str(commit.id))
diff --git a/pygitweb/main.py b/pygitweb/main.py
index 6e83e6e..e55954d 100644
--- a/pygitweb/main.py
+++ b/pygitweb/main.py
@@ -56,7 +56,7 @@ from pygitweb.validation import (
     is_valid_project,
     is_valid_ref_format,
 )
-from urllib.parse import quote
+from urllib.parse import quote, urlencode
 
 # Allowed actions (from %actions in gitweb.perl)
 ACTIONS = {
@@ -402,6 +402,40 @@ async def addproject_submit(
 # ---------- Routes (project required) ----------
 
 
+def _pagination_url(request: Request, page: int, pagecount: int) -> str:
+    """Build URL for a pagination page, preserving path and other query params."""
+    params = dict(request.query_params)
+    params["page"] = str(page)
+    params["pagecount"] = str(pagecount)
+    return f"{request.url.path}?{urlencode(params)}"
+
+
+def _parse_pagination(
+    page: str | None,
+    pagecount: str | None,
+) -> tuple[int, int]:
+    """Parse page and pagecount; default 1 and 25; raise if pagecount > 50."""
+    p = 1
+    pc = 25
+    if page is not None:
+        try:
+            p = int(page)
+        except ValueError:
+            raise HTTPException(status_code=400, detail="page must be an integer")
+        if p < 1:
+            raise HTTPException(status_code=400, detail="page must be at least 1")
+    if pagecount is not None:
+        try:
+            pc = int(pagecount)
+        except ValueError:
+            raise HTTPException(status_code=400, detail="pagecount must be an integer")
+        if pc < 1:
+            raise HTTPException(status_code=400, detail="pagecount must be at least 1")
+        if pc > 50:
+            raise HTTPException(status_code=400, detail="pagecount must not exceed 50")
+    return p, pc
+
+
 @app.get("/{project:path}", response_class=HTMLResponse)
 def dispatch(
     request: Request,
@@ -410,6 +444,8 @@ def dispatch(
     h: Annotated[str | None, Query(alias="h")] = None,
     hb: Annotated[str | None, Query(alias="hb")] = None,
     f: Annotated[str | None, Query(alias="f")] = None,
+    page: Annotated[str | None, Query(alias="page")] = None,
+    pagecount: Annotated[str | None, Query(alias="pagecount")] = None,
 ):
     """
     Dispatch by path: /project -> summary; /project/action/... -> action.
@@ -452,13 +488,17 @@ def dispatch(
     if action in ("blob", "blob_plain"):
         return git_blob(proj, hash_param, file_name, raw=(action == "blob_plain"))
     if action == "log":
-        return git_log(proj, hash_param)
+        p, pc = _parse_pagination(page, pagecount)
+        return git_log(proj, hash_param, request, p, pc)
     if action == "shortlog":
-        return git_shortlog(proj, hash_param)
+        p, pc = _parse_pagination(page, pagecount)
+        return git_shortlog(proj, hash_param, request, p, pc)
     if action == "history":
-        return git_history(proj, hash_param, file_name)
+        p, pc = _parse_pagination(page, pagecount)
+        return git_history(proj, hash_param, file_name, request, p, pc)
     if action == "tags":
-        return git_tags(proj)
+        p, pc = _parse_pagination(page, pagecount)
+        return git_tags(proj, request, p, pc)
     if action == "tag":
         return git_tag(proj, hash_param)
     if action == "commit":
@@ -658,91 +698,137 @@ def _format_commit_table_row(project: str, commit: dict[str, Any], short: bool =
         ]
 
 
-def git_log(project: str, h: str | None) -> HTMLResponse:
+def _render_pagination(
+    request: Request,
+    page: int,
+    pagecount: int,
+    has_prev: bool,
+    has_next: bool,
+    total_pages: int | None = None,
+) -> str:
+    """Render Tabler pagination HTML."""
+    prev_url = _pagination_url(request, page - 1, pagecount) if has_prev else ""
+    next_url = _pagination_url(request, page + 1, pagecount) if has_next else ""
+    page_links = None
+    if total_pages is not None and total_pages <= 20:
+        page_links = [(p, _pagination_url(request, p, pagecount)) for p in range(1, total_pages + 1)]
+    elif total_pages is not None:
+        start = max(1, page - 2)
+        end = min(total_pages, page + 2)
+        page_links = [(p, _pagination_url(request, p, pagecount)) for p in range(start, end + 1)]
+        if start > 1:
+            page_links = [(1, _pagination_url(request, 1, pagecount))] + [(-1, "")] + page_links
+        if end < total_pages:
+            page_links = page_links + [(-1, "")] + [(total_pages, _pagination_url(request, total_pages, pagecount))]
+    return env.get_template("pagination.html").render(
+        current_page=page,
+        pagecount=pagecount,
+        has_prev=has_prev,
+        has_next=has_next,
+        prev_url=prev_url,
+        next_url=next_url,
+        total_pages=total_pages,
+        page_links=page_links,
+    )
+
+
+def git_log(project: str, h: str | None, request: Request, page: int, pagecount: int) -> HTMLResponse:
     """Commit log page. Port of git_log."""
-    commits = get_commit_history(project, ref=h, max_count=100)
+    skip = (page - 1) * pagecount
+    commits = get_commit_history(project, ref=h, max_count=pagecount + 1, skip=skip)
     if not commits:
         raise HTTPException(status_code=404, detail="No commits found")
-    
+    has_next = len(commits) > pagecount
+    if has_next:
+        commits = commits[:pagecount]
     rows = []
     for commit in commits:
         rows.append(_format_commit_table_row(project, commit, short=False))
-    
     ref_display = h[:7] if h else "HEAD"
     title = f"Log - {esc_html(project)} @ {esc_html(ref_display)}"
-
     pre = PREAMBLE.render(title=title, theme="dark", site_name=config.SITE_NAME)
     table = env.get_template("table.html").render(
         cols=["Commit", "Subject", "Author", "Date", "Age"],
         rows=rows
     )
+    pagination_html = _render_pagination(request, page, pagecount, page > 1, has_next)
     return HTMLResponse(
-        f"{pre}<h1>{title}</h1>{table}{POSTAMBLE}"
+        f"{pre}<h1>{title}</h1>{table}{pagination_html}{POSTAMBLE}"
     )
 
 
-def git_shortlog(project: str, h: str | None) -> HTMLResponse:
+def git_shortlog(project: str, h: str | None, request: Request, page: int, pagecount: int) -> HTMLResponse:
     """Shortlog page. Port of git_shortlog."""
-    commits = get_commit_history(project, ref=h, max_count=100)
+    skip = (page - 1) * pagecount
+    commits = get_commit_history(project, ref=h, max_count=pagecount + 1, skip=skip)
     if not commits:
         raise HTTPException(status_code=404, detail="No commits found")
-    
+    has_next = len(commits) > pagecount
+    if has_next:
+        commits = commits[:pagecount]
     rows = []
     for commit in commits:
         rows.append(_format_commit_table_row(project, commit, short=True))
-    
     ref_display = h[:7] if h else "HEAD"
     title = f"Shortlog - {esc_html(project)} @ {esc_html(ref_display)}"
-    
     pre = PREAMBLE.render(title=title, theme="dark", site_name=config.SITE_NAME)
     table = env.get_template("table.html").render(
         cols=["Commit", "Subject", "Author", "Age"],
         rows=rows
     )
+    pagination_html = _render_pagination(request, page, pagecount, page > 1, has_next)
     return HTMLResponse(
-        f"{pre}<h1>{title}</h1>{table}{POSTAMBLE}"
+        f"{pre}<h1>{title}</h1>{table}{pagination_html}{POSTAMBLE}"
     )
 
 
-def git_history(project: str, h: str | None, f: str | None) -> HTMLResponse:
+def git_history(project: str, h: str | None, f: str | None, request: Request, page: int, pagecount: int) -> HTMLResponse:
     """History page for a file or directory. Port of git_history."""
     if not f:
         raise HTTPException(status_code=400, detail="File path (f) required for history")
     if not is_valid_pathname(f):
         raise HTTPException(status_code=400, detail="Invalid path")
-    
-    commits = get_commit_history(project, ref=h, path=f, max_count=100)
+    skip = (page - 1) * pagecount
+    commits = get_commit_history(project, ref=h, path=f, max_count=pagecount + 1, skip=skip)
     if not commits:
         raise HTTPException(status_code=404, detail="No history found for this path")
-    
+    has_next = len(commits) > pagecount
+    if has_next:
+        commits = commits[:pagecount]
     rows = []
     for commit in commits:
         rows.append(_format_commit_table_row(project, commit, short=False))
-
-    ref_display = h[:7] if h else "HEAD"    
-    title=f"History - {esc_html(f)} - {esc_html(project)} @ {esc_html(ref_display)}"
+    ref_display = h[:7] if h else "HEAD"
+    title = f"History - {esc_html(f)} - {esc_html(project)} @ {esc_html(ref_display)}"
     pre = PREAMBLE.render(title=title, theme="dark", site_name=config.SITE_NAME)
     table = env.get_template("table.html").render(
         cols=["Commit", "Subject", "Author", "Date", "Age"],
         rows=rows
     )
+    pagination_html = _render_pagination(request, page, pagecount, page > 1, has_next)
     return HTMLResponse(
-        f"{pre}<h1>{title}</h1><p>Project: <a href='/{project}'>{esc_html(project)}</a></p>{table}{POSTAMBLE}"
+        f"{pre}<h1>{title}</h1><p>Project: <a href='/{project}'>{esc_html(project)}</a></p>{table}{pagination_html}{POSTAMBLE}"
     )
 
 
-def git_tags(project: str) -> HTMLResponse:
+def git_tags(project: str, request: Request, page: int, pagecount: int) -> HTMLResponse:
     """Tags list page. Port of git_tags."""
     tags_list = git_get_tags_list(project)
     if not tags_list:
         raise HTTPException(status_code=404, detail="No tags found")
+    total = len(tags_list)
+    total_pages = (total + pagecount - 1) // pagecount if pagecount else 1
+    if page > total_pages and total > 0:
+        raise HTTPException(status_code=404, detail="Page does not exist")
+    skip = (page - 1) * pagecount
+    tags_page = tags_list[skip : skip + pagecount]
     try:
         from pygitweb.git_helpers import open_repo
         repo = open_repo(project)
     except Exception:
         raise HTTPException(status_code=404, detail="Repository not found")
     rows = []
-    for name, _ref, oid in tags_list:
+    for name, _ref, oid in tags_page:
         tag_link = f"/{project}?a=tag&h={quote(oid, safe='')}"
         target_oid = oid
         target_type = "commit"
@@ -771,8 +857,11 @@ def git_tags(project: str) -> HTMLResponse:
         cols=["Tag", "Object"],
         rows=rows,
     )
+    pagination_html = _render_pagination(
+        request, page, pagecount, page > 1, page < total_pages, total_pages=total_pages
+    )
     return HTMLResponse(
-        f"{pre}<h1>{title}</h1><p>Project: <a href='/{project}'>{esc_html(project)}</a></p>{table}{POSTAMBLE}"
+        f"{pre}<h1>{title}</h1><p>Project: <a href='/{project}'>{esc_html(project)}</a></p>{table}{pagination_html}{POSTAMBLE}"
     )
 
 
@@ -822,7 +911,7 @@ def git_tag(project: str, h: str | None) -> HTMLResponse:
     ]
     if message:
         table_rows.append(["Message", f"<pre class='tag-message'>{esc_html(message)}</pre>"])
-    table = env.get_template("table.html").render(cols=[], rows=table_rows)
+    table = env.get_template("table.html").render(cols=["Field", "Value"], rows=table_rows)
     title = f"Tag {esc_html(tag_name)} - {esc_html(project)}"
     pre = PREAMBLE.render(title=title, theme="dark", site_name=config.SITE_NAME)
     return HTMLResponse(
diff --git a/pygitweb/templates/pagination.html b/pygitweb/templates/pagination.html
new file mode 100644
index 0000000..9bda21b
--- /dev/null
+++ b/pygitweb/templates/pagination.html
@@ -0,0 +1,44 @@
+{# Tabler-style pagination. Expects: current_page, pagecount, has_prev, has_next, prev_url, next_url; optional: total_pages, page_links (list of (page_num, url) for numbered links). #}
+<nav aria-label="Pagination" class="d-flex justify-content-between align-items-center mt-3">
+  <div class="text-secondary small">
+    Page {{ current_page }}{% if total_pages is defined and total_pages is not none %} of {{ total_pages }}{% endif %}
+    <span class="ms-2">({{ pagecount }} per page)</span>
+  </div>
+  <ul class="pagination mb-0">
+    <li class="page-item{% if not has_prev %} disabled{% endif %}">
+      {% if has_prev %}
+      <a class="page-link" href="{{ prev_url }}" aria-label="Previous">
+        <span aria-hidden="true">&laquo;</span>
+      </a>
+      {% else %}
+      <span class="page-link" aria-label="Previous" aria-disabled="true">
+        <span aria-hidden="true">&laquo;</span>
+      </span>
+      {% endif %}
+    </li>
+    {% if page_links is defined and page_links %}
+      {% for p, u in page_links %}
+    <li class="page-item{% if p == current_page %} active{% endif %}{% if not u %} disabled{% endif %}">
+      {% if u %}
+      <a class="page-link" href="{{ u }}">{{ p }}</a>
+      {% else %}
+      <span class="page-link">&hellip;</span>
+      {% endif %}
+    </li>
+      {% endfor %}
+    {% else %}
+    <li class="page-item active"><span class="page-link">{{ current_page }}</span></li>
+    {% endif %}
+    <li class="page-item{% if not has_next %} disabled{% endif %}">
+      {% if has_next %}
+      <a class="page-link" href="{{ next_url }}" aria-label="Next">
+        <span aria-hidden="true">&raquo;</span>
+      </a>
+      {% else %}
+      <span class="page-link" aria-label="Next" aria-disabled="true">
+        <span aria-hidden="true">&raquo;</span>
+      </span>
+      {% endif %}
+    </li>
+  </ul>
+</nav>
