diff --git a/scripts/generate-changelog.ts b/scripts/generate-changelog.ts index 30866b8c..6bd3bd99 100644 --- a/scripts/generate-changelog.ts +++ b/scripts/generate-changelog.ts @@ -36,6 +36,16 @@ function pruneOldEntries(entries: ChangelogEntry[]): ChangelogEntry[] { return entries.filter((entry) => new Date(entry.date) >= cutoff); } +function pruneDeletedEntries(entries: ChangelogEntry[]): ChangelogEntry[] { + return entries.filter((entry) => + fs.existsSync(path.join(process.cwd(), "content/docs", entry.path)), + ); +} + +function sanitizeEntries(entries: ChangelogEntry[]): ChangelogEntry[] { + return pruneOldEntries(pruneDeletedEntries(entries)); +} + function loadExistingChangelog(): ChangelogData { try { if (fs.existsSync(OUTPUT_FILE)) { @@ -77,12 +87,31 @@ function filePathToUrl(relativePath: string): string { return `/docs/${cleanPath}`; } +function writeChangelogIfChanged(changelogData: ChangelogData): boolean { + const output = `${JSON.stringify(changelogData, null, 2)}\n`; + const current = fs.existsSync(OUTPUT_FILE) ? fs.readFileSync(OUTPUT_FILE, "utf-8") : ""; + + if (current === output) { + return false; + } + + const outputDir = path.dirname(OUTPUT_FILE); + if (!fs.existsSync(outputDir)) { + fs.mkdirSync(outputDir, { recursive: true }); + } + + fs.writeFileSync(OUTPUT_FILE, output, "utf-8"); + return true; +} + async function main() { console.log("Generating documentation changelog..."); console.log(` Since: ${SINCE_DATE.toISOString().split("T")[0]}`); const existing = loadExistingChangelog(); - const existingKeys = new Set(existing.entries.map((e) => e.key)); + const existingEntries = pruneDeletedEntries(existing.entries); + const prunedExistingCount = existing.entries.length - existingEntries.length; + const existingKeys = new Set(existingEntries.map((e) => e.key)); console.log(` Existing entries: ${existingKeys.size}`); const changes = getChangedFilesSince(SINCE_DATE); @@ -123,11 +152,33 @@ async function main() { console.log(` New entries to generate: ${newChanges.length}`); if (newChanges.length === 0) { + if (prunedExistingCount > 0) { + const wroteChangelog = writeChangelogIfChanged({ + lastUpdated: new Date().toISOString(), + entries: existingEntries, + }); + + if (wroteChangelog) { + console.log(` Pruned ${prunedExistingCount} stale existing entries`); + } + } + console.log("Changelog is up to date, no new entries"); return; } if (!hasApiKey()) { + if (prunedExistingCount > 0) { + const wroteChangelog = writeChangelogIfChanged({ + lastUpdated: new Date().toISOString(), + entries: existingEntries, + }); + + if (wroteChangelog) { + console.log(` Pruned ${prunedExistingCount} stale existing entries`); + } + } + console.log("\nNo ANTHROPIC_API_KEY found. Skipping changelog generation."); console.log(" To generate descriptions for new entries, add ANTHROPIC_API_KEY to .env.local"); console.log(` ${newChanges.length} new entries were not added.\n`); @@ -164,8 +215,8 @@ async function main() { }; }); - const mergedEntries = [...newEntries, ...existing.entries]; - const prunedEntries = pruneOldEntries(mergedEntries); + const mergedEntries = [...newEntries, ...existingEntries]; + const prunedEntries = sanitizeEntries(mergedEntries); const prunedCount = mergedEntries.length - prunedEntries.length; prunedEntries.sort((a, b) => { @@ -179,12 +230,7 @@ async function main() { entries: prunedEntries, }; - const outputDir = path.dirname(OUTPUT_FILE); - if (!fs.existsSync(outputDir)) { - fs.mkdirSync(outputDir, { recursive: true }); - } - - fs.writeFileSync(OUTPUT_FILE, JSON.stringify(changelogData, null, 2), "utf-8"); + writeChangelogIfChanged(changelogData); console.log(`Changelog updated: ${path.relative(process.cwd(), OUTPUT_FILE)}`); console.log(` Added ${newEntries.length} new entries`); diff --git a/scripts/generate-static-cache.ts b/scripts/generate-static-cache.ts index 33c932c8..4b83be74 100644 --- a/scripts/generate-static-cache.ts +++ b/scripts/generate-static-cache.ts @@ -44,7 +44,9 @@ async function main() { const { gitConfig } = await server.ssrLoadModule("./src/lib/layout.shared.tsx"); const { buildCanonicalUrl, SITE_NAME, DEFAULT_DESCRIPTION } = await server.ssrLoadModule("./src/lib/metadata"); - const { buildDocsPageStructuredData } = await server.ssrLoadModule("./src/lib/structured-data"); + const { buildDocsPageStructuredData } = await server.ssrLoadModule( + "./src/lib/structured-data", + ); // Serialize the page tree once (the expensive operation). const rawPageTree = source.getPageTree(); diff --git a/src/components/ChangelogTimeline.tsx b/src/components/ChangelogTimeline.tsx index 149c57e9..5368228b 100644 --- a/src/components/ChangelogTimeline.tsx +++ b/src/components/ChangelogTimeline.tsx @@ -1,6 +1,5 @@ import { ChangelogEntry } from "./ChangelogEntry"; import changelogData from "@/lib/changelog-entries.json"; -import { source } from "@/lib/source"; interface ChangelogDataType { lastUpdated: string; @@ -157,7 +156,6 @@ function EmptyState() { } const MONTHS_TO_SHOW = 3; -const VALID_CHANGELOG_URLS = new Set(source.getPages().map((page) => page.url)); export function ChangelogTimeline() { const data = changelogData as ChangelogDataType; @@ -166,11 +164,7 @@ export function ChangelogTimeline() { return ; } - const recentEntries = deduplicateByPath( - filterToRecentMonths(data.entries, MONTHS_TO_SHOW).filter((entry) => - VALID_CHANGELOG_URLS.has(entry.url), - ), - ); + const recentEntries = deduplicateByPath(filterToRecentMonths(data.entries, MONTHS_TO_SHOW)); if (recentEntries.length === 0) { return ; diff --git a/src/components/DocsError.test.tsx b/src/components/DocsError.test.tsx new file mode 100644 index 00000000..0794607b --- /dev/null +++ b/src/components/DocsError.test.tsx @@ -0,0 +1,15 @@ +import assert from "node:assert/strict"; +import { describe, test } from "node:test"; +import { renderToString } from "react-dom/server"; +import { DocsError } from "./DocsError"; + +describe("DocsError", () => { + test("renders an index-safe docs fallback instead of TanStack's default text", () => { + const html = renderToString( {}} />); + + assert.match(html, /Error loading docs page/); + assert.doesNotMatch(html, /Something went wrong!/); + assert.doesNotMatch(html, /Show Error/); + assert.doesNotMatch(html, /boom/); + }); +}); diff --git a/src/components/DocsError.tsx b/src/components/DocsError.tsx new file mode 100644 index 00000000..d85b8b9b --- /dev/null +++ b/src/components/DocsError.tsx @@ -0,0 +1,78 @@ +import type { ErrorComponentProps } from "@tanstack/react-router"; +import { AlertTriangle, Home, RefreshCw } from "lucide-react"; +import { useEffect } from "react"; +import { ERROR_PAGE_DESCRIPTION, ERROR_PAGE_TITLE, NOINDEX_ROBOTS_META } from "@/lib/metadata"; +import { buildDocsPath } from "@/lib/url-base"; + +type DocsErrorProps = Pick & { + error?: unknown; +}; + +function reloadPage(reset?: ErrorComponentProps["reset"]) { + reset?.(); + if (typeof window !== "undefined") { + window.location.reload(); + } +} + +function upsertMeta(name: string, content: string) { + let element = document.querySelector(`meta[name="${name}"]`); + if (!(element instanceof HTMLMetaElement)) { + element = document.createElement("meta"); + element.name = name; + document.head.append(element); + } + + element.content = content; + return element; +} + +export function DocsError({ reset }: DocsErrorProps) { + useEffect(() => { + document.title = ERROR_PAGE_TITLE; + upsertMeta("description", ERROR_PAGE_DESCRIPTION); + const robotsMeta = upsertMeta("robots", NOINDEX_ROBOTS_META); + + return () => { + if (robotsMeta.content === NOINDEX_ROBOTS_META) { + robotsMeta.remove(); + } + }; + }, []); + + return ( + + + + + + + + Error loading docs page + + An error prevented this page from loading. Try refreshing, or go back to the docs home + page. + + + + + reloadPage(reset)} + className="inline-flex h-10 items-center justify-center gap-2 rounded-lg border border-transparent bg-fd-primary px-4 text-sm font-medium text-fd-primary-foreground transition-colors hover:bg-fd-primary/80 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fd-ring" + > + + Refresh + + + + Docs home + + + + + ); +} diff --git a/src/components/GlobalScripts.tsx b/src/components/GlobalScripts.tsx index faee1487..51d70ac2 100644 --- a/src/components/GlobalScripts.tsx +++ b/src/components/GlobalScripts.tsx @@ -2,8 +2,30 @@ import { useEffect } from "react"; +const SUPERCHAT_FRAME_ID = "arona-frame"; +const SUPERCHAT_FRAME_LABEL = "Superwall support chat"; + +function labelSuperchatFrame() { + const frame = document.getElementById(SUPERCHAT_FRAME_ID); + if (!(frame instanceof HTMLIFrameElement)) return false; + + frame.title ||= SUPERCHAT_FRAME_LABEL; + if (!frame.getAttribute("aria-label")) { + frame.setAttribute("aria-label", SUPERCHAT_FRAME_LABEL); + } + + return true; +} + export function GlobalScripts() { useEffect(() => { + const observer = new MutationObserver(() => { + if (labelSuperchatFrame()) observer.disconnect(); + }); + if (!labelSuperchatFrame()) { + observer.observe(document.body, { childList: true, subtree: true }); + } + const meshSdkKey = (window as any).__ENV__?.MESH_SDK_KEY; const unifyScriptSrc = (window as any).__ENV__?.UNIFY_SCRIPT_SRC; const unifyApiKey = (window as any).__ENV__?.UNIFY_API_KEY; @@ -167,6 +189,8 @@ export function GlobalScripts() { // On error, do nothing } })(); + + return () => observer.disconnect(); }, []); return null; diff --git a/src/components/SdkLatestVersion.tsx b/src/components/SdkLatestVersion.tsx index 07bea671..5ec729c9 100644 --- a/src/components/SdkLatestVersion.tsx +++ b/src/components/SdkLatestVersion.tsx @@ -17,7 +17,7 @@ export function SdkLatestVersion({ version, repoUrl, className }: SdkLatestVersi target="_blank" rel="noopener noreferrer" className={cn( - "inline-flex w-full items-center text-base italic text-fd-muted-foreground opacity-50 no-underline transition-opacity hover:opacity-100", + "inline-flex w-full items-center text-base italic text-fd-muted-foreground no-underline transition-colors hover:text-fd-foreground", className, )} > diff --git a/src/lib/changelog-entries.json b/src/lib/changelog-entries.json index cbf56b73..46c2beb9 100644 --- a/src/lib/changelog-entries.json +++ b/src/lib/changelog-entries.json @@ -1,5 +1,5 @@ { - "lastUpdated": "2026-05-15T21:12:13.469Z", + "lastUpdated": "2026-06-23T19:27:44.886Z", "entries": [ { "key": "content/docs/agents/automations.mdx:5ad135045995068c5eb0a94e8e3fbae4d7535195", @@ -81,16 +81,6 @@ "date": "2026-05-15T21:02:42.000Z", "changeType": "modified" }, - { - "key": "content/docs/agents/hosted-machines.mdx:1824660462bf774d222a1b7af60f75165fd5c4d7", - "path": "agents/hosted-machines.mdx", - "title": "Hosted Machines", - "description": "Updated Documentation documentation", - "category": "Documentation", - "url": "/docs/agents/hosted-machines", - "date": "2026-05-15T20:46:10.000Z", - "changeType": "modified" - }, { "key": "content/docs/agents/index.mdx:1824660462bf774d222a1b7af60f75165fd5c4d7", "path": "agents/index.mdx", @@ -245,16 +235,6 @@ "date": "2026-05-15T17:51:18.000Z", "changeType": "added" }, - { - "key": "content/docs/agents/hosted-machines.mdx:f496da1103bb46c04d6acee611b0191b4389b602", - "path": "agents/hosted-machines.mdx", - "title": "Hosted Machines", - "description": "New Documentation documentation", - "category": "Documentation", - "url": "/docs/agents/hosted-machines", - "date": "2026-05-15T17:51:18.000Z", - "changeType": "added" - }, { "key": "content/docs/agents/settings.mdx:f496da1103bb46c04d6acee611b0191b4389b602", "path": "agents/settings.mdx", @@ -2233,16 +2213,6 @@ "date": "2026-03-30T21:40:39.000Z", "changeType": "modified" }, - { - "key": "content/docs/react-native/vibe-coding-guide.mdx:3ff5bbaa035edc701d12a4cbc3b1fb817cb8c607", - "path": "react-native/vibe-coding-guide.mdx", - "title": "Superwall React Native Vibe Coding Guide", - "description": "Updated React Native SDK documentation", - "category": "React Native SDK", - "url": "/docs/react-native/vibe-coding-guide", - "date": "2026-03-30T21:40:39.000Z", - "changeType": "modified" - }, { "key": "content/docs/dashboard/dashboard-campaigns/campaign-rules.mdx:3ff5bbaa035edc701d12a4cbc3b1fb817cb8c607", "path": "dashboard/dashboard-campaigns/campaign-rules.mdx", @@ -2734,17 +2704,6 @@ "date": "2026-03-30T21:40:39.000Z", "changeType": "modified" }, - { - "key": "content/docs/dashboard/guides/using-stripe-checkout-in-app.mdx:3ff5bbaa035edc701d12a4cbc3b1fb817cb8c607", - "path": "dashboard/guides/using-stripe-checkout-in-app.mdx", - "title": "Using Stripe's New Web Checkout In-App", - "description": "Updated guides for Dashboard", - "category": "Dashboard", - "subcategory": "Guides", - "url": "/docs/dashboard/guides/using-stripe-checkout-in-app", - "date": "2026-03-30T21:40:39.000Z", - "changeType": "modified" - }, { "key": "content/docs/dashboard/guides/using-superwall-for-onboarding-flows.mdx:3ff5bbaa035edc701d12a4cbc3b1fb817cb8c607", "path": "dashboard/guides/using-superwall-for-onboarding-flows.mdx", @@ -2926,16 +2885,6 @@ "date": "2026-03-30T21:40:39.000Z", "changeType": "modified" }, - { - "key": "content/docs/web-checkout/web-checkout-overview.mdx:3ff5bbaa035edc701d12a4cbc3b1fb817cb8c607", - "path": "web-checkout/web-checkout-overview.mdx", - "title": "Overview", - "description": "Updated Web Checkout documentation", - "category": "Web Checkout", - "url": "/docs/web-checkout/web-checkout-overview", - "date": "2026-03-30T21:40:39.000Z", - "changeType": "modified" - }, { "key": "content/docs/web-checkout/web-checkout-sdk-setup.mdx:3ff5bbaa035edc701d12a4cbc3b1fb817cb8c607", "path": "web-checkout/web-checkout-sdk-setup.mdx", @@ -3335,17 +3284,6 @@ "date": "2026-03-19T19:18:30.000Z", "changeType": "modified" }, - { - "key": "content/docs/dashboard/guides/using-stripe-checkout-in-app.mdx:31a7365538fa6c506a0c0b2eaf067ae67ccd708f", - "path": "dashboard/guides/using-stripe-checkout-in-app.mdx", - "title": "Using Stripe's New Web Checkout In-App", - "description": "Updated guides for Dashboard", - "category": "Dashboard", - "subcategory": "Guides", - "url": "/docs/dashboard/guides/using-stripe-checkout-in-app", - "date": "2026-03-19T19:18:30.000Z", - "changeType": "modified" - }, { "key": "content/docs/integrations/adjust.mdx:31a7365538fa6c506a0c0b2eaf067ae67ccd708f", "path": "integrations/adjust.mdx", @@ -3396,17 +3334,6 @@ "date": "2026-03-19T19:18:30.000Z", "changeType": "modified" }, - { - "key": "content/docs/dashboard/guides/using-stripe-checkout-in-app.mdx:c6dc2b47235153d0f6d276dccb1141655f638798", - "path": "dashboard/guides/using-stripe-checkout-in-app.mdx", - "title": "Using Stripe's New Web Checkout In-App", - "description": "Updated guides for Dashboard", - "category": "Dashboard", - "subcategory": "Guides", - "url": "/docs/dashboard/guides/using-stripe-checkout-in-app", - "date": "2026-03-18T17:42:38.000Z", - "changeType": "modified" - }, { "key": "content/docs/web-checkout/web-checkout-direct-stripe-checkout.mdx:c6dc2b47235153d0f6d276dccb1141655f638798", "path": "web-checkout/web-checkout-direct-stripe-checkout.mdx", @@ -4288,16 +4215,6 @@ "date": "2026-03-10T15:34:06.000Z", "changeType": "modified" }, - { - "key": "content/docs/ios/vibe-coding-guide.mdx:153a37d3d6cb55613551aad2e7e31b5f9fd2fa2a", - "path": "ios/vibe-coding-guide.mdx", - "title": "Superwall iOS Vibe Coding Guide", - "description": "Updated iOS SDK documentation", - "category": "iOS SDK", - "url": "/docs/ios/vibe-coding-guide", - "date": "2026-03-10T15:34:06.000Z", - "changeType": "modified" - }, { "key": "content/docs/android/guides/3rd-party-analytics/cohorting-in-3rd-party-tools.mdx:153a37d3d6cb55613551aad2e7e31b5f9fd2fa2a", "path": "android/guides/3rd-party-analytics/cohorting-in-3rd-party-tools.mdx", @@ -4738,16 +4655,6 @@ "date": "2026-03-10T15:34:06.000Z", "changeType": "modified" }, - { - "key": "content/docs/android/vibe-coding-guide.mdx:153a37d3d6cb55613551aad2e7e31b5f9fd2fa2a", - "path": "android/vibe-coding-guide.mdx", - "title": "Superwall Android Vibe Coding Guide", - "description": "Updated Android SDK documentation", - "category": "Android SDK", - "url": "/docs/android/vibe-coding-guide", - "date": "2026-03-10T15:34:06.000Z", - "changeType": "modified" - }, { "key": "content/docs/flutter/changelog.mdx:153a37d3d6cb55613551aad2e7e31b5f9fd2fa2a", "path": "flutter/changelog.mdx", @@ -5340,16 +5247,6 @@ "date": "2026-03-10T15:34:06.000Z", "changeType": "modified" }, - { - "key": "content/docs/flutter/vibe-coding-guide.mdx:153a37d3d6cb55613551aad2e7e31b5f9fd2fa2a", - "path": "flutter/vibe-coding-guide.mdx", - "title": "Superwall Flutter Vibe Coding Guide", - "description": "Updated Flutter SDK documentation", - "category": "Flutter SDK", - "url": "/docs/flutter/vibe-coding-guide", - "date": "2026-03-10T15:34:06.000Z", - "changeType": "modified" - }, { "key": "content/docs/expo/guides/3rd-party-analytics/cohorting-in-3rd-party-tools.mdx:153a37d3d6cb55613551aad2e7e31b5f9fd2fa2a", "path": "expo/guides/3rd-party-analytics/cohorting-in-3rd-party-tools.mdx", @@ -5746,17 +5643,6 @@ "date": "2026-03-10T15:34:06.000Z", "changeType": "modified" }, - { - "key": "content/docs/expo/sdk-reference/getPresentationResult.mdx:153a37d3d6cb55613551aad2e7e31b5f9fd2fa2a", - "path": "expo/sdk-reference/getPresentationResult.mdx", - "title": "getPresentationResult()", - "description": "Updated sdk reference for Expo SDK", - "category": "Expo SDK", - "subcategory": "SDK Reference", - "url": "/docs/expo/sdk-reference/getPresentationResult", - "date": "2026-03-10T15:34:06.000Z", - "changeType": "modified" - }, { "key": "content/docs/expo/sdk-reference/hooks/usePlacement.mdx:153a37d3d6cb55613551aad2e7e31b5f9fd2fa2a", "path": "expo/sdk-reference/hooks/usePlacement.mdx", @@ -5790,16 +5676,6 @@ "date": "2026-03-10T15:34:06.000Z", "changeType": "modified" }, - { - "key": "content/docs/expo/vibe-coding-guide.mdx:153a37d3d6cb55613551aad2e7e31b5f9fd2fa2a", - "path": "expo/vibe-coding-guide.mdx", - "title": "Superwall Expo Vibe Coding Guide", - "description": "Updated Expo SDK documentation", - "category": "Expo SDK", - "url": "/docs/expo/vibe-coding-guide", - "date": "2026-03-10T15:34:06.000Z", - "changeType": "modified" - }, { "key": "content/docs/react-native/changelog.mdx:153a37d3d6cb55613551aad2e7e31b5f9fd2fa2a", "path": "react-native/changelog.mdx", @@ -5985,26 +5861,6 @@ "date": "2026-03-10T15:34:06.000Z", "changeType": "modified" }, - { - "key": "content/docs/react-native/vibe-coding-guide.mdx:153a37d3d6cb55613551aad2e7e31b5f9fd2fa2a", - "path": "react-native/vibe-coding-guide.mdx", - "title": "Superwall React Native Vibe Coding Guide", - "description": "Updated React Native SDK documentation", - "category": "React Native SDK", - "url": "/docs/react-native/vibe-coding-guide", - "date": "2026-03-10T15:34:06.000Z", - "changeType": "modified" - }, - { - "key": "content/docs/dashboard/charts.mdx:153a37d3d6cb55613551aad2e7e31b5f9fd2fa2a", - "path": "dashboard/charts.mdx", - "title": "Charts", - "description": "Updated Dashboard documentation", - "category": "Dashboard", - "url": "/docs/dashboard/charts", - "date": "2026-03-10T15:34:06.000Z", - "changeType": "modified" - }, { "key": "content/docs/dashboard/dashboard-campaigns/campaigns-audience.mdx:153a37d3d6cb55613551aad2e7e31b5f9fd2fa2a", "path": "dashboard/dashboard-campaigns/campaigns-audience.mdx", @@ -9342,16 +9198,6 @@ "date": "2026-03-03T22:34:17.000Z", "changeType": "modified" }, - { - "key": "content/docs/ios/vibe-coding-guide.mdx:ac034e0905fa0f85e009589f8a323658e726b629", - "path": "ios/vibe-coding-guide.mdx", - "title": "Superwall iOS Vibe Coding Guide", - "description": "Updated iOS SDK documentation", - "category": "iOS SDK", - "url": "/docs/ios/vibe-coding-guide", - "date": "2026-03-03T22:34:17.000Z", - "changeType": "modified" - }, { "key": "content/docs/android/guides/3rd-party-analytics/cohorting-in-3rd-party-tools.mdx:ac034e0905fa0f85e009589f8a323658e726b629", "path": "android/guides/3rd-party-analytics/cohorting-in-3rd-party-tools.mdx", @@ -9770,16 +9616,6 @@ "date": "2026-03-03T22:34:17.000Z", "changeType": "modified" }, - { - "key": "content/docs/android/vibe-coding-guide.mdx:ac034e0905fa0f85e009589f8a323658e726b629", - "path": "android/vibe-coding-guide.mdx", - "title": "Superwall Android Vibe Coding Guide", - "description": "Updated Android SDK documentation", - "category": "Android SDK", - "url": "/docs/android/vibe-coding-guide", - "date": "2026-03-03T22:34:17.000Z", - "changeType": "modified" - }, { "key": "content/docs/flutter/changelog.mdx:ac034e0905fa0f85e009589f8a323658e726b629", "path": "flutter/changelog.mdx", @@ -10372,16 +10208,6 @@ "date": "2026-03-03T22:34:17.000Z", "changeType": "modified" }, - { - "key": "content/docs/flutter/vibe-coding-guide.mdx:ac034e0905fa0f85e009589f8a323658e726b629", - "path": "flutter/vibe-coding-guide.mdx", - "title": "Superwall Flutter Vibe Coding Guide", - "description": "Updated Flutter SDK documentation", - "category": "Flutter SDK", - "url": "/docs/flutter/vibe-coding-guide", - "date": "2026-03-03T22:34:17.000Z", - "changeType": "modified" - }, { "key": "content/docs/expo/guides/3rd-party-analytics/cohorting-in-3rd-party-tools.mdx:ac034e0905fa0f85e009589f8a323658e726b629", "path": "expo/guides/3rd-party-analytics/cohorting-in-3rd-party-tools.mdx", @@ -10767,17 +10593,6 @@ "date": "2026-03-03T22:34:17.000Z", "changeType": "modified" }, - { - "key": "content/docs/expo/sdk-reference/getPresentationResult.mdx:ac034e0905fa0f85e009589f8a323658e726b629", - "path": "expo/sdk-reference/getPresentationResult.mdx", - "title": "getPresentationResult()", - "description": "Updated sdk reference for Expo SDK", - "category": "Expo SDK", - "subcategory": "SDK Reference", - "url": "/docs/expo/sdk-reference/getPresentationResult", - "date": "2026-03-03T22:34:17.000Z", - "changeType": "modified" - }, { "key": "content/docs/expo/sdk-reference/hooks/usePlacement.mdx:ac034e0905fa0f85e009589f8a323658e726b629", "path": "expo/sdk-reference/hooks/usePlacement.mdx", @@ -10789,16 +10604,6 @@ "date": "2026-03-03T22:34:17.000Z", "changeType": "modified" }, - { - "key": "content/docs/expo/vibe-coding-guide.mdx:ac034e0905fa0f85e009589f8a323658e726b629", - "path": "expo/vibe-coding-guide.mdx", - "title": "Superwall Expo Vibe Coding Guide", - "description": "Updated Expo SDK documentation", - "category": "Expo SDK", - "url": "/docs/expo/vibe-coding-guide", - "date": "2026-03-03T22:34:17.000Z", - "changeType": "modified" - }, { "key": "content/docs/react-native/changelog.mdx:ac034e0905fa0f85e009589f8a323658e726b629", "path": "react-native/changelog.mdx", @@ -10984,26 +10789,6 @@ "date": "2026-03-03T22:34:17.000Z", "changeType": "modified" }, - { - "key": "content/docs/react-native/vibe-coding-guide.mdx:ac034e0905fa0f85e009589f8a323658e726b629", - "path": "react-native/vibe-coding-guide.mdx", - "title": "Superwall React Native Vibe Coding Guide", - "description": "Updated React Native SDK documentation", - "category": "React Native SDK", - "url": "/docs/react-native/vibe-coding-guide", - "date": "2026-03-03T22:34:17.000Z", - "changeType": "modified" - }, - { - "key": "content/docs/dashboard/charts.mdx:ac034e0905fa0f85e009589f8a323658e726b629", - "path": "dashboard/charts.mdx", - "title": "Charts", - "description": "Updated Dashboard documentation", - "category": "Dashboard", - "url": "/docs/dashboard/charts", - "date": "2026-03-03T22:34:17.000Z", - "changeType": "modified" - }, { "key": "content/docs/dashboard/dashboard-campaigns/campaigns-audience.mdx:ac034e0905fa0f85e009589f8a323658e726b629", "path": "dashboard/dashboard-campaigns/campaigns-audience.mdx", @@ -11375,17 +11160,6 @@ "date": "2026-03-03T22:34:17.000Z", "changeType": "modified" }, - { - "key": "content/docs/dashboard/guides/using-stripe-bottom-sheet-checkout-in-app.mdx:ac034e0905fa0f85e009589f8a323658e726b629", - "path": "dashboard/guides/using-stripe-bottom-sheet-checkout-in-app.mdx", - "title": "Using Stripe's New Payment Sheet Checkout In-App", - "description": "Updated guides for Dashboard", - "category": "Dashboard", - "subcategory": "Guides", - "url": "/docs/dashboard/guides/using-stripe-bottom-sheet-checkout-in-app", - "date": "2026-03-03T22:34:17.000Z", - "changeType": "modified" - }, { "key": "content/docs/dashboard/guides/using-superwall-for-onboarding-flows.mdx:ac034e0905fa0f85e009589f8a323658e726b629", "path": "dashboard/guides/using-superwall-for-onboarding-flows.mdx", @@ -13630,16 +13404,6 @@ "date": "2026-03-02T23:15:59.000Z", "changeType": "modified" }, - { - "key": "content/docs/ios/vibe-coding-guide.mdx:ecad2c62c8cb13ccd7a00333d5c4e86744990657", - "path": "ios/vibe-coding-guide.mdx", - "title": "Superwall iOS Vibe Coding Guide", - "description": "Updated iOS SDK documentation", - "category": "iOS SDK", - "url": "/docs/ios/vibe-coding-guide", - "date": "2026-03-02T23:15:59.000Z", - "changeType": "modified" - }, { "key": "content/docs/android/guides/3rd-party-analytics/cohorting-in-3rd-party-tools.mdx:ecad2c62c8cb13ccd7a00333d5c4e86744990657", "path": "android/guides/3rd-party-analytics/cohorting-in-3rd-party-tools.mdx", @@ -14069,16 +13833,6 @@ "date": "2026-03-02T23:15:59.000Z", "changeType": "modified" }, - { - "key": "content/docs/android/vibe-coding-guide.mdx:ecad2c62c8cb13ccd7a00333d5c4e86744990657", - "path": "android/vibe-coding-guide.mdx", - "title": "Superwall Android Vibe Coding Guide", - "description": "Updated Android SDK documentation", - "category": "Android SDK", - "url": "/docs/android/vibe-coding-guide", - "date": "2026-03-02T23:15:59.000Z", - "changeType": "modified" - }, { "key": "content/docs/flutter/changelog.mdx:ecad2c62c8cb13ccd7a00333d5c4e86744990657", "path": "flutter/changelog.mdx", @@ -14671,16 +14425,6 @@ "date": "2026-03-02T23:15:59.000Z", "changeType": "modified" }, - { - "key": "content/docs/flutter/vibe-coding-guide.mdx:ecad2c62c8cb13ccd7a00333d5c4e86744990657", - "path": "flutter/vibe-coding-guide.mdx", - "title": "Superwall Flutter Vibe Coding Guide", - "description": "Updated Flutter SDK documentation", - "category": "Flutter SDK", - "url": "/docs/flutter/vibe-coding-guide", - "date": "2026-03-02T23:15:59.000Z", - "changeType": "modified" - }, { "key": "content/docs/expo/guides/3rd-party-analytics/cohorting-in-3rd-party-tools.mdx:ecad2c62c8cb13ccd7a00333d5c4e86744990657", "path": "expo/guides/3rd-party-analytics/cohorting-in-3rd-party-tools.mdx", @@ -15066,17 +14810,6 @@ "date": "2026-03-02T23:15:59.000Z", "changeType": "modified" }, - { - "key": "content/docs/expo/sdk-reference/getPresentationResult.mdx:ecad2c62c8cb13ccd7a00333d5c4e86744990657", - "path": "expo/sdk-reference/getPresentationResult.mdx", - "title": "getPresentationResult()", - "description": "Updated sdk reference for Expo SDK", - "category": "Expo SDK", - "subcategory": "SDK Reference", - "url": "/docs/expo/sdk-reference/getPresentationResult", - "date": "2026-03-02T23:15:59.000Z", - "changeType": "modified" - }, { "key": "content/docs/expo/sdk-reference/hooks/usePlacement.mdx:ecad2c62c8cb13ccd7a00333d5c4e86744990657", "path": "expo/sdk-reference/hooks/usePlacement.mdx", @@ -15121,16 +14854,6 @@ "date": "2026-03-02T23:15:59.000Z", "changeType": "modified" }, - { - "key": "content/docs/expo/vibe-coding-guide.mdx:ecad2c62c8cb13ccd7a00333d5c4e86744990657", - "path": "expo/vibe-coding-guide.mdx", - "title": "Superwall Expo Vibe Coding Guide", - "description": "Updated Expo SDK documentation", - "category": "Expo SDK", - "url": "/docs/expo/vibe-coding-guide", - "date": "2026-03-02T23:15:59.000Z", - "changeType": "modified" - }, { "key": "content/docs/react-native/changelog.mdx:ecad2c62c8cb13ccd7a00333d5c4e86744990657", "path": "react-native/changelog.mdx", @@ -15316,26 +15039,6 @@ "date": "2026-03-02T23:15:59.000Z", "changeType": "modified" }, - { - "key": "content/docs/react-native/vibe-coding-guide.mdx:ecad2c62c8cb13ccd7a00333d5c4e86744990657", - "path": "react-native/vibe-coding-guide.mdx", - "title": "Superwall React Native Vibe Coding Guide", - "description": "Updated React Native SDK documentation", - "category": "React Native SDK", - "url": "/docs/react-native/vibe-coding-guide", - "date": "2026-03-02T23:15:59.000Z", - "changeType": "modified" - }, - { - "key": "content/docs/dashboard/charts.mdx:ecad2c62c8cb13ccd7a00333d5c4e86744990657", - "path": "dashboard/charts.mdx", - "title": "Charts", - "description": "Updated Dashboard documentation", - "category": "Dashboard", - "url": "/docs/dashboard/charts", - "date": "2026-03-02T23:15:59.000Z", - "changeType": "modified" - }, { "key": "content/docs/dashboard/dashboard-campaigns/campaigns-audience.mdx:ecad2c62c8cb13ccd7a00333d5c4e86744990657", "path": "dashboard/dashboard-campaigns/campaigns-audience.mdx", @@ -15718,17 +15421,6 @@ "date": "2026-03-02T23:15:59.000Z", "changeType": "modified" }, - { - "key": "content/docs/dashboard/guides/using-stripe-bottom-sheet-checkout-in-app.mdx:ecad2c62c8cb13ccd7a00333d5c4e86744990657", - "path": "dashboard/guides/using-stripe-bottom-sheet-checkout-in-app.mdx", - "title": "Using Stripe's New Payment Sheet Checkout In-App", - "description": "Updated guides for Dashboard", - "category": "Dashboard", - "subcategory": "Guides", - "url": "/docs/dashboard/guides/using-stripe-bottom-sheet-checkout-in-app", - "date": "2026-03-02T23:15:59.000Z", - "changeType": "modified" - }, { "key": "content/docs/dashboard/guides/using-superwall-for-onboarding-flows.mdx:ecad2c62c8cb13ccd7a00333d5c4e86744990657", "path": "dashboard/guides/using-superwall-for-onboarding-flows.mdx", @@ -17293,4 +16985,4 @@ "changeType": "modified" } ] -} \ No newline at end of file +} diff --git a/src/lib/local-debug.test.ts b/src/lib/local-debug.test.ts new file mode 100644 index 00000000..a95db614 --- /dev/null +++ b/src/lib/local-debug.test.ts @@ -0,0 +1,18 @@ +import assert from "node:assert/strict"; +import { describe, test } from "node:test"; +import { isLocalDebugHost } from "./local-debug"; + +describe("local debug host guard", () => { + test("allows local preview and dev hosts", () => { + assert.equal(isLocalDebugHost("localhost:3005"), true); + assert.equal(isLocalDebugHost("127.0.0.1:3005"), true); + assert.equal(isLocalDebugHost("[::1]:3005"), true); + }); + + test("rejects production-like hosts", () => { + assert.equal(isLocalDebugHost("superwall.com"), false); + assert.equal(isLocalDebugHost("docs.superwall.com"), false); + assert.equal(isLocalDebugHost(""), false); + assert.equal(isLocalDebugHost(null), false); + }); +}); diff --git a/src/lib/local-debug.ts b/src/lib/local-debug.ts new file mode 100644 index 00000000..0810747d --- /dev/null +++ b/src/lib/local-debug.ts @@ -0,0 +1,12 @@ +export function isLocalDebugHost(host?: string | null) { + if (!host) return false; + + const normalized = host.trim().toLowerCase(); + if (!normalized) return false; + + const hostname = normalized.startsWith("[") + ? normalized.slice(1, normalized.indexOf("]")) + : normalized.split(":")[0]; + + return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1"; +} diff --git a/src/lib/metadata.test.ts b/src/lib/metadata.test.ts new file mode 100644 index 00000000..14f13711 --- /dev/null +++ b/src/lib/metadata.test.ts @@ -0,0 +1,16 @@ +import assert from "node:assert/strict"; +import { describe, test } from "node:test"; +import { ERROR_PAGE_TITLE, hasRouteError, NOINDEX_ROBOTS_META } from "./metadata"; + +describe("metadata error helpers", () => { + test("detects error route matches for noindex fallback pages", () => { + assert.equal(hasRouteError([{ status: "success" }, { status: "error" }]), true); + assert.equal(hasRouteError([{ status: "success" }]), false); + assert.equal(hasRouteError(), false); + }); + + test("uses crawler-safe error metadata values", () => { + assert.match(ERROR_PAGE_TITLE, /^Error loading docs page/); + assert.equal(NOINDEX_ROBOTS_META, "noindex, nofollow"); + }); +}); diff --git a/src/lib/metadata.ts b/src/lib/metadata.ts index a6b83b8c..a2362da8 100644 --- a/src/lib/metadata.ts +++ b/src/lib/metadata.ts @@ -3,6 +3,13 @@ export const SITE_NAME = "Superwall Docs"; export const DEFAULT_DESCRIPTION = "Guides and references for Superwall SDKs, the dashboard, and integrations."; export const TWITTER_HANDLE = "@superwall"; +export const ERROR_PAGE_TITLE = `Error loading docs page | ${SITE_NAME}`; +export const ERROR_PAGE_DESCRIPTION = "An error prevented this Superwall docs page from loading."; +export const NOINDEX_ROBOTS_META = "noindex, nofollow"; + +export function hasRouteError(matches?: Array<{ status?: unknown }>) { + return matches?.some((match) => match.status === "error") ?? false; +} export function normalizeMetadataPath(path?: string | null) { if (!path) return ""; diff --git a/src/mdx-components.tsx b/src/mdx-components.tsx index 2c94d89b..f4082246 100644 --- a/src/mdx-components.tsx +++ b/src/mdx-components.tsx @@ -274,9 +274,9 @@ const Card = ({ {title} - + {children} - + {isExternal && ( rootRouteImport, } as any) +const DebugErrorRoute = DebugErrorRouteImport.update({ + id: '/debug/error', + path: '/debug/error', + getParentRoute: () => rootRouteImport, +} as any) const DashboardLlmsDottxtRoute = DashboardLlmsDottxtRouteImport.update({ id: '/dashboard/llms.txt', path: '/dashboard/llms.txt', @@ -289,6 +295,7 @@ export interface FileRoutesByFullPath { '/api/search': typeof ApiSearchRoute '/dashboard/llms-full.txt': typeof DashboardLlmsFullDottxtRoute '/dashboard/llms.txt': typeof DashboardLlmsDottxtRoute + '/debug/error': typeof DebugErrorRoute '/expo/llms-full.txt': typeof ExpoLlmsFullDottxtRoute '/expo/llms.txt': typeof ExpoLlmsDottxtRoute '/flutter/llms-full.txt': typeof FlutterLlmsFullDottxtRoute @@ -333,6 +340,7 @@ export interface FileRoutesByTo { '/api/search': typeof ApiSearchRoute '/dashboard/llms-full.txt': typeof DashboardLlmsFullDottxtRoute '/dashboard/llms.txt': typeof DashboardLlmsDottxtRoute + '/debug/error': typeof DebugErrorRoute '/expo/llms-full.txt': typeof ExpoLlmsFullDottxtRoute '/expo/llms.txt': typeof ExpoLlmsDottxtRoute '/flutter/llms-full.txt': typeof FlutterLlmsFullDottxtRoute @@ -377,6 +385,7 @@ export interface FileRoutesById { '/api/search': typeof ApiSearchRoute '/dashboard/llms-full.txt': typeof DashboardLlmsFullDottxtRoute '/dashboard/llms.txt': typeof DashboardLlmsDottxtRoute + '/debug/error': typeof DebugErrorRoute '/expo/llms-full.txt': typeof ExpoLlmsFullDottxtRoute '/expo/llms.txt': typeof ExpoLlmsDottxtRoute '/flutter/llms-full.txt': typeof FlutterLlmsFullDottxtRoute @@ -423,6 +432,7 @@ export interface FileRouteTypes { | '/api/search' | '/dashboard/llms-full.txt' | '/dashboard/llms.txt' + | '/debug/error' | '/expo/llms-full.txt' | '/expo/llms.txt' | '/flutter/llms-full.txt' @@ -467,6 +477,7 @@ export interface FileRouteTypes { | '/api/search' | '/dashboard/llms-full.txt' | '/dashboard/llms.txt' + | '/debug/error' | '/expo/llms-full.txt' | '/expo/llms.txt' | '/flutter/llms-full.txt' @@ -510,6 +521,7 @@ export interface FileRouteTypes { | '/api/search' | '/dashboard/llms-full.txt' | '/dashboard/llms.txt' + | '/debug/error' | '/expo/llms-full.txt' | '/expo/llms.txt' | '/flutter/llms-full.txt' @@ -555,6 +567,7 @@ export interface RootRouteChildren { ApiSearchRoute: typeof ApiSearchRoute DashboardLlmsFullDottxtRoute: typeof DashboardLlmsFullDottxtRoute DashboardLlmsDottxtRoute: typeof DashboardLlmsDottxtRoute + DebugErrorRoute: typeof DebugErrorRoute ExpoLlmsFullDottxtRoute: typeof ExpoLlmsFullDottxtRoute ExpoLlmsDottxtRoute: typeof ExpoLlmsDottxtRoute FlutterLlmsFullDottxtRoute: typeof FlutterLlmsFullDottxtRoute @@ -796,6 +809,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof ExpoLlmsFullDottxtRouteImport parentRoute: typeof rootRouteImport } + '/debug/error': { + id: '/debug/error' + path: '/debug/error' + fullPath: '/debug/error' + preLoaderRoute: typeof DebugErrorRouteImport + parentRoute: typeof rootRouteImport + } '/dashboard/llms.txt': { id: '/dashboard/llms.txt' path: '/dashboard/llms.txt' @@ -912,6 +932,7 @@ const rootRouteChildren: RootRouteChildren = { ApiSearchRoute: ApiSearchRoute, DashboardLlmsFullDottxtRoute: DashboardLlmsFullDottxtRoute, DashboardLlmsDottxtRoute: DashboardLlmsDottxtRoute, + DebugErrorRoute: DebugErrorRoute, ExpoLlmsFullDottxtRoute: ExpoLlmsFullDottxtRoute, ExpoLlmsDottxtRoute: ExpoLlmsDottxtRoute, FlutterLlmsFullDottxtRoute: FlutterLlmsFullDottxtRoute, diff --git a/src/router.tsx b/src/router.tsx index 556bea3e..44cfc282 100644 --- a/src/router.tsx +++ b/src/router.tsx @@ -1,5 +1,6 @@ import { createRouter as createTanStackRouter } from "@tanstack/react-router"; import { routeTree } from "./routeTree.gen"; +import { DocsError } from "@/components/DocsError"; import { NotFound } from "@/components/not-found"; export function getRouter() { @@ -7,6 +8,7 @@ export function getRouter() { routeTree, defaultPreload: "intent", scrollRestoration: true, + defaultErrorComponent: DocsError, defaultNotFoundComponent: NotFound, }); } diff --git a/src/routes/$.tsx b/src/routes/$.tsx index 62d9b833..fb93cfdb 100644 --- a/src/routes/$.tsx +++ b/src/routes/$.tsx @@ -1,9 +1,7 @@ import { createFileRoute, notFound } from "@tanstack/react-router"; import { DocsLayout } from "fumadocs-ui/layouts/notebook"; import { getLayoutTabs, type LayoutTab } from "fumadocs-ui/layouts/shared"; -import type { Root as PageTreeRoot } from "fumadocs-core/page-tree"; import { createServerFn } from "@tanstack/react-start"; -import { source } from "@/lib/source"; import browserCollections from "fumadocs-mdx:collections/browser"; import { DocsBody, DocsDescription, DocsPage, DocsTitle } from "fumadocs-ui/layouts/notebook/page"; import { getMDXComponents } from "@/mdx-components"; @@ -20,6 +18,7 @@ import { buildCanonicalUrl, DEFAULT_DESCRIPTION, DEFAULT_SOCIAL_IMAGE_URL, + NOINDEX_ROBOTS_META, SITE_NAME, TWITTER_HANDLE, } from "@/lib/metadata"; @@ -35,6 +34,9 @@ import { buildMarkdownAlternatePath } from "@/lib/markdown-alternate"; import { staticCacheMiddleware } from "@/lib/static-cache-middleware"; +type Source = (typeof import("@/lib/source"))["source"]; +type SerializedPageTree = Awaited>; + type DocsLoaderData = { url: string; path: string; @@ -43,10 +45,16 @@ type DocsLoaderData = { pageDescription: string; canonicalUrl: string; structuredData: JsonLdGraph; - pageTree: Awaited>; + pageTree: SerializedPageTree; }; export const Route = createFileRoute("/$")({ + headers: ({ match }) => + match.status === "error" + ? { + "X-Robots-Tag": NOINDEX_ROBOTS_META, + } + : undefined, component: Page, loader: async ({ params }) => { const slugs = params._splat?.split("/") ?? []; @@ -95,7 +103,7 @@ export const Route = createFileRoute("/$")({ }, }); -let pageTreePromise: Promise>> | null = null; +let pageTreePromise: Promise | null = null; const serverLoader = createServerFn({ method: "GET", @@ -103,6 +111,7 @@ const serverLoader = createServerFn({ .middleware([staticCacheMiddleware]) .inputValidator((slugs: string[]) => slugs) .handler(async ({ data: slugs }): Promise => { + const { source } = await import("@/lib/source"); const page = source.getPage(slugs); if (!page) return null; @@ -141,11 +150,9 @@ const clientLoader = browserCollections.docs.createClientLoader({ { toc, frontmatter, default: MDX }, { url, - path, githubUrl, }: { url: string; - path: string; githubUrl: string; }, ) { @@ -195,7 +202,10 @@ function parseSDKSubPath(url: string): string | null { return withoutBase.slice(sdkPrefix.length); } -function getDocsLayoutTabs(pageTree: PageTreeRoot, currentSubPath: string | null): LayoutTab[] { +function getDocsLayoutTabs( + pageTree: SerializedPageTree, + currentSubPath: string | null, +): LayoutTab[] { const tabs = getLayoutTabs(pageTree, { transform: (option) => { const tabPrefix = option.url.replace(/^\/docs\//, "").split("/")[0]; diff --git a/src/routes/__root.tsx b/src/routes/__root.tsx index 4c36d029..8cbc9e76 100644 --- a/src/routes/__root.tsx +++ b/src/routes/__root.tsx @@ -19,12 +19,69 @@ import { GlobalScripts } from "@/components/GlobalScripts"; import { DEFAULT_DESCRIPTION, DEFAULT_SOCIAL_IMAGE_URL, + ERROR_PAGE_DESCRIPTION, + ERROR_PAGE_TITLE, + hasRouteError, + NOINDEX_ROBOTS_META, SITE_NAME, TWITTER_HANDLE, } from "@/lib/metadata"; import { normalizeDocsInternalHref } from "@/lib/docs-url"; import { resolveSdkAwareDocsHref } from "@/lib/sdk-navigation"; import { DOCS_BASE, toRouterPath } from "@/lib/url-base"; +import { DocsError } from "@/components/DocsError"; + +const CHUNK_LOAD_RECOVERY_SCRIPT = String.raw` +(function () { + var key = "superwall-docs:chunk-load-reload"; + var ttl = 5 * 60 * 1000; + + function messageFrom(value) { + if (!value) return ""; + if (typeof value === "string") return value; + if (typeof value.message === "string") return value.message; + if (typeof value.toString === "function") return value.toString(); + return ""; + } + + function isChunkLoadError(value) { + var message = messageFrom(value); + return message.indexOf("Failed to fetch dynamically imported module") !== -1 || + message.indexOf("Importing a module script failed") !== -1 || + message.indexOf("error loading dynamically imported module") !== -1; + } + + function reloadOnce() { + var now = Date.now(); + var marker = location.href + "|" + now; + + try { + var previous = sessionStorage.getItem(key); + if (previous) { + var parts = previous.split("|"); + var previousUrl = parts.slice(0, -1).join("|"); + var previousTime = Number(parts[parts.length - 1]); + if (previousUrl === location.href && now - previousTime < ttl) return false; + } + sessionStorage.setItem(key, marker); + } catch (_) { + return false; + } + + location.reload(); + return true; + } + + window.addEventListener("vite:preloadError", function (event) { + if (reloadOnce()) event.preventDefault(); + }); + + window.addEventListener("unhandledrejection", function (event) { + if (!isChunkLoadError(event.reason)) return; + if (reloadOnce()) event.preventDefault(); + }); +})(); +`; type RootProviderLinkProps = React.ComponentProps<"a"> & { prefetch?: boolean; @@ -104,70 +161,91 @@ function DocsLink({ href, prefetch = true, ...props }: RootProviderLinkProps) { } export const Route = createRootRoute({ - head: () => ({ - meta: [ - { - charSet: "utf-8", - }, - { - name: "viewport", - content: "width=device-width, initial-scale=1", - }, - { - title: SITE_NAME, - }, - { - name: "description", - content: DEFAULT_DESCRIPTION, - }, - { - property: "og:site_name", - content: SITE_NAME, - }, - { - property: "og:type", - content: "website", - }, - { - property: "og:title", - content: SITE_NAME, - }, - { - property: "og:description", - content: DEFAULT_DESCRIPTION, - }, - { - property: "og:image", - content: DEFAULT_SOCIAL_IMAGE_URL, - }, - { - name: "twitter:card", - content: "summary_large_image", - }, - { - name: "twitter:site", - content: TWITTER_HANDLE, - }, - { - name: "twitter:creator", - content: TWITTER_HANDLE, - }, - { - name: "twitter:title", - content: SITE_NAME, - }, - { - name: "twitter:description", - content: DEFAULT_DESCRIPTION, - }, - { - name: "twitter:image", - content: DEFAULT_SOCIAL_IMAGE_URL, - }, - ], - links: [{ rel: "stylesheet", href: appCss }], - }), + headers: ({ matches }) => + hasRouteError(matches) + ? { + "X-Robots-Tag": NOINDEX_ROBOTS_META, + } + : undefined, + head: ({ matches }) => { + const isErrorPage = hasRouteError(matches); + const title = isErrorPage ? ERROR_PAGE_TITLE : SITE_NAME; + const description = isErrorPage ? ERROR_PAGE_DESCRIPTION : DEFAULT_DESCRIPTION; + + return { + meta: [ + { + charSet: "utf-8", + }, + { + name: "viewport", + content: "width=device-width, initial-scale=1", + }, + { + title, + }, + { + name: "description", + content: description, + }, + ...(isErrorPage + ? [ + { + name: "robots", + content: NOINDEX_ROBOTS_META, + }, + ] + : []), + { + property: "og:site_name", + content: SITE_NAME, + }, + { + property: "og:type", + content: "website", + }, + { + property: "og:title", + content: title, + }, + { + property: "og:description", + content: description, + }, + { + property: "og:image", + content: DEFAULT_SOCIAL_IMAGE_URL, + }, + { + name: "twitter:card", + content: "summary_large_image", + }, + { + name: "twitter:site", + content: TWITTER_HANDLE, + }, + { + name: "twitter:creator", + content: TWITTER_HANDLE, + }, + { + name: "twitter:title", + content: title, + }, + { + name: "twitter:description", + content: description, + }, + { + name: "twitter:image", + content: DEFAULT_SOCIAL_IMAGE_URL, + }, + ], + links: [{ rel: "stylesheet", href: appCss }], + }; + }, component: RootComponent, + errorComponent: RootErrorComponent, }); function RootComponent() { @@ -178,11 +256,31 @@ function RootComponent() { ); } +function RootErrorComponent(props: React.ComponentProps) { + return ( + + + + ); +} + function RootDocument({ children }: { children: React.ReactNode }) { + const isErrorPage = useRouterState({ + select: (state) => state.matches.some((match) => match.status === "error"), + }); + return ( + + {isErrorPage ? ( + <> + {ERROR_PAGE_TITLE} + + + > + ) : null}
+ An error prevented this page from loading. Try refreshing, or go back to the docs home + page. +
+