The Problem with Traditional Pagination
Pagination is one of the most overlooked SEO challenges for large websites.
Why?
- Search engines struggle with deep crawlability – Traditional pagination structures often bury important content, making deep pages hard to reach.
- Users get frustrated – Clicking through endless "Next" buttons or dealing with poorly designed pagination UI isn't user-friendly.
We built an SEO-friendly pagination solution on our Strapi-powered blog at Deploi.ca/blog, which has:
- 4,100+ blog posts
- 83 paginated listing pages
This pagination system keeps deep content crawlable while providing a clean and intuitive user experience.
The Smart, SEO-Optimized Pagination Solution
Our pagination ensures that any listing page is reachable in just two clicks from the blog landing page while keeping the UI uncluttered.
How It Works
We use anchor pages and dynamic pagination updates instead of showing dozens of individual page links.
When a user or search engine lands on a paginated page, they see a progressive pagination structure that adapts dynamically.
Pagination Structure Example
On the Deploi blog, here’s the pagination you see on the first page:
Users can navigate to the next page by clicking the second links. If we expand the ellipsis, here are the rendered links available to both users and search engines:
Google has access to all the pagination links within the current range (1-9), as well as anchor pages, which we defined in increments of ten.
In this example, let’s say Google follows anchor page 30, the pagination will dynamically update to look like this:
As you can see in the screenshot above, both users and Google have access to the previous page, the next page, the first page and the last page.
But that’s not all, let’s reveal the first ellipsis by the left:
Now we can easily access all the previous anchor pages, which are pages 20 and 10.
By expanding the second ellipsis, we see that Google can crawl all the pages within the current range (30-39) and also access all the remaining anchor pages (40, 50, 60, 70, and 80):
If we click on page 37 for example, which is in the current range, pagination will update accordingly:
The shows that Google could reach the deepest listing page with only two link follows from the blog landng page and access any of the 4100+ blog posts with just three follows.
The Code Behind It
Our smart pagination system dynamically reveals relevant pages while keeping the UI intuitive. Let’s break down the key components of our React-based Pagination component.
Understanding the Props
Our Pagination component uses the following props:
pageCount
– Total number of pages.gotoPage
– Callback function to navigate to a different page.searchString
– Optional search query parameter.startUrl
– Base URL for page navigation.pageInde
– Current active page.
Managing State
We use React’s useState
and useEffect
hooks to handle page changes and dynamic visibility:
1const [activePage, setActivePage] = useState(pageIndex);
2const [showLeftPages, setShowLeftPages] = useState(false);
3const [showRightPages, setShowRightPages] = useState(false);
4
5useEffect(() => {
6 setActivePage(pageIndex);
7 setShowLeftPages(false);
8 setShowRightPages(false);
9}, [pageIndex]);
activePage
: Keeps track of the currently selected page.showLeftPages
/showRightPages
: Controls visibility of hidden anchor pages.useEffect
resets visibility states when the page index updates.
Generating Page Numbers
To create an SEO-friendly pagination structure, we generate page numbers dynamically:
This ensures:
- The first and last pages are always visible.
- The current page and its neighbors (previous/next) are included.
- Anchor pages at every 10th interval are displayed.
Handling Visibility
To avoid clutter, we only show pages based on specific conditions:
1const isVisible = (page) => {
2 if (page === 1 || page === pageCount) return true;
3 if (page === activePage - 1 || page === activePage || page === activePage + 1) return true;
4 if (showLeftPages && page < activePage) return true;
5 if (showRightPages && page > activePage) return true;
6 return false;
7};
- First and last pages are always visible.
- Previous, current, and next pages are always included.
- Clicking the left/right ellipsis reveals hidden anchor and current range pages.
Handling Page Clicks
When a user selects a page, we update the state and trigger the gotoPage
function:
1const handlePageChange = (page) => {
2 if (page >= 1 && page <= pageCount) {
3 setShowLeftPages(false);
4 setShowRightPages(false);
5 gotoPage && gotoPage(page);
6 }
7};
For ellipsis clicks:
1const handleLeftEllipsisClick = () => {
2 setShowLeftPages(true);
3 setShowRightPages(false);
4};
5
6const handleRightEllipsisClick = () => {
7 setShowRightPages(true);
8 setShowLeftPages(false);
9};
- Clicking the left ellipsis expands previous anchor pages.
- Clicking the right ellipsis expands future anchor pages.
Rendering the Pagination UI
Each page number is displayed dynamically:
1const renderPageNumbers = () => {
2 const pages = generatePageNumbers();
3 let lastRenderedPage = 0;
4
5 return (
6 <ul className="flex items-center justify-center gap-2 flex-wrap">
7 {pages.map((page, index) => {
8 const shouldShowLeftEllipsis =
9 !showLeftPages &&
10 ((page > 2 && page <= activePage && lastRenderedPage === 1) ||
11 (activePage >= 3 && activePage <= 9 && page === 2));
12
13 const shouldShowRightEllipsis =
14 !showRightPages &&
15 page < pageCount &&
16 page >= activePage &&
17 pages[index + 1] === pageCount;
18
19 const isPageVisible = isVisible(page);
20 lastRenderedPage = page;
21
22 return (
23 <React.Fragment key={page}>
24 {shouldShowLeftEllipsis && (
25 <li
26 onClick={handleLeftEllipsisClick}
27 className="cursor-pointer text-primary-100 w-[40px] h-[40px] flex items-center justify-center hover:bg-primary-50 transition-colors rounded-full border border-primary-100"
28 >
29 ...
30 </li>
31 )}
32 <li
33 className={`${!isPageVisible ? "hidden" : ""} cursor-pointer border border-primary-100 text-base font-semibold text-primary-100 rounded-full w-[40px] h-[40px] flex items-center justify-center hover:bg-primary-50 transition-colors ${page === activePage ? "bg-primary-100 text-white" : ""}`}
34 >
35 <a
36 href={
37 searchString
38 ? `${startUrl}?p=${page}&q=${searchString}`
39 : `${startUrl}?p=${page}`
40 }
41 className="w-full h-full flex items-center justify-center"
42 >
43 {page < 10 ? `0${page}` : page}
44 </a>
45 </li>
46 {shouldShowRightEllipsis && (
47 <li
48 onClick={handleRightEllipsisClick}
49 className="cursor-pointer text-primary-100 w-[40px] h-[40px] flex items-center justify-center hover:bg-primary-50 transition-colors rounded-full border border-primary-100"
50 >
51 ...
52 </li>
53 )}
54 </React.Fragment>
55 );
56 })}
57 </ul>
58 );
59};
- If needed, the left ellipsis (
...
) is displayed before hidden pages. - If needed, the right ellipsis (
...
) is displayed after the current range. - Page links dynamically update based on visibility logic.
Wrapping Everything Together
Finally, the component renders the pagination UI:
1return (
2 <div className="flex flex-col md:flex-row justify-center items-center">
3 {renderPageNumbers()}
4 </div>
5);
This ensures our pagination works across different screen sizes.
By structuring pagination this way, we ensure:
- Efficient crawling for search engines (deep content remains discoverable).
- User-friendly navigation is retained by hiding crawler-foucsed links behind ellipsis.
- Scalability for any large website (whether a blog, eCommerce store, or content-heavy platform).
This approach solves pagination SEO issues while maintaining a great user experience.
Want to implement this on your site? Get in touch with us at Deploi.ca.
Martin is the Director of Digital Strategy & Growth at Deploi. With 25+ years of building for the web, he helps brands grow through digital transformations and smart marketing. Outside of work, he enjoys hiking with his wife and kids and channeling his inner child on the field or court.