From 43ca24e057e637c1c4174d0509dfb60dd20e49d4 Mon Sep 17 00:00:00 2001 From: PothieuG Date: Mon, 11 May 2026 14:19:16 +0200 Subject: [PATCH 1/3] Creating a ISR indicator comparing last updated data using tina routing with current date --- app/[filename]/ServerRulePage.tsx | 6 ++- app/api/rule-freshness/route.ts | 25 +++++++++ components/RuleFreshnessIndicator.tsx | 78 +++++++++++++++++++++++++++ 3 files changed, 108 insertions(+), 1 deletion(-) create mode 100644 app/api/rule-freshness/route.ts create mode 100644 components/RuleFreshnessIndicator.tsx 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..bd318cbb1 --- /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 ( + + + + + ); +} From af723a5adc0538a671124260f054fd4796b0bac2 Mon Sep 17 00:00:00 2001 From: PothieuG Date: Mon, 11 May 2026 14:42:59 +0200 Subject: [PATCH 2/3] Fixing build, by adding a missing type --- components/tooltip/tooltip.d.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 components/tooltip/tooltip.d.ts 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; From e01556e22e0f78fd3393663d6183bd41196b7a06 Mon Sep 17 00:00:00 2001 From: PothieuG Date: Mon, 11 May 2026 15:52:07 +0200 Subject: [PATCH 3/3] Fixing tooltipe visibility --- components/RuleFreshnessIndicator.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/RuleFreshnessIndicator.tsx b/components/RuleFreshnessIndicator.tsx index bd318cbb1..d5d02faeb 100644 --- a/components/RuleFreshnessIndicator.tsx +++ b/components/RuleFreshnessIndicator.tsx @@ -68,7 +68,7 @@ export default function RuleFreshnessIndicator({ relativePath, staticLastUpdated const config = STATUS_CONFIG[status]; return ( - +