Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/feat-error-boundaries-5047.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'asyncapi-website': patch
---

feat(ui): add a top-level React `ErrorBoundary` and wrap every `Layout` return tree in it, so a render error in any descendant component (broken MDX page, future page, etc.) no longer tears down the whole site — it shows a recoverable fallback instead.
54 changes: 54 additions & 0 deletions components/ErrorBoundary.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { Component, ErrorInfo, ReactNode } from 'react';

interface ErrorBoundaryProps {
children: ReactNode;
fallback?: ReactNode;
}

interface ErrorBoundaryState {
hasError: boolean;
error: Error | null;
}

/**
* Top-level ErrorBoundary for the AsyncAPI website.
*
* Wrapping the whole app in <ErrorBoundary> at the layout level means a render
* error in any descendant component (e.g. a future page, a broken MDX block)
* no longer tears down the whole site — it just shows a small recoverable
* fallback so navigation still works.
*
* See: https://react.dev/reference/react/Component#catching-rendering-errors-with-an-error-boundary
*/
export default class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
constructor(props: ErrorBoundaryProps) {
super(props);
this.state = { hasError: false, error: null };
}

static getDerivedStateFromError(error: Error): ErrorBoundaryState {
return { hasError: true, error };
}

componentDidCatch(error: Error, info: ErrorInfo): void {
// Surface the error to the browser console; in production a real Sentry
// hook would go here.
// eslint-disable-next-line no-console
console.error('[ErrorBoundary] Caught a render error:', error, info);
}

render(): ReactNode {
if (this.state.hasError) {
if (this.props.fallback) {
return this.props.fallback;
}
return (
<div role="alert" style={{ padding: '2rem', fontFamily: 'system-ui, sans-serif' }}>
<h1>Something went wrong.</h1>
<p>Please refresh the page, or use the navigation to continue.</p>
</div>
);
}
return this.props.children;
}
}
33 changes: 24 additions & 9 deletions components/layout/Layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import type { IPost, IPosts } from '@/types/post';

import BlogContext from '../../context/BlogContext';
import { getAllPosts, getDocBySlug, getPostBySlug } from '../../utils/api';
import ErrorBoundary from '../ErrorBoundary';
import BlogLayout from './BlogLayout';
import DocsLayout from './DocsLayout';
import GenericPostLayout from './GenericPostLayout';
Expand All @@ -28,9 +29,11 @@ export default function Layout({ children }: ILayoutProps): React.JSX.Element {

return (
<div data-testid='Docs-main-container'>
<DocsLayout post={post} navItems={allDocPosts}>
{children}
</DocsLayout>
<ErrorBoundary>
<DocsLayout post={post} navItems={allDocPosts}>
{children}
</DocsLayout>
</ErrorBoundary>
Comment on lines +32 to +36

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the relevant layout file and the ErrorBoundary implementation.
ls -la
rg -n "function Layout|const Layout|export default function Layout|export default Layout" components/layout/Layout.tsx || true
sed -n '1,140p' components/layout/Layout.tsx

# Find ErrorBoundary definition and inspect its props/behavior.
rg -n "ErrorBoundary" components/layout/Layout.tsx components -S
# Attempt to locate the imported ErrorBoundary file.
python3 - <<'PY'
import re, pathlib
p = pathlib.Path("components/layout/Layout.tsx")
txt = p.read_text()
m = re.search(r"from ['\"]([^'\"]*ErrorBoundary[^'\"]*)['\"]", txt)
print("import_guess:", m.group(1) if m else None)
PY

# Grep for the ErrorBoundary component definition.
fd -t f "ErrorBoundary.*" components pages src . 2>/dev/null || true
rg -n "export (default )?function ErrorBoundary|const ErrorBoundary|function ErrorBoundary|class ErrorBoundary" components -S || true
rg -n "resetKeys|fallbackRender|onReset|resetBoundary|useErrorBoundary|react-error-boundary" components -S || true

# Inspect the ErrorBoundary source file(s) if found.
python3 - <<'PY'
import subprocess, shlex, re, pathlib, os, json, sys
# Use rg results to pick likely files
res = subprocess.check_output(["bash","-lc","rg -l \"ErrorBoundary\" components | head -n 50"]).decode().splitlines()
print("\nCandidate files mentioning ErrorBoundary:")
for f in res[:30]:
    print(f)
PY

Repository: asyncapi/website

Length of output: 8841


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Show the full ErrorBoundary implementation (should be small).
sed -n '1,200p' components/ErrorBoundary.tsx

# Show Layout around the relevant branches with line numbers.
nl -ba components/layout/Layout.tsx | sed -n '1,140p'

Repository: asyncapi/website

Length of output: 1945


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Show pages/_app.tsx where Layout is mounted.
sed -n '1,200p' pages/_app.tsx

# Show Layout branches with line numbers (use awk instead of nl).
awk '{print NR ":" $0}' components/layout/Layout.tsx | sed -n '1,140p'

# Inspect layouts to see whether they render chrome around children or just children.
for f in components/layout/DocsLayout.tsx components/layout/BlogLayout.tsx components/layout/GenericPostLayout.tsx; do
  echo "---- $f ----"
  test -f "$f" && sed -n '1,220p' "$f" || echo "missing $f"
done

Repository: asyncapi/website

Length of output: 19431


Move ErrorBoundary to wrap children, not the whole docs/blog/post layout

In components/layout/Layout.tsx, ErrorBoundary wraps DocsLayout / BlogLayout / GenericPostLayout, so a render error in descendants replaces the entire section chrome (e.g., sidebar/TOC/header) with the fallback (see around lines 32–36, 45–49, 66–68). Also, components/ErrorBoundary.tsx has no reset logic—once hasError is set, it will keep showing the fallback until the boundary is unmounted/remounted (so in-branch navigation can get stuck).

Move the boundary inside each layout to wrap only the children slot (or add a reset mechanism keyed to the route/content).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@components/layout/Layout.tsx` around lines 32 - 36, The ErrorBoundary
currently wraps the entire DocsLayout/BlogLayout/GenericPostLayout in
components/layout/Layout.tsx so layout chrome is replaced on child render errors
and the boundary never resets; change Layout.tsx to remove the outer
ErrorBoundary and instead place ErrorBoundary inside each layout component
(DocsLayout, BlogLayout, GenericPostLayout) so it only wraps the children prop,
or add reset logic in components/ErrorBoundary.tsx (reset hasError when the
route/content key changes via useRouter().asPath or an explicit key prop) so
navigation clears the error state.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate and print the relevant sections of Layout.tsx
FILE="components/layout/Layout.tsx"
if [ ! -f "$FILE" ]; then
  echo "Missing file: $FILE" >&2
  exit 1
fi

echo "=== File: $FILE ==="
nl -ba "$FILE" | sed -n '1,140p'

# Find all ErrorBoundary usages in this file (and nearby contexts)
echo
echo "=== ErrorBoundary occurrences in $FILE ==="
rg -n "ErrorBoundary" "$FILE" || true

# Check where `pathname` comes from in this file (if referenced)
echo
echo "=== pathname references in $FILE ==="
rg -n "pathname" "$FILE" || true

# Identify ErrorBoundary import to see what props it supports (resetKey, etc.)
echo
echo "=== ErrorBoundary import lines in $FILE ==="
rg -n "import .*ErrorBoundary|from .*ErrorBoundary" "$FILE" || true

# If ErrorBoundary is a local component, find its definition
echo
echo "=== Searching for local ErrorBoundary component ==="
# Try common patterns
rg -n "function ErrorBoundary|const ErrorBoundary|class ErrorBoundary" components . || true

# If there's a file exporting ErrorBoundary, list it
echo
echo "=== Files exporting ErrorBoundary ==="
fd -t f "ErrorBoundary.*" . || true

Repository: asyncapi/website

Length of output: 165


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="components/layout/Layout.tsx"
if [ ! -f "$FILE" ]; then
  echo "Missing file: $FILE" >&2
  exit 1
fi

echo "=== File: $FILE (first 160 lines) ==="
awk '{printf "%6d\t%s\n", NR, $0}' "$FILE" | sed -n '1,160p'

echo
echo "=== ErrorBoundary occurrences in $FILE ==="
rg -n "ErrorBoundary" "$FILE" || true

echo
echo "=== pathname references in $FILE ==="
rg -n "pathname" "$FILE" || true

echo
echo "=== ErrorBoundary import lines in $FILE ==="
rg -n "import .*ErrorBoundary|from .*ErrorBoundary" "$FILE" || true

echo
echo "=== Searching for local ErrorBoundary component definition (best-effort) ==="
rg -n "function ErrorBoundary|const ErrorBoundary|class ErrorBoundary" components . || true

echo
echo "=== Files exporting ErrorBoundary (best-effort) ==="
fd -t f -i "errorboundary" . || true

Repository: asyncapi/website

Length of output: 4259


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="components/ErrorBoundary.tsx"
if [ ! -f "$FILE" ]; then
  echo "Missing file: $FILE" >&2
  exit 1
fi

echo "=== File: $FILE (first 220 lines) ==="
awk '{printf "%6d\t%s\n", NR, $0}' "$FILE" | sed -n '1,220p'

echo
echo "=== ErrorBoundaryProps definition ==="
rg -n "interface ErrorBoundaryProps|type ErrorBoundaryProps|ErrorBoundaryProps" "$FILE" || true

echo
echo "=== reset-related props/methods ==="
rg -n "reset|componentDidUpdate|shouldComponentUpdate|getDerivedStateFromError|componentDidCatch|key" "$FILE" || true

Repository: asyncapi/website

Length of output: 2717


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Find implementations
rg -n "function getDocBySlug|getDocBySlug\s*=\s*\(|getDocBySlug:" -S components/utils pages src utils . || true
rg -n "function getPostBySlug|getPostBySlug\s*=\s*\(|getPostBySlug:" -S components/utils pages src utils . || true

# Narrow to the utils/api import location used by Layout.tsx
# Layout imports from '../../utils/api' relative to components/layout -> utils/api.ts?(x)
fd -t f -a "api.*" utils components . 2>/dev/null || true

# Specifically print the api file(s) that contain getDocBySlug/getPostBySlug
fd -t f "api.*" . | head -n 50

Repository: asyncapi/website

Length of output: 16700


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect utils/api.ts around getDocBySlug and getPostBySlug
FILE="utils/api.ts"
if [ ! -f "$FILE" ]; then
  echo "Missing file: $FILE" >&2
  exit 1
fi

echo "=== File: $FILE (first 220 lines) ==="
awk '{printf "%6d\t%s\n", NR, $0}' "$FILE" | sed -n '1,220p'

echo
echo "=== getDocBySlug implementation ==="
awk 'NR>=30 && NR<=90 {printf "%6d\t%s\n", NR, $0}' "$FILE"

echo
echo "=== getPostBySlug implementation ==="
awk 'NR>=1 && NR<=60 {printf "%6d\t%s\n", NR, $0}' "$FILE"

# Find where Layout.tsx is used (Next entrypoint)
echo
echo "=== Searching for Layout usage ==="
rg -n "from '.*components/layout/Layout'|components/layout/Layout|<Layout\b|function MyApp|_app\.tsx" . || true

# Prefer checking Next.js _app
echo
echo "=== Looking for pages/_app or app/layout ==="
rg -n "pages/_app|app/layout|_app\.tsx|appDir" . || true

# Print candidate entrypoints if found
echo
echo "=== Listing likely Next entry files ==="
fd -t f "_app\.(t|j)sx|_document\.(t|j)sx|layout\.(t|j)sx" pages app . || true

Repository: asyncapi/website

Length of output: 4609


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect _app.tsx
FILE="pages/_app.tsx"
echo "=== File: $FILE (first 140 lines) ==="
awk '{printf "%6d\t%s\n", NR, $0}' "$FILE" | sed -n '1,140p'

echo
echo "=== Listing pages/ route files related to docs/blog ==="
# list likely route files
fd -t f -a "pages" -x sh -lc 'echo {}' . 2>/dev/null | head -n 5 || true

# Use fd scoped to pages
fd -t f "docs" pages -a 2>/dev/null || true
fd -t f "blog" pages -a 2>/dev/null || true

# Also print all top-level pages files (limited)
echo
echo "=== Top-level pages/* (first 80 entries) ==="
find pages -maxdepth 2 -type f | sed -n '1,80p'

Repository: asyncapi/website

Length of output: 2895


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Search for docs routes in pages/ ==="
rg -n "docs" pages --glob='*.tsx' --glob='*.ts' || true

echo
echo "=== List likely docs page files ==="
fd -t f "docs" pages -d 4 || true
fd -t f "doc" pages -d 4 || true

echo
echo "=== Print pages under pages/** that include '/docs' in filename/path ==="
find pages -type f | rg -n "docs|doc" || true

echo
echo "=== Search for getStaticProps/getServerSideProps under docs pages ==="
# best-effort: look for getDocBySlug usage to find which pages call it
rg -n "getDocBySlug" pages components utils --glob='*.ts' --glob='*.tsx' || true

Repository: asyncapi/website

Length of output: 3473


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Count and list pages/ files (maxdepth 6, limited output) ==="
COUNT=$(find pages -type f | wc -l | tr -d ' ')
echo "pages files: $COUNT"
find pages -maxdepth 6 -type f | sort | sed -n '1,200p'

echo
echo "=== Find dynamic route patterns in pages/ ==="
rg -n "\[[^\]]+\]" pages --glob='*.tsx' --glob='*.ts' || true

echo
echo "=== Search for route files that mention '/docs' or 'docs' in their content ==="
rg -n "['\"]/docs['\"]|startsWith\\('/docs'|/docs/" pages --glob='*.tsx' --glob='*.ts' || true

echo
echo "=== Inspect config/posts.json for docs slug format ==="
POSTS_FILE="config/posts.json"
if [ ! -f "$POSTS_FILE" ]; then
  POSTS_FILE="config/posts.json"
fi

if [ ! -f "$POSTS_FILE" ]; then
  echo "Missing $POSTS_FILE" >&2
  exit 1
fi

# Print first few occurrences of "/docs/" within the docs section for slug shape
# (Keep output small.)
rg -n "\"slug\"\s*:\s*\"/docs/" "$POSTS_FILE" --max-count 20 || true

# Also show a couple non-doc slugs to compare
rg -n "\"slug\"\s*:\s*\"/blog/" "$POSTS_FILE" --max-count 10 || true

echo
echo "=== Show a small sample around first /docs/ slug occurrence ==="
FIRST_LINE=$(rg -n "\"slug\"\s*:\s*\"/docs/" "$POSTS_FILE" | head -n 1 | cut -d: -f1 || true)
if [ -n "${FIRST_LINE:-}" ]; then
  START=$((FIRST_LINE-5))
  END=$((FIRST_LINE+20))
  awk -v s=$START -v e=$END 'NR>=s && NR<=e {print NR ":" $0}' "$POSTS_FILE"
fi

Repository: asyncapi/website

Length of output: 12231


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Find posts.json in repo ==="
fd -t f -i "posts.json" . || true
fd -t f "posts.json" . || true

echo
echo "=== Find config directory contents (if any) ==="
ls -la config 2>/dev/null || true
ls -la . | head

echo
echo "=== Inspect scripts/build-pages.ts ==="
FILE="scripts/build-pages.ts"
if [ -f "$FILE" ]; then
  awk '{printf "%6d\t%s\n", NR, $0}' "$FILE" | sed -n '1,220p'
else
  echo "Missing $FILE" >&2
fi

echo
echo "=== Inspect tests/build-pages.test.ts (if exists) ==="
FILE="tests/build-pages.test.ts"
if [ -f "$FILE" ]; then
  awk '{printf "%6d\t%s\n", NR, $0}' "$FILE" | sed -n '1,220p'
else
  echo "Missing $FILE" >&2
fi

echo
echo "=== Search for generated pages path patterns in build script output ==="
# look for "pages/" writes or templates
rg -n "pages/|mkdir|writeFile|fs\.|outputDir|outDir|docs|blog" scripts/build-pages.ts tests/build-pages.test.ts || true

Repository: asyncapi/website

Length of output: 12750


Reset the ErrorBoundary when navigation changes

components/ErrorBoundary.tsx sets hasError in state and never clears it, so once a boundary enters the fallback it will stay there. Since components/layout/Layout.tsx is mounted in pages/_app.tsx and the /docs/*//blog/* branches reuse the same (unkeyed) <ErrorBoundary> instance during client-side navigation, the fallback won’t clear when moving within the same branch. Key each <ErrorBoundary> by a navigation-changing value (use router.asPath), or add a resetKey-driven reset inside components/ErrorBoundary.tsx.

Apply to: components/layout/Layout.tsx line ranges 32-36, 45-49, 56-58, 66-68, and 74.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@components/layout/Layout.tsx` around lines 32 - 36, The ErrorBoundary
instance in Layout.tsx never resets on client navigation; wrap each
ErrorBoundary in Layout.tsx (the ones around DocsLayout, BlogLayout, and other
branches) with a key driven by the router path (e.g. import useRouter from
'next/router' and add key={router.asPath} to the <ErrorBoundary> element) so a
new boundary is mounted on navigation and has its hasError state cleared; if you
prefer, alternatively update components/ErrorBoundary.tsx to accept a resetKey
prop and clear hasError when resetKey changes, but the quickest fix is adding
key={router.asPath} to ErrorBoundary in Layout.tsx for the affected blocks.

</div>
);
}
Expand All @@ -39,24 +42,36 @@ export default function Layout({ children }: ILayoutProps): React.JSX.Element {

return (
<div data-testid='Blogs-main-container'>
<BlogLayout post={post as unknown as IPosts['blog'][number]} navItems={posts.blog}>
{children}
</BlogLayout>
<ErrorBoundary>
<BlogLayout post={post as unknown as IPosts['blog'][number]} navItems={posts.blog}>
{children}
</BlogLayout>
</ErrorBoundary>
</div>
);
}
if (pathname === '/blog') {
return (
<div data-testid='Blogs-sub-container'>
<BlogContext.Provider value={{ navItems: posts.blog }}>{children}</BlogContext.Provider>
<ErrorBoundary>
<BlogContext.Provider value={{ navItems: posts.blog }}>{children}</BlogContext.Provider>
</ErrorBoundary>
</div>
);
}
const post = getPostBySlug(pathname);

if (post) {
return <GenericPostLayout post={post as unknown as IPosts['blog'][number]}>{children}</GenericPostLayout>;
return (
<ErrorBoundary>
<GenericPostLayout post={post as unknown as IPosts['blog'][number]}>{children}</GenericPostLayout>
</ErrorBoundary>
);
}

return <div className='min-h-screen bg-white dark:bg-dark-background transition-colors duration-300'>{children}</div>;
return (
<div className='min-h-screen bg-white dark:bg-dark-background transition-colors duration-300'>
<ErrorBoundary>{children}</ErrorBoundary>
</div>
);
}
Loading