diff --git a/app/[filename]/ClientFallbackPage.tsx b/app/[filename]/ClientFallbackPage.tsx
index 7e5d86188..dab17e89a 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 2d115c448..243739ae9 100644
--- a/app/[filename]/page.tsx
+++ b/app/[filename]/page.tsx
@@ -70,17 +70,56 @@ 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);
+
+ // 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: {
+ ...basicProps.data,
+ rule: {
+ ...basicProps.data.rule,
+ related: [], // Clear broken related rules
+ },
+ },
+ query: basicProps.query,
+ variables: basicProps.variables,
+ brokenReferences: {
+ detected: true,
+ paths: brokenPaths,
+ } as BrokenReferences,
+ };
+ }
} catch (error) {
console.error(`[getRuleData] failed for filename="${filename}":`, error);
return null;
@@ -272,6 +311,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..cb347e5e2 100644
--- a/app/api/tina/rule/route.ts
+++ b/app/api/tina/rule/route.ts
@@ -10,16 +10,58 @@ 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);
+
+ // 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");
+ }
+
+ console.warn(`Broken related rule references detected: ${brokenPaths.join(", ")}`);
+
+ // Return basic result with metadata about the broken references
+ return NextResponse.json(
+ {
+ ...basicResult,
+ data: {
+ ...basicResult.data,
+ rule: {
+ ...basicResult.data.rule,
+ related: [], // Clear broken related rules
+ },
+ },
+ _brokenReferences: {
+ detected: true,
+ paths: brokenPaths,
+ message: `This rule has broken related rule references. 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..4d5946927
--- /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 = `/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
+ }
+ }
+}