diff --git a/app/[filename]/ServerRulePage.tsx b/app/[filename]/ServerRulePage.tsx
index 32c51709f..d8493bb8c 100644
--- a/app/[filename]/ServerRulePage.tsx
+++ b/app/[filename]/ServerRulePage.tsx
@@ -14,6 +14,7 @@ import { useIsAdminPage } from "@/components/hooks/useIsAdminPage";
import GitHubMetadata from "@/components/last-updated-by";
import RelatedRulesCard from "@/components/RelatedRulesCard";
import RuleActionButtons from "@/components/RuleActionButtons";
+import RuleFreshnessIndicator from "@/components/RuleFreshnessIndicator";
import { SocialVideoEmbed } from "@/components/shared/SocialVideoEmbed";
import { getMarkdownComponentMapping } from "@/components/tina-markdown/markdown-component-mapping";
import { Card } from "@/components/ui/card";
@@ -87,7 +88,10 @@ export default function ServerRulePage({ serverRulePageProps, tinaProps }: Serve
diff --git a/app/api/rule-freshness/route.ts b/app/api/rule-freshness/route.ts
new file mode 100644
index 000000000..8b7af72b3
--- /dev/null
+++ b/app/api/rule-freshness/route.ts
@@ -0,0 +1,25 @@
+import { NextResponse } from "next/server";
+import client from "@/tina/__generated__/client";
+
+// Always fetch fresh data from TinaCMS — never use cached version
+export const dynamic = "force-dynamic";
+
+// Only allow paths like: my-rule/rule.mdx or my-rule/rule.md
+const VALID_RELATIVE_PATH = /^[\w-]+\/rule\.(mdx?|md)$/;
+
+export async function GET(req: Request) {
+ const { searchParams } = new URL(req.url);
+ const relativePath = searchParams.get("relativePath");
+
+ if (!relativePath || !VALID_RELATIVE_PATH.test(relativePath)) {
+ return NextResponse.json({ error: "Invalid or missing relativePath" }, { status: 400 });
+ }
+
+ try {
+ const res = await client.queries.ruleDataBasic({ relativePath });
+ const lastUpdated = res?.data?.rule?.lastUpdated ?? null;
+ return NextResponse.json({ lastUpdated });
+ } catch {
+ return NextResponse.json({ error: "Rule not found" }, { status: 404 });
+ }
+}
diff --git a/components/RuleFreshnessIndicator.tsx b/components/RuleFreshnessIndicator.tsx
new file mode 100644
index 000000000..d5d02faeb
--- /dev/null
+++ b/components/RuleFreshnessIndicator.tsx
@@ -0,0 +1,78 @@
+"use client";
+
+import { useEffect, useState } from "react";
+import Tooltip from "@/components/tooltip/tooltip";
+
+type FreshnessStatus = "loading" | "fresh" | "stale" | "error";
+
+interface RuleFreshnessIndicatorProps {
+ relativePath: string;
+ staticLastUpdated: string | null | undefined;
+}
+
+const STATUS_CONFIG: Record, { color: string; label: string; tooltip: string }> = {
+ loading: {
+ color: "bg-gray-400",
+ label: "Checking...",
+ tooltip: "Checking whether this page is up to date",
+ },
+ fresh: {
+ color: "bg-green-500",
+ label: "Up to date",
+ tooltip: "This page reflects the latest published content",
+ },
+ stale: {
+ color: "bg-orange-500",
+ label: "May be outdated",
+ tooltip: "Newer content exists — this page is scheduled to refresh shortly",
+ },
+};
+
+export default function RuleFreshnessIndicator({ relativePath, staticLastUpdated }: RuleFreshnessIndicatorProps) {
+ const [status, setStatus] = useState("loading");
+
+ useEffect(() => {
+ let cancelled = false;
+
+ async function check() {
+ try {
+ const res = await fetch(`/api/rule-freshness?relativePath=${encodeURIComponent(relativePath)}`, { cache: "no-store" });
+ if (!res.ok || cancelled) {
+ setStatus("error");
+ return;
+ }
+ const json = await res.json();
+ const currentLastUpdated: string | null = json.lastUpdated ?? null;
+
+ if (!currentLastUpdated || !staticLastUpdated) {
+ // Can't compare — treat as fresh to avoid false alarms
+ setStatus("fresh");
+ return;
+ }
+
+ const isStale = new Date(currentLastUpdated) > new Date(staticLastUpdated);
+ setStatus(isStale ? "stale" : "fresh");
+ } catch {
+ if (!cancelled) setStatus("error");
+ }
+ }
+
+ check();
+ return () => {
+ cancelled = true;
+ };
+ }, [relativePath, staticLastUpdated]);
+
+ if (status === "error") return null;
+
+ const config = STATUS_CONFIG[status];
+
+ return (
+
+
+
+ {config.label}
+
+
+ );
+}
diff --git a/components/tooltip/tooltip.d.ts b/components/tooltip/tooltip.d.ts
new file mode 100644
index 000000000..e2e0e800d
--- /dev/null
+++ b/components/tooltip/tooltip.d.ts
@@ -0,0 +1,13 @@
+import type { ReactNode } from "react";
+
+interface TooltipProps {
+ children: ReactNode;
+ text: string;
+ showDelay?: number;
+ hideDelay?: number;
+ className?: string;
+ opaque?: boolean;
+}
+
+declare const Tooltip: (props: TooltipProps) => JSX.Element;
+export default Tooltip;