From f460e875d3e521ffd42a376c119a6616ff708a57 Mon Sep 17 00:00:00 2001 From: PothieuG Date: Tue, 6 Jan 2026 19:06:08 +0100 Subject: [PATCH 1/3] Fix 404 on broken related rule references by fetching basic data first --- app/[filename]/ClientFallbackPage.tsx | 9 +++++ app/[filename]/ServerRulePage.tsx | 9 ++++- app/[filename]/page.tsx | 46 ++++++++++++++++++--- app/api/tina/rule/route.ts | 51 +++++++++++++++++++---- components/BrokenReferenceBanner.tsx | 58 +++++++++++++++++++++++++++ tina/queries/queries.gql | 49 ++++++++++++++++++++++ 6 files changed, 207 insertions(+), 15 deletions(-) create mode 100644 components/BrokenReferenceBanner.tsx diff --git a/app/[filename]/ClientFallbackPage.tsx b/app/[filename]/ClientFallbackPage.tsx index cacfbab5a..0de54cacb 100644 --- a/app/[filename]/ClientFallbackPage.tsx +++ b/app/[filename]/ClientFallbackPage.tsx @@ -151,6 +151,13 @@ export default function ClientFallbackPage({ filename, searchParams }: ClientFal const sanitizedBasePath = getSanitizedBasePath(); + const brokenReferences = ruleResult._brokenReferences + ? { + detected: ruleResult._brokenReferences.detected, + paths: ruleResult._brokenReferences.paths, + } + : null; + setData({ type: "rule", rule: ruleResult.data.rule, @@ -161,6 +168,7 @@ export default function ClientFallbackPage({ filename, searchParams }: ClientFal query: ruleResult.query, variables: ruleResult.variables, }, + brokenReferences, }); setLoading(false); return; @@ -243,6 +251,7 @@ export default function ClientFallbackPage({ filename, searchParams }: ClientFal rule: data.rule, ruleCategoriesMapping: data.ruleCategoriesMapping, sanitizedBasePath: data.sanitizedBasePath, + brokenReferences: data.brokenReferences, }} /> diff --git a/app/[filename]/ServerRulePage.tsx b/app/[filename]/ServerRulePage.tsx index f8525353d..601bc1dda 100644 --- a/app/[filename]/ServerRulePage.tsx +++ b/app/[filename]/ServerRulePage.tsx @@ -3,6 +3,7 @@ import { tinaField } from "tinacms/dist/react"; import { TinaMarkdown } from "tinacms/dist/rich-text"; import AuthorsCard from "@/components/AuthorsCard"; import Breadcrumbs from "@/components/Breadcrumbs"; +import BrokenReferenceBanner from "@/components/BrokenReferenceBanner"; import CategoriesCard from "@/components/CategoriesCard"; import Discussion from "@/components/Discussion"; import HelpCard from "@/components/HelpCard"; @@ -12,11 +13,13 @@ import RuleActionButtons from "@/components/RuleActionButtons"; import { YouTubeShorts } from "@/components/shared/Youtube"; import { getMarkdownComponentMapping } from "@/components/tina-markdown/markdown-component-mapping"; import { Card } from "@/components/ui/card"; +import type { BrokenReferences } from "@/app/[filename]/page"; export interface ServerRulePageProps { rule: any; ruleCategoriesMapping: { title: string; uri: string }[]; sanitizedBasePath: string; + brokenReferences?: BrokenReferences | null; } export type ServerRulePagePropsWithTinaProps = { @@ -28,7 +31,7 @@ export default function ServerRulePage({ serverRulePageProps, tinaProps }: Serve const { data } = tinaProps; const rule = data?.rule; - const { ruleCategoriesMapping, sanitizedBasePath } = serverRulePageProps; + const { ruleCategoriesMapping, sanitizedBasePath, brokenReferences } = serverRulePageProps; const primaryCategory = ruleCategoriesMapping?.[0]; const breadcrumbCategories = primaryCategory ? [{ title: primaryCategory.title, link: `/${primaryCategory.uri}` }] : undefined; @@ -37,6 +40,10 @@ export default function ServerRulePage({ serverRulePageProps, tinaProps }: Serve <> + {brokenReferences?.detected && ( + + )} +
{rule?.isArchived && rule?.archivedreason && ( diff --git a/app/[filename]/page.tsx b/app/[filename]/page.tsx index 587e7d5c6..14d48c320 100644 --- a/app/[filename]/page.tsx +++ b/app/[filename]/page.tsx @@ -70,17 +70,50 @@ const getCategoryData = async (filename: string) => { } }; +export interface BrokenReferences { + detected: boolean; + paths: string[]; +} + const getRuleData = async (filename: string) => { try { - const tinaProps = await client.queries.ruleData({ + const basicProps = await client.queries.ruleDataBasic({ relativePath: filename + "/rule.mdx", }); - return { - data: tinaProps.data, - query: tinaProps.query, - variables: tinaProps.variables, - }; + try { + const fullProps = await client.queries.ruleData({ + relativePath: filename + "/rule.mdx", + }); + + return { + data: fullProps.data, + query: fullProps.query, + variables: fullProps.variables, + brokenReferences: null as BrokenReferences | null, + }; + } catch (relatedError) { + const errorMessage = relatedError instanceof Error ? relatedError.message : String(relatedError); + const brokenPathMatch = errorMessage.match(/Unable to find record ([^\n]+)/); + const brokenPath = brokenPathMatch ? brokenPathMatch[1].trim() : "unknown path"; + + + return { + data: { + ...basicProps.data, + rule: { + ...basicProps.data.rule, + related: [], // Clear broken related rules + }, + }, + query: basicProps.query, + variables: basicProps.variables, + brokenReferences: { + detected: true, + paths: [brokenPath], + } as BrokenReferences, + }; + } } catch (error) { console.error("Error fetching rule data:", error); return null; @@ -272,6 +305,7 @@ export default async function Page({ rule: rule.data.rule, ruleCategoriesMapping: ruleCategoriesMapping, sanitizedBasePath: sanitizedBasePath, + brokenReferences: rule.brokenReferences, }} /> diff --git a/app/api/tina/rule/route.ts b/app/api/tina/rule/route.ts index 5aba355bd..f8ccaf89b 100644 --- a/app/api/tina/rule/route.ts +++ b/app/api/tina/rule/route.ts @@ -10,16 +10,51 @@ export async function GET(request: NextRequest) { if (!relativePath) { return NextResponse.json({ error: "relativePath is required" }, { status: 400 }); } + const fetchOptions = await getFetchOptions(); - // Fetch rule using the Tina client with branch support - let result; - if (fetchOptions) { - result = await client.queries.ruleData({ relativePath }, fetchOptions); - } else { - result = await client.queries.ruleData({ relativePath }); - } - return NextResponse.json(result, { status: 200 }); + // First, try the basic query (without related field resolution) + // This ensures we always get the rule data even if references are broken + const basicResult = fetchOptions + ? await client.queries.ruleDataBasic({ relativePath }, fetchOptions) + : await client.queries.ruleDataBasic({ relativePath }); + + // Now try the full query to get related rules + try { + const fullResult = fetchOptions + ? await client.queries.ruleData({ relativePath }, fetchOptions) + : await client.queries.ruleData({ relativePath }); + + // Full query succeeded - no broken references + return NextResponse.json(fullResult, { status: 200 }); + } catch (relatedError) { + // Full query failed but basic succeeded - broken references detected + const errorMessage = relatedError instanceof Error ? relatedError.message : String(relatedError); + const brokenPathMatch = errorMessage.match(/Unable to find record ([^\n]+)/); + const brokenPath = brokenPathMatch ? brokenPathMatch[1].trim() : "unknown path"; + + console.warn(`Broken related rule reference detected: ${brokenPath}`); + + // Return basic result with metadata about the broken reference + return NextResponse.json( + { + ...basicResult, + data: { + ...basicResult.data, + rule: { + ...basicResult.data.rule, + related: [], // Clear broken related rules + }, + }, + _brokenReferences: { + detected: true, + paths: [brokenPath], + message: `This rule has a broken related rule reference: ${brokenPath}. Please edit the rule to fix this.`, + }, + }, + { status: 200 } + ); + } } catch (error) { console.error("Error fetching rule from Tina:", error); return NextResponse.json({ error: "Failed to fetch rule", details: error instanceof Error ? error.message : String(error) }, { status: 500 }); diff --git a/components/BrokenReferenceBanner.tsx b/components/BrokenReferenceBanner.tsx new file mode 100644 index 000000000..4120198d3 --- /dev/null +++ b/components/BrokenReferenceBanner.tsx @@ -0,0 +1,58 @@ +"use client"; + +import Link from "next/link"; +import { useIsAdminPage } from "@/components/hooks/useIsAdminPage"; + +interface BrokenReferenceBannerProps { + brokenPaths: string[]; + ruleUri: string; +} + +export default function BrokenReferenceBanner({ brokenPaths, ruleUri }: BrokenReferenceBannerProps) { + const { isAdmin } = useIsAdminPage(); + + const adminUrl = `/rules/admin#/~/rules/${encodeURIComponent(ruleUri)}`; + + return ( +
+
+
+ +
+
+

+ Broken Related Rule Reference{brokenPaths.length > 1 ? "s" : ""} +

+

+ This rule references {brokenPaths.length > 1 ? "rules that no longer exist" : "a rule that no longer exists"}: +

+
    + {brokenPaths.map((path, idx) => ( +
  • + {path} +
  • + ))} +
+ {isAdmin ? ( +

+ Please edit the Related Rules field to remove or update the broken reference. +

+ ) : ( +

+ + Open in admin panel + {" "} + to fix this issue. +

+ )} +
+
+
+ ); +} diff --git a/tina/queries/queries.gql b/tina/queries/queries.gql index 8e9b7bf3f..cb8df244a 100644 --- a/tina/queries/queries.gql +++ b/tina/queries/queries.gql @@ -290,3 +290,52 @@ query ruleData($relativePath: String!) { } } } + +# Fallback rule query that doesn't resolve related rule references +# Used when ruleData fails due to broken/missing related rule paths +query ruleDataBasic($relativePath: String!) { + rule(relativePath: $relativePath) { + ... on Document { + _sys { + filename + basename + hasReferences + breadcrumbs + path + relativePath + extension + } + id + } + ... on Rule { + title + uri + body + authors { + title + url + } + categories { + category { + ... on CategoryCategory { + uri + title + } + } + } + # Note: related field omitted - this query is used when related rules are broken + redirects + guid + seoDescription + created + createdBy + createdByEmail + lastUpdated + lastUpdatedBy + lastUpdatedByEmail + isArchived + archivedreason + sidebarVideo + } + } +} From 67bfb6bc0bfe4af82cd05080f89c71a380958bca Mon Sep 17 00:00:00 2001 From: PothieuG Date: Tue, 6 Jan 2026 19:30:01 +0100 Subject: [PATCH 2/3] Updating regex to get a list of broken related rules links if there is more than one. --- app/[filename]/page.tsx | 12 +++++++++--- app/api/tina/rule/route.ts | 19 +++++++++++++------ 2 files changed, 22 insertions(+), 9 deletions(-) diff --git a/app/[filename]/page.tsx b/app/[filename]/page.tsx index 14d48c320..032f31335 100644 --- a/app/[filename]/page.tsx +++ b/app/[filename]/page.tsx @@ -94,9 +94,15 @@ const getRuleData = async (filename: string) => { }; } catch (relatedError) { const errorMessage = relatedError instanceof Error ? relatedError.message : String(relatedError); - const brokenPathMatch = errorMessage.match(/Unable to find record ([^\n]+)/); - const brokenPath = brokenPathMatch ? brokenPathMatch[1].trim() : "unknown path"; + // Extract all broken paths from error message (there may be multiple) + const brokenPathMatches = errorMessage.matchAll(/Unable to find record ([^\n]+)/g); + const brokenPaths = Array.from(brokenPathMatches, (match) => match[1].trim()); + + // Fallback if no paths found + if (brokenPaths.length === 0) { + brokenPaths.push("unknown path"); + } return { data: { @@ -110,7 +116,7 @@ const getRuleData = async (filename: string) => { variables: basicProps.variables, brokenReferences: { detected: true, - paths: [brokenPath], + paths: brokenPaths, } as BrokenReferences, }; } diff --git a/app/api/tina/rule/route.ts b/app/api/tina/rule/route.ts index f8ccaf89b..cb347e5e2 100644 --- a/app/api/tina/rule/route.ts +++ b/app/api/tina/rule/route.ts @@ -30,12 +30,19 @@ export async function GET(request: NextRequest) { } catch (relatedError) { // Full query failed but basic succeeded - broken references detected const errorMessage = relatedError instanceof Error ? relatedError.message : String(relatedError); - const brokenPathMatch = errorMessage.match(/Unable to find record ([^\n]+)/); - const brokenPath = brokenPathMatch ? brokenPathMatch[1].trim() : "unknown path"; - console.warn(`Broken related rule reference detected: ${brokenPath}`); + // Extract all broken paths from error message (there may be multiple) + const brokenPathMatches = errorMessage.matchAll(/Unable to find record ([^\n]+)/g); + const brokenPaths = Array.from(brokenPathMatches, (match) => match[1].trim()); - // Return basic result with metadata about the broken reference + // Fallback if no paths found + if (brokenPaths.length === 0) { + brokenPaths.push("unknown path"); + } + + console.warn(`Broken related rule references detected: ${brokenPaths.join(", ")}`); + + // Return basic result with metadata about the broken references return NextResponse.json( { ...basicResult, @@ -48,8 +55,8 @@ export async function GET(request: NextRequest) { }, _brokenReferences: { detected: true, - paths: [brokenPath], - message: `This rule has a broken related rule reference: ${brokenPath}. Please edit the rule to fix this.`, + paths: brokenPaths, + message: `This rule has broken related rule references. Please edit the rule to fix this.`, }, }, { status: 200 } From 86d29ee0e2f4d8c6621d6c0210b0d0334a19da97 Mon Sep 17 00:00:00 2001 From: PothieuG Date: Tue, 6 Jan 2026 19:30:28 +0100 Subject: [PATCH 3/3] Fixing the url to the admin panel. --- components/BrokenReferenceBanner.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/BrokenReferenceBanner.tsx b/components/BrokenReferenceBanner.tsx index 4120198d3..4d5946927 100644 --- a/components/BrokenReferenceBanner.tsx +++ b/components/BrokenReferenceBanner.tsx @@ -11,7 +11,7 @@ interface BrokenReferenceBannerProps { export default function BrokenReferenceBanner({ brokenPaths, ruleUri }: BrokenReferenceBannerProps) { const { isAdmin } = useIsAdminPage(); - const adminUrl = `/rules/admin#/~/rules/${encodeURIComponent(ruleUri)}`; + const adminUrl = `/admin#/~/rules/${encodeURIComponent(ruleUri)}`; return (