diff --git a/routers/common/pagetmpl.go b/routers/common/pagetmpl.go
new file mode 100644
index 0000000000..52c9fceba3
--- /dev/null
+++ b/routers/common/pagetmpl.go
@@ -0,0 +1,75 @@
+// Copyright 2025 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package common
+
+import (
+	goctx "context"
+	"errors"
+
+	activities_model "code.gitea.io/gitea/models/activities"
+	"code.gitea.io/gitea/models/db"
+	issues_model "code.gitea.io/gitea/models/issues"
+	"code.gitea.io/gitea/modules/log"
+	"code.gitea.io/gitea/services/context"
+)
+
+// StopwatchTmplInfo is a view on a stopwatch specifically for template rendering
+type StopwatchTmplInfo struct {
+	IssueLink  string
+	RepoSlug   string
+	IssueIndex int64
+	Seconds    int64
+}
+
+func getActiveStopwatch(goCtx goctx.Context) *StopwatchTmplInfo {
+	ctx := context.GetWebContext(goCtx)
+	if ctx.Doer == nil {
+		return nil
+	}
+
+	_, sw, issue, err := issues_model.HasUserStopwatch(ctx, ctx.Doer.ID)
+	if err != nil {
+		if !errors.Is(err, goctx.Canceled) {
+			log.Error("Unable to HasUserStopwatch for user:%-v: %v", ctx.Doer, err)
+		}
+		return nil
+	}
+
+	if sw == nil || sw.ID == 0 {
+		return nil
+	}
+
+	return &StopwatchTmplInfo{
+		issue.Link(),
+		issue.Repo.FullName(),
+		issue.Index,
+		sw.Seconds() + 1, // ensure time is never zero in ui
+	}
+}
+
+func notificationUnreadCount(goCtx goctx.Context) int64 {
+	ctx := context.GetWebContext(goCtx)
+	if ctx.Doer == nil {
+		return 0
+	}
+	count, err := db.Count[activities_model.Notification](ctx, activities_model.FindNotificationOptions{
+		UserID: ctx.Doer.ID,
+		Status: []activities_model.NotificationStatus{activities_model.NotificationStatusUnread},
+	})
+	if err != nil {
+		if !errors.Is(err, goctx.Canceled) {
+			log.Error("Unable to find notification for user:%-v: %v", ctx.Doer, err)
+		}
+		return 0
+	}
+	return count
+}
+
+func PageTmplFunctions(ctx *context.Context) {
+	if ctx.IsSigned {
+		// defer the function call to the last moment when the tmpl renders
+		ctx.Data["NotificationUnreadCount"] = notificationUnreadCount
+		ctx.Data["GetActiveStopwatch"] = getActiveStopwatch
+	}
+}
diff --git a/routers/web/repo/issue_stopwatch.go b/routers/web/repo/issue_stopwatch.go
index 73e279e0a6..5a8d203771 100644
--- a/routers/web/repo/issue_stopwatch.go
+++ b/routers/web/repo/issue_stopwatch.go
@@ -4,8 +4,6 @@
 package repo
 
 import (
-	"strings"
-
 	"code.gitea.io/gitea/models/db"
 	issues_model "code.gitea.io/gitea/models/issues"
 	"code.gitea.io/gitea/modules/eventsource"
@@ -72,39 +70,3 @@ func CancelStopwatch(c *context.Context) {
 
 	c.JSONRedirect("")
 }
-
-// GetActiveStopwatch is the middleware that sets .ActiveStopwatch on context
-func GetActiveStopwatch(ctx *context.Context) {
-	if strings.HasPrefix(ctx.Req.URL.Path, "/api") {
-		return
-	}
-
-	if !ctx.IsSigned {
-		return
-	}
-
-	_, sw, issue, err := issues_model.HasUserStopwatch(ctx, ctx.Doer.ID)
-	if err != nil {
-		ctx.ServerError("HasUserStopwatch", err)
-		return
-	}
-
-	if sw == nil || sw.ID == 0 {
-		return
-	}
-
-	ctx.Data["ActiveStopwatch"] = StopwatchTmplInfo{
-		issue.Link(),
-		issue.Repo.FullName(),
-		issue.Index,
-		sw.Seconds() + 1, // ensure time is never zero in ui
-	}
-}
-
-// StopwatchTmplInfo is a view on a stopwatch specifically for template rendering
-type StopwatchTmplInfo struct {
-	IssueLink  string
-	RepoSlug   string
-	IssueIndex int64
-	Seconds    int64
-}
diff --git a/routers/web/user/notification.go b/routers/web/user/notification.go
index 1c91ff6364..f0c4390852 100644
--- a/routers/web/user/notification.go
+++ b/routers/web/user/notification.go
@@ -4,7 +4,6 @@
 package user
 
 import (
-	goctx "context"
 	"errors"
 	"fmt"
 	"net/http"
@@ -35,32 +34,6 @@ const (
 	tplNotificationSubscriptions templates.TplName = "user/notification/notification_subscriptions"
 )
 
-// GetNotificationCount is the middleware that sets the notification count in the context
-func GetNotificationCount(ctx *context.Context) {
-	if strings.HasPrefix(ctx.Req.URL.Path, "/api") {
-		return
-	}
-
-	if !ctx.IsSigned {
-		return
-	}
-
-	ctx.Data["NotificationUnreadCount"] = func() int64 {
-		count, err := db.Count[activities_model.Notification](ctx, activities_model.FindNotificationOptions{
-			UserID: ctx.Doer.ID,
-			Status: []activities_model.NotificationStatus{activities_model.NotificationStatusUnread},
-		})
-		if err != nil {
-			if err != goctx.Canceled {
-				log.Error("Unable to GetNotificationCount for user:%-v: %v", ctx.Doer, err)
-			}
-			return -1
-		}
-
-		return count
-	}
-}
-
 // Notifications is the notifications page
 func Notifications(ctx *context.Context) {
 	getNotifications(ctx)
diff --git a/routers/web/web.go b/routers/web/web.go
index 463f486250..4d635f04f0 100644
--- a/routers/web/web.go
+++ b/routers/web/web.go
@@ -280,10 +280,8 @@ func Routes() *web.Router {
 		routes.Get("/api/swagger", append(mid, misc.Swagger)...) // Render V1 by default
 	}
 
-	// TODO: These really seem like things that could be folded into Contexter or as helper functions
-	mid = append(mid, user.GetNotificationCount)
-	mid = append(mid, repo.GetActiveStopwatch)
 	mid = append(mid, goGet)
+	mid = append(mid, common.PageTmplFunctions)
 
 	others := web.NewRouter()
 	others.Use(mid...)
diff --git a/templates/base/head_navbar.tmpl b/templates/base/head_navbar.tmpl
index eab6c00840..35e14d38d3 100644
--- a/templates/base/head_navbar.tmpl
+++ b/templates/base/head_navbar.tmpl
@@ -1,8 +1,11 @@
 {{$notificationUnreadCount := 0}}
 {{if and .IsSigned .NotificationUnreadCount}}
-	{{$notificationUnreadCount = call .NotificationUnreadCount}}
+	{{$notificationUnreadCount = call .NotificationUnreadCount ctx}}
+{{end}}
+{{$activeStopwatch := NIL}}
+{{if and .IsSigned EnableTimetracking .GetActiveStopwatch}}
+	{{$activeStopwatch = call .GetActiveStopwatch ctx}}
 {{end}}
-
 <nav id="navbar" aria-label="{{ctx.Locale.Tr "aria.navbar"}}">
 	<div class="navbar-left">
 		<!-- the logo -->
@@ -12,8 +15,8 @@
 
 		<!-- mobile right menu, it must be here because in mobile view, each item is a flex column, the first item is a full row column -->
 		<div class="ui secondary menu navbar-mobile-right only-mobile">
-			{{if and .IsSigned EnableTimetracking .ActiveStopwatch}}
-			<a id="mobile-stopwatch-icon" class="active-stopwatch item" href="{{.ActiveStopwatch.IssueLink}}" title="{{ctx.Locale.Tr "active_stopwatch"}}" data-seconds="{{.ActiveStopwatch.Seconds}}">
+			{{if $activeStopwatch}}
+			<a id="mobile-stopwatch-icon" class="active-stopwatch item" href="{{$activeStopwatch.IssueLink}}" title="{{ctx.Locale.Tr "active_stopwatch"}}" data-seconds="{{$activeStopwatch.Seconds}}">
 				<div class="tw-relative">
 					{{svg "octicon-stopwatch"}}
 					<span class="header-stopwatch-dot"></span>
@@ -82,8 +85,8 @@
 				</div><!-- end content avatar menu -->
 			</div><!-- end dropdown avatar menu -->
 		{{else if .IsSigned}}
-			{{if and EnableTimetracking .ActiveStopwatch}}
-			<a class="item not-mobile active-stopwatch" href="{{.ActiveStopwatch.IssueLink}}" title="{{ctx.Locale.Tr "active_stopwatch"}}" data-seconds="{{.ActiveStopwatch.Seconds}}">
+			{{if $activeStopwatch}}
+			<a class="item not-mobile active-stopwatch" href="{{$activeStopwatch.IssueLink}}" title="{{ctx.Locale.Tr "active_stopwatch"}}" data-seconds="{{$activeStopwatch.Seconds}}">
 				<div class="tw-relative">
 					{{svg "octicon-stopwatch"}}
 					<span class="header-stopwatch-dot"></span>
@@ -186,15 +189,15 @@
 		{{end}}
 	</div><!-- end full right menu -->
 
-	{{if and .IsSigned EnableTimetracking .ActiveStopwatch}}
+	{{if $activeStopwatch}}
 		<div class="active-stopwatch-popup tippy-target">
 			<div class="tw-flex tw-items-center tw-gap-2 tw-p-3">
-				<a class="stopwatch-link tw-flex tw-items-center tw-gap-2 muted" href="{{.ActiveStopwatch.IssueLink}}">
+				<a class="stopwatch-link tw-flex tw-items-center tw-gap-2 muted" href="{{$activeStopwatch.IssueLink}}">
 					{{svg "octicon-issue-opened" 16}}
-					<span class="stopwatch-issue">{{.ActiveStopwatch.RepoSlug}}#{{.ActiveStopwatch.IssueIndex}}</span>
+					<span class="stopwatch-issue">{{$activeStopwatch.RepoSlug}}#{{$activeStopwatch.IssueIndex}}</span>
 				</a>
 				<div class="tw-flex tw-gap-1">
-					<form class="stopwatch-commit form-fetch-action" method="post" action="{{.ActiveStopwatch.IssueLink}}/times/stopwatch/toggle">
+					<form class="stopwatch-commit form-fetch-action" method="post" action="{{$activeStopwatch.IssueLink}}/times/stopwatch/toggle">
 						{{.CsrfTokenHtml}}
 						<button
 							type="submit"
@@ -202,7 +205,7 @@
 							data-tooltip-content="{{ctx.Locale.Tr "repo.issues.stop_tracking"}}"
 						>{{svg "octicon-square-fill"}}</button>
 					</form>
-					<form class="stopwatch-cancel form-fetch-action" method="post" action="{{.ActiveStopwatch.IssueLink}}/times/stopwatch/cancel">
+					<form class="stopwatch-cancel form-fetch-action" method="post" action="{{$activeStopwatch.IssueLink}}/times/stopwatch/cancel">
 						{{.CsrfTokenHtml}}
 						<button
 							type="submit"
diff --git a/templates/user/notification/notification_div.tmpl b/templates/user/notification/notification_div.tmpl
index 0d2371a358..9af2cd53b3 100644
--- a/templates/user/notification/notification_div.tmpl
+++ b/templates/user/notification/notification_div.tmpl
@@ -1,6 +1,6 @@
 <div role="main" aria-label="{{.Title}}" class="page-content user notification" id="notification_div" data-sequence-number="{{.SequenceNumber}}">
 	<div class="ui container">
-		{{$notificationUnreadCount := call .NotificationUnreadCount}}
+		{{$notificationUnreadCount := call .NotificationUnreadCount ctx}}
 		<div class="tw-flex tw-items-center tw-justify-between tw-mb-[--page-spacing]">
 			<div class="small-menu-items ui compact tiny menu">
 				<a class="{{if eq .Status 1}}active {{end}}item" href="{{AppSubUrl}}/notifications?q=unread">