# Infinite scroll SEO: load-more lists Google can still crawl

> Infinite scroll SEO means building the list as ordinary paginated pages first, then adding scrolling on top, so Google can reach every item without scrolling. The pattern, the code and how to check it.

Updated 2026-09-26 · Technical SEO · HTML version: https://getreport.app/guides/infinite-scroll-seo

Infinite scroll SEO is the work of making a list that loads more items as the visitor scrolls crawlable by search engines, and the answer is always the same: build the list as ordinary paginated pages with their own URLs first, then add the scrolling on top. Googlebot does not scroll or click "Load more", so items that only appear after one of those actions are invisible to it unless they also exist on a page it can request directly. This guide is for teams running shops, blogs, news feeds or directories with infinite scroll or a load-more button. It belongs to our guide to [JavaScript SEO and how crawlers handle scripts](https://getreport.app/guides/javascript-seo).

## Quick answer

- **Every batch of items needs its own URL**, such as `/shoes/?page=2`, that shows exactly that batch when requested directly.
- **Link the pages with real `<a href>` links** in the HTML, for example a "Next page" link or a load-more control that is a link underneath.
- **Update the address bar with the History API** as batches load, so refreshing or sharing lands on the right page.
- **Give each page a self-referencing canonical.** Do not canonicalise page 2 onwards to page 1.
- **Lazy-load when content enters the viewport**, not on a scroll event or click.
- Check the category page with the free [JavaScript SEO check](https://getreport.app/tools/js-rendering-check): the links to page 2 should be in the raw HTML column.

## Why infinite scroll hides content from Google

A typical infinite scroll works like this: the page loads the first 24 products, and a script listens for the visitor reaching the bottom, fetches the next 24 from an API and appends them. The URL never changes. There is no page 2 anywhere, only an API endpoint.

Google's renderer loads the page once and does not scroll or click. Google's [pagination and incremental loading documentation](https://developers.google.com/search/docs/specialty/ecommerce/pagination-and-incremental-page-loading) therefore asks for crawlable links to every part of the list rather than relying on the scrolling behaviour itself. In that setup, product 25 onwards exists only behind a scroll, and the only way Google can find those products is through other links: the sitemap, related products, other categories. Category pages exist to pass links to the products on them; with pure infinite scroll, most of that job is not done.

Other readers fare worse. Most AI crawlers do not run JavaScript at all, so they see only what is in the first HTML response. The difference between "loaded by scrolling" and "loaded on the first view" matters as well: content that loads as soon as it is visible is fine for Google, while content that waits for an action it never takes is not.

## The pattern: paginated pages underneath

Google's recommendation, in the same documentation and its 2014 post "Infinite scroll search-friendly recommendations" on the Search Central blog, is to put a paginated series under the scrolling list:

1. **Split the list into component pages.** `/shoes/` shows items 1–24, `/shoes/?page=2` items 25–48, and so on. Use absolute page numbers, not relative values such as `?after=yesterday`, and never a fragment such as `#page=2`, which Google ignores when it separates URLs.
2. **Make every page work on its own.** Request `/shoes/?page=3` with JavaScript off and you get items 49–72 in the HTML, a title that says "page 3" and links to the neighbouring pages.
3. **Link the pages in sequence** with `<a href>`, so crawlers can walk from page 1 to the last page.
4. **Enhance with JavaScript.** When the script loads a batch, it fetches the same items that page would show and updates the URL with `history.pushState()` or `history.replaceState()`.

The visitor still gets an infinite list. The crawler gets a normal paginated category. Both see the same items in the same order.

```html
<!-- In the server HTML of /shoes/?page=2 -->
<ul id="product-list">
  <li><a href="https://getreport.app/shoes/trail-runner-blue/">Trail runner, blue</a></li>
  <!-- … 23 more … -->
</ul>
<nav aria-label="Pagination">
  <a href="https://getreport.app/shoes/" rel="prev">Previous page</a>
  <a href="https://getreport.app/shoes/?page=3" rel="next" id="load-more">Load more shoes</a>
</nav>
```

## Load more: a link that becomes a button

A load-more control is the most robust form of infinite loading, because the visitor asks for more content and the footer stays reachable. Build it as a link to the next page, then let the script take over:

```js
const link = document.querySelector('#load-more');

link?.addEventListener('click', async (event) => {
  event.preventDefault();
  const url = new URL(link.href);
  const res = await fetch(url, { headers: { Accept: 'text/html' } });
  const doc = new DOMParser().parseFromString(await res.text(), 'text/html');

  // Append the next page's items and take over its "next" link.
  document.querySelector('#product-list').append(...doc.querySelectorAll('#product-list > li'));
  const next = doc.querySelector('#load-more');
  history.pushState({ page: url.searchParams.get('page') }, '', url);
  if (next) link.href = next.href;
  else link.remove();
});
```

Fetching the next page's HTML keeps the script and the paginated pages consistent by construction: there is only one source of truth for what page 3 contains. A JSON endpoint works too, as long as it returns exactly the same items.

To make it load automatically as the visitor scrolls, observe the link instead of waiting for a click:

```js
const observer = new IntersectionObserver((entries) => {
  if (entries.some((e) => e.isIntersecting)) link?.click();
}, { rootMargin: '600px' });
if (link) observer.observe(link);
```

`IntersectionObserver` fires when the element comes near the viewport. It is also the method Google's [lazy-loading guidance](https://developers.google.com/search/docs/crawling-indexing/javascript/lazy-loading) recommends for content, because it does not depend on scroll events.

### pushState or replaceState?

`pushState` adds a history entry per batch, so the back button steps through the pages; `replaceState` only updates the current entry, so back leaves the list. Either is acceptable for Google, which reads the paginated URLs and not the history stack. For visitors, `replaceState` usually feels better on a long feed, while `pushState` suits load-more buttons where each click is a deliberate step. Whichever you choose, the URL in the address bar should always open the batch the visitor is looking at.

## Canonicals, titles and indexing

- **Canonical:** each page points at itself. `/shoes/?page=2` has `<link rel="canonical" href="https://example-shop.hr/shoes/?page=2">`. Pointing all pages at page 1 tells Google that page 2 is a copy, and it may stop crawling the links on it.
- **Titles:** add the page number ("Running shoes – page 2"), so the pages are not duplicates of each other in titles.
- **Indexing:** do not `noindex` pages 2 and beyond. They are how Google reaches the older products and posts.
- **rel="prev" and rel="next":** Google stopped using them as an indexing signal in 2019. They do no harm, and other tools read them, so keep them if your platform emits them. The report lists them as information only.
- **Sort orders and filters:** paginate the default order only. Sorted and filtered variants of the same list are best canonicalised or kept out of the index; the guide on [faceted navigation and parameter URLs](https://getreport.app/guides/faceted-navigation-and-parameter-urls) covers the choice.

The guide on [pagination after rel=prev/next](https://getreport.app/guides/pagination-after-rel-prev-next) goes deeper into paginated series in WordPress, WooCommerce and Shopify.

## Lazy-loaded images and content

Lazy loading and infinite scroll are often confused. Lazy loading defers images or sections that are already part of the page until they near the viewport. It is safe for SEO when it uses the browser's native `loading="lazy"` or an `IntersectionObserver`, and when the image URL is in the HTML (`src` or `srcset`), not only in a `data-src` attribute a script swaps in on scroll.

Three rules keep it safe:

1. **Never lazy-load the first screen.** The hero or LCP image loads eagerly.
2. **Put the real URL in `src`** with `loading="lazy"`, or at least in a `<noscript>` fallback for script-based libraries.
3. **Load text immediately.** Descriptions, reviews and specifications belong in the HTML, not in a section that loads on scroll.

[Lazy loading done right](https://getreport.app/guides/lazy-loading-done-right) covers images and iframes in detail.

## Visitors, accessibility and performance

Infinite scroll has costs beyond SEO, which is one reason many shops choose a load-more button instead:

- **The footer becomes unreachable.** Contact details, shipping terms and the privacy link keep moving away. A load-more button avoids this.
- **Keyboard and screen reader users lose their place** when content is inserted without focus management. After a batch loads, move focus to the first new item or announce the change in an `aria-live` region.
- **The DOM keeps growing.** After ten batches a listing can have thousands of nodes, which slows interaction. Our guide to [DOM size](https://getreport.app/guides/dom-size-why-3000-nodes-is-a-problem) explains the limits; long feeds should recycle or remove off-screen items.
- **Returning to the list** after opening a product should restore the scroll position and the loaded batches, which works naturally when the URL carries the page number.

## How to check infinite scroll

Start with a category or feed that has more than one page of items.

> **Free tool:** [JavaScript rendering checker: raw vs rendered HTML](https://getreport.app/tools/js-rendering-check): Free JavaScript rendering checker: compare raw HTML with the rendered page and see which text, links, tags and structured data appear only after scripts run.

The check loads the page once without JavaScript and once in Chromium, without scrolling or clicking, which is how Googlebot behaves too. Compare the links in the two columns: the link to page 2 should be in the raw HTML, and the product links on page 1 should be in both. If the rendered column has no link to more items, crawlers cannot reach them from this page.

> **Check: The visible text is present without JavaScript.** Google renders JavaScript later and with a budget, so text that only appears after scripts run can be indexed late or not at all. Other search engines and link previews may never see it.
>
> 1. Serve the main content in the HTML (server-side rendering or static generation) and use JavaScript only to enhance it.
> 2. Check the difference in the technical detail; menus and widgets are fine, headlines and body copy are not.

Then test the paginated pages directly:

1. Open `/shoes/?page=2` in a private window with JavaScript disabled. It should show the second batch, not the first and not an empty list.
2. Run `curl -s "https://example-shop.hr/shoes/?page=2" | grep -o 'href="[^"]*page=3[^"]*"'` to confirm the next link is in the HTML.
3. Request a page beyond the end, such as `?page=999`. It should answer `404`, not an empty page with `200`.
4. In Google Search Console, URL Inspection on page 2 shows whether Google has crawled it and which canonical it chose.

## Common mistakes

- **An API-only feed** with no paginated URLs, so items beyond the first batch have no crawlable home.
- **Fragment URLs** (`#page=2`) that Google treats as the same page.
- **Page 2 showing page 1's items** when requested directly, because the server ignores the parameter and only the script knows about pages.
- **All pages canonicalised to page 1,** which hides the links on later pages.
- **A load-more `<button>` with no link underneath,** leaving crawlers nothing to follow.
- **Lazy-loaded text** that appears only after a scroll event.
- **Out-of-range pages answering `200`,** which creates endless empty pages for crawlers to find.

## Questions people ask

### Is infinite scroll bad for SEO?

Not when it sits on top of paginated pages. Google does not scroll, so items that only load after scrolling are invisible to it unless each batch also exists at its own URL, linked with ordinary `<a href>` links. Build the paginated pages first, make each one work without JavaScript, then add the scrolling on top and update the URL with the History API as batches load.

### Is a load-more button better than infinite scroll for SEO?

Neither is better on its own; both need paginated URLs underneath. A load-more control that is a real link to the next page is easier to make crawlable and accessible, and it keeps the footer reachable for visitors. Automatic infinite scroll suits endless feeds. In both cases Google follows the links to the paginated pages, not the button or the scrolling.

### Does lazy loading affect SEO?

Not when it is done with native `loading="lazy"` or an `IntersectionObserver` and the content loads as it becomes visible. Google's renderer does not scroll, but it does load content that enters the viewport this way. Problems appear with libraries that wait for scroll events, image URLs hidden in `data-src` attributes, and text that loads late. Never lazy-load the first screen or the main text.
