Skip to content

feat: add pagination to blog page#5123

Open
jayvaliya wants to merge 6 commits intoasyncapi:masterfrom
jayvaliya:feat/4938-blog-pagination
Open

feat: add pagination to blog page#5123
jayvaliya wants to merge 6 commits intoasyncapi:masterfrom
jayvaliya:feat/4938-blog-pagination

Conversation

@jayvaliya
Copy link

@jayvaliya jayvaliya commented Feb 9, 2026

Description

This PR implements client-side pagination for the blog page, addressing issue #4938. The implementation displays 12 posts per page with URL-based state management for shareability and browser history support.

Features Added

  • BlogPagination component with Previous/Next buttons and page number navigation
  • Smart page display with ellipsis for large page counts (e.g., 1 ... 4 5 6 ... 14)
  • URL-based pagination (/blog?page=2) for shareable links
  • Filter integration - pagination resets to page 1 when filters change
  • Invalid page handling - auto-redirects out-of-range pages to valid ones
  • Scroll to top when navigating between pages
  • Accessibility - ARIA labels and keyboard navigation support

Related issue(s)

Resolves #4938


🚀 Performance Improvements

This PR delivers significant performance gains by reducing the number of DOM nodes rendered at once:

⚡ Load Time Performance

  • 74% faster page load: 5.8s → 1.5s (4.3 seconds saved)
  • 100% LCP improvement: 2.51s → 0s (instant content visibility)
  • Zero layout shift: CLS 0.06 → 0.00 (perfect stability)

🎨 Rendering Performance

  • 89% faster rendering: 167ms → 18ms
  • 44% faster painting: 16ms → 9ms
  • 31% less main thread blocking: 584ms → 399ms

💻 JavaScript Efficiency

  • 42% reduction in scripting time: 749ms → 436ms
  • Document request latency reduced by 602ms

🌳 DOM Optimization

  • 30-65% fewer DOM nodes: ~7,338 → 2,621-5,125 nodes
  • 12-54% fewer event listeners: up to 1,641 → 744-1,444 listeners
  • Significantly reduced memory footprint and improved browser responsiveness

📈 Before/After Comparison

Metric Before (asyncapi.com) After (localhost) Improvement
Total Load Time 5.8s 1.5s ⚡ 74% faster
LCP 2.51s 0s ✅ 100%
CLS 0.06 0 ✅ Perfect
Rendering 167ms 18ms ⚡ 89% faster
Scripting 749ms 436ms ⚡ 42% faster
DOM Nodes ~7,338 ~3,873 avg 📉 47% fewer

🎯 User Experience Impact

  • Mobile users will experience dramatically faster load times
  • SEO benefits from improved Core Web Vitals scores
  • Scalability - performance remains consistent as blog grows
  • Better accessibility - reduced cognitive load with paginated content

📸 Performance Screenshots

Before (without pagination)

before

After (with pagination)

after

✅ Testing

  • All 11 Cypress E2E tests passing
  • Lint passes
  • Manual testing of pagination navigation
  • Manual testing of filter + pagination combination
  • Manual testing of invalid page URL correction

Summary by CodeRabbit

  • New Features

    • Added blog pagination UI with Previous/Next and page number buttons, highlights current page, and shows "Showing X to Y of Z posts"
    • Renders posts per-page and synchronizes page via URL; smooth-scrolls to top on page change; filter changes prune page param so filters reset pagination
  • Tests

    • Added end-to-end tests validating pagination controls, URL navigation, filter interactions, and scroll/aria behaviors

@netlify
Copy link

netlify bot commented Feb 9, 2026

Deploy Preview for asyncapi-website failed.

Built without sensitive environment variables

Name Link
🔨 Latest commit c96ddfc
🔍 Latest deploy log https://app.netlify.com/projects/asyncapi-website/deploys/698dba9fdee34e0008596734

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 9, 2026

📝 Walkthrough

Walkthrough

Adds client-side pagination: new BlogPagination component and type, integrates pagination into the blog index (slicing posts per page, page validation, shallow routing), resets page on filter changes, updates Filter to omit page when applying filters, and adds Cypress E2E tests for pagination and filter interactions. (≤50 words)

Changes

Cohort / File(s) Summary
Pagination Component & Types
components/navigation/BlogPagination.tsx, types/components/navigation/BlogPaginationProps.ts
New BlogPagination component and BlogPaginationProps interface. Implements getPageNumbers, Previous/Next controls, ellipses, aria attributes, data-testid markers, current-page highlighting, and onPageChange callback.
Blog Page Pagination Logic
pages/blog/index.tsx
Adds POSTS_PER_PAGE, computes totalPages and validCurrentPage, slices displayedPosts, adds handlePageChange (shallow router updates + scroll-to-main), synchronizes URL page param, and resets page when filters change via prevFiltersRef. Renders BlogPagination conditionally.
Filter Integration
components/navigation/Filter.tsx
Effect now clones routeQuery, deletes page, and passes the pruned query to onFilterApply so applying filters clears pagination page param.
E2E Tests
cypress/blog.cy.js
Adds end-to-end tests for pagination and filter interactions: pagination controls presence and behavior, navigation via page buttons and Next/Prev, disabled states, URL query assertions (page + filters), scroll-to-top on page change, and clearing filters removes page param.

Sequence Diagram

sequenceDiagram
    actor User
    participant Router as Next.js Router
    participant Blog as Blog Page
    participant Filter as Filter Component
    participant Pagination as BlogPagination

    rect rgba(100,150,200,0.5)
    Note over User,Pagination: Page navigation flow
    User->>Pagination: Click page number / Next / Prev
    Pagination->>Blog: onPageChange(page)
    Blog->>Router: router.push(url with updated page) (shallow)
    Router->>Blog: router.query updated
    Blog->>Blog: Recompute displayedPosts (apply filters → slice)
    Blog->>Pagination: Render updated pagination state
    Pagination->>User: Updated posts + pagination UI
    end

    rect rgba(150,100,200,0.5)
    Note over User,Filter: Filter apply flow
    User->>Filter: Apply filters
    Filter->>Blog: onFilterApply(query without `page`)
    Blog->>Router: router.push(filtered query) (shallow)
    Router->>Blog: router.query updated
    Blog->>Blog: Detect filter change, reset page → page=1, recompute displayedPosts
    Blog->>Pagination: Render page 1 with filtered posts
    Pagination->>User: Filtered posts shown from page 1
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 I hop through pages, quick and bright,
Twelve posts per leap, my path alight.
Filters cleared, the trail is new,
URLs tidy, and views stay true.
Hop on — the blog's a comfy bite!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: add pagination to blog page' is a concise and clear summary that directly reflects the main change: implementing pagination functionality on the blog page.
Linked Issues check ✅ Passed The PR fully addresses all coding requirements from issue #4938: 12 posts per page, URL-based pagination, BlogPagination component with Previous/Next/page controls, filter integration with page reset, invalid page handling, accessibility features (ARIA labels), and comprehensive Cypress E2E testing.
Out of Scope Changes check ✅ Passed All changes directly align with the linked issue #4938 objectives: new BlogPagination component, pagination logic in blog index, Filter.tsx modification to exclude page param, TypeScript types, and E2E tests. No unrelated modifications detected.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

No actionable comments were generated in the recent review. 🎉

🧹 Recent nitpick comments
pages/blog/index.tsx (2)

47-67: onFilter dependency on router is overly broad — may cause unnecessary re-creations.

The entire router object is listed as a dependency of useCallback. In Next.js, while the router reference is typically stable, accessing router.query and router.push inside the callback means the intent is to depend on query changes and the push method. Consider using router.query and router.pathname as dependencies (or extracting them) to make the dependency contract explicit and avoid potential re-creation on unrelated router state changes.

Proposed fix
   const onFilter = useCallback(
     // eslint-disable-next-line `@typescript-eslint/no-unused-vars`
     (data: IBlogPost[], _query: FilterType) => {
       setPosts(data);

       const currentQuery = { ...router.query };

       delete currentQuery.page;
       const currentFilters = JSON.stringify(currentQuery);

       if (router.query.page && prevFiltersRef.current && currentFilters !== prevFiltersRef.current) {
         const queryParams = new URLSearchParams(currentQuery as Record<string, string>).toString();
         const url = queryParams ? `${router.pathname}?${queryParams}` : router.pathname;

         router.push(url, undefined, { shallow: true });
       }

       prevFiltersRef.current = currentFilters;
     },
-    [router]
+    // eslint-disable-next-line react-hooks/exhaustive-deps -- only react to query/pathname changes
+    [router.query, router.pathname]
   );

110-119: Page-validation effect could loop if posts.length fluctuates.

If filters cause posts to change (reducing totalPages and thus validCurrentPage), the router.replace on line 117 updates the URL which triggers the effect again. While the rawPage !== validCurrentPage guard should stabilize it, the router dependency makes it sensitive to any router state change. Consider using router.replace via a ref or removing router from the dependency array with an eslint-disable comment explaining the intent.

Based on learnings, in this repository when intentionally omitting dependencies in React hooks, add an eslint-disable comment with an explanatory note.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@asyncapi-bot
Copy link
Contributor

asyncapi-bot commented Feb 9, 2026

⚡️ Lighthouse report for the changes in this PR:

Category Score
🔴 Performance 41
🟢 Accessibility 98
🟢 Best practices 92
🟢 SEO 100
🔴 PWA 33

Lighthouse ran on https://deploy-preview-5123--asyncapi-website.netlify.app/

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@pages/blog/index.tsx`:
- Line 85: Currently showClearFilters uses Object.keys(router.query) so a bare
pagination key like page makes the button appear; change the logic to ignore the
pagination key by computing the query minus 'page' (e.g., destructure { page,
...rest } = router.query or filter out 'page') and set showClearFilters =
Object.keys(rest).length > 0 (or filter out keys === 'page' and empty values).
Update the expression that defines showClearFilters to exclude 'page' so the
Clear filters button only shows when real filters are present.
- Around line 47-49: The eslint-disable-next-line comment is suppressing the
wrong line; move the directive so it targets the unused parameter `_query` in
the onFilter callback: adjust the comment placement for the useCallback/onFilter
declaration so it sits immediately before the parameter or convert it to an
inline disable on the `_query` parameter (affecting the onFilter function using
useCallback and the `_query: FilterType` parameter) to only suppress the
unused-var rule for that parameter.
🧹 Nitpick comments (2)
cypress/blog.cy.js (1)

1-67: Good baseline E2E coverage for the happy path.

A few gaps worth noting for future improvement:

  • No test for the last page — verifying that the Next button is disabled on the final page.
  • No test for invalid page values (e.g., ?page=999, ?page=-1, ?page=abc) — the PR objectives mention invalid-page handling/redirect, but it's not covered here.
  • No assertion on the number of rendered posts — e.g., verifying that exactly 12 BlogPostItem elements appear on a full page.
  • Tests like line 53 (/blog?page=3) assume ≥ 36 posts exist; these will break if the post count drops below that threshold.

None of these are blockers, but they'd strengthen confidence in the pagination logic.

pages/blog/index.tsx (1)

48-67: Filter-change detection via JSON.stringify is order-sensitive.

JSON.stringify(currentQuery) comparison (line 55 vs prevFiltersRef.current) can produce false positives if query-parameter order differs between navigations (e.g., user editing the URL bar). The only consequence is an unnecessary page reset to 1, so it's not critical — but using a sorted-keys comparison or shallow object equality would be more robust.

@@ -58,13 +84,40 @@ export default function BlogIndexPage() {
};
const showClearFilters = Object.keys(router.query).length > 0;
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

"Clear filters" button appears when only ?page=N is in the URL.

showClearFilters is true whenever router.query has any keys, including just page. A user on /blog?page=2 with no active filters will see "Clear filters" — clicking it navigates to /blog which resets pagination but is misleading since no filters are active.

Consider excluding page from the check:

Proposed fix
- const showClearFilters = Object.keys(router.query).length > 0;
+ const showClearFilters = Object.keys(router.query).some((key) => key !== 'page');
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const showClearFilters = Object.keys(router.query).length > 0;
const showClearFilters = Object.keys(router.query).some((key) => key !== 'page');
🤖 Prompt for AI Agents
In `@pages/blog/index.tsx` at line 85, Currently showClearFilters uses
Object.keys(router.query) so a bare pagination key like page makes the button
appear; change the logic to ignore the pagination key by computing the query
minus 'page' (e.g., destructure { page, ...rest } = router.query or filter out
'page') and set showClearFilters = Object.keys(rest).length > 0 (or filter out
keys === 'page' and empty values). Update the expression that defines
showClearFilters to exclude 'page' so the Clear filters button only shows when
real filters are present.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@pages/blog/index.tsx`:
- Around line 186-190: Replace the unstable array index key on BlogPostItem with
a stable identifier: change the key used in the map over displayedPosts from the
numeric index to post.slug (i.e., in the JSX where displayedPosts.map((post,
index) => <BlogPostItem key=... post={post} />) use key={post.slug}); ensure
post.slug is unique for each post and fallback to a stable id only if necessary
to prevent duplicate keys.
- Around line 52-62: The query construction is incorrectly casting router.query
to Record<string,string> which corrupts array-valued params; add a helper
function buildQueryString(obj: Record<string, string | string[] | undefined>)
that creates a URLSearchParams by iterating Object.entries and for each key if
the value is an array append each element, if it's a string append it, then
return params.toString(); replace the four problematic spots (the onFilter block
using currentQuery, the handlePageChange usage, the pagination validation that
builds a URL, and the usage in components/navigation/Filter.tsx around
newQuery/currentQuery) to call buildQueryString(currentQuery) or
buildQueryString(newQuery) instead of new URLSearchParams(... as
Record<string,string>).toString().
🧹 Nitpick comments (3)
pages/blog/index.tsx (3)

110-119: Page-validation effect has router in its dependency array — risk of redundant router.replace calls.

The router object reference can change on every render in the Next.js Pages Router. Combined with router.replace itself updating the router, this effect may fire more than necessary. While the rawPage !== validCurrentPage guard prevents an infinite loop, the extra evaluations are wasteful.

Consider using router.pathname or extracting a stable reference via useRef instead of depending on the entire router object. Alternatively, the eslint exhaustive-deps rule may have forced this — if so, a suppression comment explaining why would be appropriate here.


97-101: scrollIntoView fires before the shallow route transition commits.

router.push returns a Promise that resolves after the transition. Calling scrollIntoView synchronously afterward means it executes before React re-renders with the new page's content (display of new posts, pagination update). This usually "works" because the target element already exists, but if the content height changes significantly between pages, the scroll position may be off.

Proposed fix — scroll after transition
-    router.push(`${router.pathname}?${queryParams}`, undefined, {
+    router.push(`${router.pathname}?${queryParams}`, undefined, {
       shallow: true
-    });
-    document.getElementById('main-content')?.scrollIntoView({ behavior: 'smooth' });
+    }).then(() => {
+      document.getElementById('main-content')?.scrollIntoView({ behavior: 'smooth' });
+    });

47-49: ESLint suppression lacks an explanatory comment.

The eslint-disable is correct but doesn't explain why the parameter is unused. A brief note helps future readers understand this is intentional (the _query parameter is required by the Filter component's callback signature but not needed here).

-    // eslint-disable-next-line `@typescript-eslint/no-unused-vars`
+    // eslint-disable-next-line `@typescript-eslint/no-unused-vars` -- _query is required by Filter's onFilter signature but unused here

Based on learnings, in the asyncapi/website repository, when you intentionally omit dependencies in React hooks, add an eslint-disable comment with an explanatory note to justify the design choice.

Comment on lines +52 to +62
const currentQuery = { ...router.query };

delete currentQuery.page;
const currentFilters = JSON.stringify(currentQuery);

if (router.query.page && prevFiltersRef.current && currentFilters !== prevFiltersRef.current) {
const queryParams = new URLSearchParams(currentQuery as Record<string, string>).toString();
const url = queryParams ? `${router.pathname}?${queryParams}` : router.pathname;

router.push(url, undefined, { shallow: true });
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# Find the file and check its structure
find . -name "index.tsx" -path "*/pages/blog/*" 2>/dev/null

Repository: asyncapi/website

Length of output: 412


🏁 Script executed:

# Get the file content around the specified lines
cat -n pages/blog/index.tsx | sed -n '45,70p'

Repository: asyncapi/website

Length of output: 382


🏁 Script executed:

# Check for handlePageChange function and line 95 context
cat -n pages/blog/index.tsx | sed -n '90,105p'

Repository: asyncapi/website

Length of output: 412


🏁 Script executed:

# Search for all URLSearchParams usages in the file
rg -n 'URLSearchParams' pages/blog/index.tsx -C 3

Repository: asyncapi/website

Length of output: 390


🏁 Script executed:

# Check the router.query usage pattern
rg -n 'router\.query' pages/blog/index.tsx -C 2

Repository: asyncapi/website

Length of output: 1699


🏁 Script executed:

# Search for filter-related code and how filters are built
rg -n 'onFilter|handleFilter|filter' pages/blog/index.tsx -A 5 -B 2

Repository: asyncapi/website

Length of output: 1325


🏁 Script executed:

# Check if there's any multi-value query parameter usage
rg -n '\?.*=.*&.*=' pages/blog/index.tsx
rg -n 'Array\.isArray.*query' pages/blog/index.tsx

Repository: asyncapi/website

Length of output: 42


🏁 Script executed:

# Search for query builder patterns
rg -n 'type.*=' pages/blog/index.tsx

Repository: asyncapi/website

Length of output: 369


🏁 Script executed:

# Check the broader context of how filters are created and passed
cat -n pages/blog/index.tsx | sed -n '65,92p'

Repository: asyncapi/website

Length of output: 929


🏁 Script executed:

# Verify if Next.js router types indicate string | string[]
rg -n 'NextRouter|ParsedUrlQuery|router:' pages/blog/index.tsx -B 2 -A 2

Repository: asyncapi/website

Length of output: 42


🏁 Script executed:

# Check imports and type definitions
cat -n pages/blog/index.tsx | head -30

Repository: asyncapi/website

Length of output: 1584


🏁 Script executed:

# Find the Filter component to see how it builds query parameters
find . -name "Filter*" -type f | grep -E "\.(tsx?|jsx?)$"

Repository: asyncapi/website

Length of output: 320


🏁 Script executed:

# Check the applyFilter helper to understand filter structure
find . -name "*applyFilter*" -o -name "*filter*" | grep -E "helpers|utils" | head -10

Repository: asyncapi/website

Length of output: 95


🏁 Script executed:

# Search for how query parameters are constructed with filters
rg -n 'router\.push.*query\|router\.replace.*query' -A 3 -B 3

Repository: asyncapi/website

Length of output: 42


🏁 Script executed:

# Look for multi-valued query parameters in tests or documentation
rg -n 'type=.*&type=' pages/blog/
rg -n 'tags=.*&tags=' pages/blog/

Repository: asyncapi/website

Length of output: 42


🏁 Script executed:

# Check how Filter component might add array values to query
rg -n 'append|push.*query' pages/ --type=tsx -A 2 -B 2

Repository: asyncapi/website

Length of output: 87


🏁 Script executed:

# Create a test to verify URLSearchParams behavior
python3 << 'PYTHON_EOF'
from urllib.parse import urlencode, parse_qs

# Test 1: Direct object with array
test_obj = {'type': 'a', 'tags': ['tag1', 'tag2']}
print("Test 1: Object with array value")
print(f"  Object: {test_obj}")

# When URLSearchParams gets an array as string, it calls toString()
import json
print(f"  Array toString(): {json.dumps(test_obj['tags']).replace('"', '')}")

# Test 2: How to properly handle arrays
params_list = []
for key, value in test_obj.items():
    if isinstance(value, list):
        for v in value:
            params_list.append((key, v))
    else:
        params_list.append((key, value))

print(f"  Proper approach: {urlencode(params_list)}")

# Test 3: What happens with cast
print("\nTest 2: Simulating cast behavior")
# When you cast array to string in URLSearchParams context
stringified = str(test_obj['tags'])  # This becomes "['tag1', 'tag2']"
print(f"  Array stringified: {stringified}")
print(f"  URL encoded: {urlencode([('tags', stringified)])}")

PYTHON_EOF

Repository: asyncapi/website

Length of output: 308


🏁 Script executed:

# Check Next.js documentation on router.query to confirm array behavior
rg -n 'string\[\]|Array.*query|ParsedUrlQuery' --type=ts

Repository: asyncapi/website

Length of output: 3901


🏁 Script executed:

# Check the Filter component to see how it constructs query params
cat -n components/navigation/Filter.tsx

Repository: asyncapi/website

Length of output: 3399


🏁 Script executed:

# Check applyFilter helper
cat -n components/helpers/applyFilter.ts

Repository: asyncapi/website

Length of output: 5824


🏁 Script executed:

# Look for how query is updated from Filter component
rg -n 'router\.push.*filter\|router\.query.*filter' components/navigation/ -B 3 -A 3

Repository: asyncapi/website

Length of output: 42


🏁 Script executed:

# Verify actual URLSearchParams behavior with Python
python3 << 'PYTHON_EOF'
from urllib.parse import urlencode

# Test how URLSearchParams in JavaScript would handle an object with array
# Simulating JavaScript URLSearchParams behavior
test_cases = [
    ("Case 1: string value", {"type": "a"}),
    ("Case 2: array as string", {"type": ["a", "b"]}),
]

print("=== URLSearchParams behavior ===\n")

for label, obj in test_cases:
    print(f"{label}")
    print(f"  Input: {obj}")
    
    # This is what happens when JS calls toString() on array
    if isinstance(obj["type"], list):
        array_str = str(obj["type"])  # JavaScript: ['a', 'b'].toString() = 'a,b'
        print(f"  Array.toString() result: {array_str}")
        # Then URLSearchParams would encode this
        encoded = urlencode({"type": array_str})
        print(f"  URLSearchParams result: {encoded}")
        print(f"  (Incorrect - should be: type=a&type=b)\n")
    else:
        encoded = urlencode({"type": obj["type"]})
        print(f"  URLSearchParams result: {encoded}\n")

print("=== How to fix it ===")
print("When array values exist, append them separately:")
obj_with_array = {"type": ["a", "b"], "page": "1"}
params = []
for key, val in obj_with_array.items():
    if isinstance(val, list):
        for v in val:
            params.append((key, v))
    else:
        params.append((key, val))
print(f"  Result: {urlencode(params)}")
PYTHON_EOF

Repository: asyncapi/website

Length of output: 463


🏁 Script executed:

# Check if the Filter component actually creates multi-valued query params
cat -n components/navigation/Filter.tsx | head -100

Repository: asyncapi/website

Length of output: 3399


Fix URLSearchParams handling of array-valued query parameters in three locations.

router.query values can be string | string[] (e.g. ?type=a&type=b{ type: ['a','b'] }). The casts to Record<string, string> cause URLSearchParams to call .toString() on array values, producing corrupted query strings like type=%5B%27a%27%2C+%27b%27%5D instead of type=a&type=b.

This affects:

  • pages/blog/index.tsx line 52 (onFilter callback)
  • pages/blog/index.tsx line 95 (handlePageChange)
  • pages/blog/index.tsx line 115 (pagination validation)
  • components/navigation/Filter.tsx line 81

Create a helper to properly handle array values:

Proposed fix — helper to safely build query strings
+// Place near the top of the file or in a utils module
+function buildQueryString(query: Record<string, string | string[] | undefined>): string {
+  const params = new URLSearchParams();
+  for (const [key, value] of Object.entries(query)) {
+    if (value === undefined) continue;
+    if (Array.isArray(value)) {
+      value.forEach((v) => params.append(key, v));
+    } else {
+      params.append(key, value);
+    }
+  }
+  return params.toString();
+}

Then replace all four instances of the problematic pattern with buildQueryString(currentQuery) or buildQueryString(newQuery) accordingly.

🤖 Prompt for AI Agents
In `@pages/blog/index.tsx` around lines 52 - 62, The query construction is
incorrectly casting router.query to Record<string,string> which corrupts
array-valued params; add a helper function buildQueryString(obj: Record<string,
string | string[] | undefined>) that creates a URLSearchParams by iterating
Object.entries and for each key if the value is an array append each element, if
it's a string append it, then return params.toString(); replace the four
problematic spots (the onFilter block using currentQuery, the handlePageChange
usage, the pagination validation that builds a URL, and the usage in
components/navigation/Filter.tsx around newQuery/currentQuery) to call
buildQueryString(currentQuery) or buildQueryString(newQuery) instead of new
URLSearchParams(... as Record<string,string>).toString().

Comment on lines +186 to +190
<ul className='mx-auto mt-12 grid max-w-lg gap-5 lg:max-w-none lg:grid-cols-3'>
{displayedPosts.map((post, index) => (
<BlogPostItem key={index} post={post} />
))}
</ul>
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# Find IBlogPost interface definition
fd . -type f -name "*.ts" -o -name "*.tsx" | xargs rg -l "interface IBlogPost"

Repository: asyncapi/website

Length of output: 232


🏁 Script executed:

# Search for BlogPostItem component
fd . -type f -name "*.tsx" | xargs rg -l "BlogPostItem" | head -5

Repository: asyncapi/website

Length of output: 437


🏁 Script executed:

# Check post.slug usage to see if it's a stable identifier
rg "post\.slug" --type ts --type tsx -B2 -A2

Repository: asyncapi/website

Length of output: 87


🏁 Script executed:

# Examine the BlogPostItem component for nested anchor tags
fd -name "BlogPostItem.tsx" -exec cat {} \;

Repository: asyncapi/website

Length of output: 290


🏁 Script executed:

# Find IBlogPost interface
rg "interface IBlogPost" -A 20

Repository: asyncapi/website

Length of output: 42


🏁 Script executed:

# Read BlogPostItem.tsx
cat components/navigation/BlogPostItem.tsx

Repository: asyncapi/website

Length of output: 5868


🏁 Script executed:

# Check pages/blog/index.tsx for context around line 186-190
sed -n '180,195p' pages/blog/index.tsx

Repository: asyncapi/website

Length of output: 773


🏁 Script executed:

# Search for where post.slug is used to verify it exists
rg "\.slug" | head -20

Repository: asyncapi/website

Length of output: 2111


Replace array index with post.slug as the React key in pagination.

Using array indices as keys breaks reconciliation when navigating between pages. When the user moves from page 1 to page 2, displayedPosts contains entirely different posts, but React reuses keys 011, attempting to patch existing DOM nodes instead of remounting the components. This causes stale content and styling issues. Use the stable post.slug identifier instead.

Proposed fix
-                  {displayedPosts.map((post, index) => (
-                    <BlogPostItem key={index} post={post} />
+                  {displayedPosts.map((post) => (
+                    <BlogPostItem key={post.slug} post={post} />
                   ))}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<ul className='mx-auto mt-12 grid max-w-lg gap-5 lg:max-w-none lg:grid-cols-3'>
{displayedPosts.map((post, index) => (
<BlogPostItem key={index} post={post} />
))}
</ul>
<ul className='mx-auto mt-12 grid max-w-lg gap-5 lg:max-w-none lg:grid-cols-3'>
{displayedPosts.map((post) => (
<BlogPostItem key={post.slug} post={post} />
))}
</ul>
🤖 Prompt for AI Agents
In `@pages/blog/index.tsx` around lines 186 - 190, Replace the unstable array
index key on BlogPostItem with a stable identifier: change the key used in the
map over displayedPosts from the numeric index to post.slug (i.e., in the JSX
where displayedPosts.map((post, index) => <BlogPostItem key=... post={post} />)
use key={post.slug}); ensure post.slug is unique for each post and fallback to a
stable id only if necessary to prevent duplicate keys.

@codecov
Copy link

codecov bot commented Feb 11, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 100.00%. Comparing base (b1e7a01) to head (c96ddfc).

Additional details and impacted files
@@            Coverage Diff            @@
##            master     #5123   +/-   ##
=========================================
  Coverage   100.00%   100.00%           
=========================================
  Files           22        22           
  Lines          796       796           
  Branches       146       146           
=========================================
  Hits           796       796           

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@jayvaliya jayvaliya force-pushed the feat/4938-blog-pagination branch from b0b24ad to 5fa7356 Compare February 11, 2026 18:28
- Add BlogPaginationProps type definitions
- Create reusable pagination component with:
  - Previous/Next navigation buttons
  - Page number buttons with ellipsis for large counts
  - 'Showing X to Y of Z posts' text
  - Accessibility attributes (aria-label, aria-current)
  - Test IDs for E2E testing
- Add POSTS_PER_PAGE constant (12 posts)
- Extract current page from URL query params
- Calculate pagination values from filtered posts
- Add handlePageChange for URL-based navigation
- Redirect invalid page numbers to valid pages
- Reset page to 1 when filters change
- Scroll to top when changing pages
Prevents pagination query param from being treated as a filter,which
would cause empty results when navigating directly to apaginated URL
- Test pagination controls visibility
- Test page navigation via buttons and page numbers
- Test Previous/Next button disabled states
- Test scroll to top behavior
- Test direct URL navigation
- Test filter + pagination URL params
- Test Clear filters functionality
@jayvaliya jayvaliya force-pushed the feat/4938-blog-pagination branch from 5fa7356 to c96ddfc Compare February 12, 2026 11:33
@sonarqubecloud
Copy link

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: To Be Triaged

Development

Successfully merging this pull request may close these issues.

[FEATURE] Add pagination to blog page

2 participants