SEO for local service businesses and ecommerce stores in the USA (888) 400-5050
Ecommerce SEO

Managing JavaScript-based filters: ensuring content visibility in the rendered HTML

How Google renders script-driven filters, what it can and cannot see, and how to build and test filters that work for shoppers and crawlers.

Open laptop with code on the screen on a wooden desk

The problem: what shoppers see is not always what crawlers see

Modern ecommerce filters are rarely plain links. When a shopper ticks "waterproof" and the product grid updates without a page reload, a script has run in their browser: it has asked a server for matching products, replaced the contents of the page and, if the developer was careful, updated the address bar. It feels instant, and it is the standard way to build a filter interface. For search engines, though, it raises an awkward question. If the products only exist in the page after a script has run, and the filtered states only exist after someone clicks, what does a crawler actually see?

The answer depends on how the filters were built. Done well, JavaScript filters give shoppers a smooth experience and give search engines everything they need. Done badly, they hide products, hide links and leave valuable landing pages without addresses that crawlers can use. This guide explains how Google handles JavaScript, what it can and cannot see, how to build and test filters that work for both audiences and what to do when you inherit a system that does not. It is the final article in our series on faceted navigation SEO, and it matters especially for the pages we recommend creating in strategic indexing of filtered attributes, since those pages must be visible to be indexed.

Open laptop with code on the screen on a wooden desk
Filters built with scripts must still produce real links and real content in the page HTML.

How Google processes JavaScript

Google's documentation describes a three-stage process for pages that depend on JavaScript: crawling, rendering and indexing. First Googlebot fetches the HTML of a URL. Then, if the page needs JavaScript to show its content, the URL is queued for rendering, where a recent version of Chromium runs the scripts and produces the final page. Finally the rendered result is used for indexing. Google has said that the rendering step happens on a queue that may delay it, in most cases for a short time but sometimes longer.

Several consequences follow, and they explain most JavaScript SEO problems.

  • Google can render JavaScript, but it costs more. Pages that need rendering take longer to process, and heavy scripts can fail or time out.
  • Google does not interact with pages like a person. It does not click buttons, tick checkboxes, scroll to trigger loading or hover to reveal menus. Content that appears only after such an interaction is generally not seen.
  • Google follows links, not events. A link has to be an anchor element with an href attribute that contains a real URL for crawlers to treat it as a link. Script-driven click handlers on other elements are not links, however much they behave like them for a shopper.
  • Rendering uses a standard, stateless session. It does not keep cookies or local storage between requests, so content that depends on stored state may not appear.
  • Resources must be reachable. If robots.txt blocks the scripts, stylesheets or API endpoints needed to build the page, rendering may produce an empty or broken result.

Other search engines and AI systems differ, and many process JavaScript less capably than Google does. A page that depends heavily on script is therefore riskier across the wider ecosystem than it appears from Google's capabilities alone.

The common filter patterns and how each fares

PatternHow it worksSEO visibility
Server-rendered filter linksEach option is an ordinary link to a URL that returns a complete pageFully crawlable. Control is through canonical, noindex and robots rules
Script-enhanced linksLinks point to real URLs, and a script intercepts clicks to update the page without reloadingCrawlable, because the HTML contains real links and the URLs return full pages. This is progressive enhancement
History API with real URLsA script updates the grid and uses pushState to change the address to a real, server-supported URLGood, if each URL also works when requested directly and returns the full page
Fragment-based stateFilters change the part of the address after the hash signCrawlers generally ignore the fragment, so filtered states cannot be indexed. Good for noise, bad for landing pages
Pure client-side stateFilters update the grid with no URL change at allInvisible to crawlers, and also unshareable and awkward for shoppers
Buttons with click handlersElements that are not links trigger script actionsCrawlers do not click them, so the content behind them is not discovered
Third-party search and filter widgetsAn external service renders the listing, often inside the pageDepends on the service. Some render server-side, others rely on browser scripts

The pattern to aim for is progressive enhancement. The HTML delivered by the server already contains the products and real links for the filter options that should be crawlable. A script then improves the experience by updating the page smoothly. If the script fails, or a crawler ignores it, the underlying page still works.

What a crawler needs from a filtered page

Whatever technology you choose, a page you want indexed has to meet a short list of conditions.

  1. A real URL. The selection must have an address that returns that selection when requested directly, without any prior interaction.
  2. The products in the page. The product list, with names, links and prices, should be present in the HTML that Google renders. Ideally it is present in the initial HTML.
  3. Crawlable links to the page. At least one anchor with an href pointing at the URL must exist somewhere on the site.
  4. Standard head elements. A unique title, a canonical and, where appropriate, a robots directive, delivered consistently.
  5. A correct status code. A 200 for pages that exist, and a real 404 for combinations that do not.
  6. Accessible resources. No robots.txt blocks on the scripts, styles or data endpoints that the page needs to render.

If your landing pages meet those conditions, the details of how the grid updates are mostly your own business. If they do not, no amount of clever markup will rescue them.

Green and white source code on a dark monitor
What the browser builds and what a crawler sees can be different things.

Common failures and how to spot them

Links that are not links

A filter option built as a button, a span or an anchor without an href, with a click handler that runs the filter, is invisible to crawlers as a link. Inspect your filter markup. If the elements are not anchors with real addresses, crawlers will not follow them, and any landing pages that you want found must be linked in another way.

Content loaded after interaction

Products that appear only after a scroll event, a "load more" click or a tab selection may not be seen. Google does not scroll or click. For listings that you want indexed, make the first set of products part of the page that loads, and provide paginated URLs for the rest.

Empty initial HTML

Some single-page applications ship an almost empty HTML shell and build everything in the browser. Google can usually render these, but it takes longer, can fail and leaves other crawlers with nothing. Check what the raw HTML contains for a category page. If the product list is missing, consider server-side rendering or static generation.

Metadata set only by script

Titles, canonical tags and robots directives injected by JavaScript can be processed by Google, but they are fragile. Conflicts between the HTML and the rendered version, such as a noindex in the raw HTML that script later removes, are risky, because Google may act on the first version it sees. Keep the critical head elements in the server response.

Soft 404 pages

A filter combination with no results that still returns a normal status and a friendly "nothing found" message becomes a soft error. In a single-page application the server often returns 200 for every address. Make sure impossible combinations return a real 404, or at least carry noindex.

Blocked resources

A robots.txt rule that blocks the folder containing scripts or the endpoint that returns product data can leave Google with a page that renders without products. Allow the resources needed for rendering.

Fragments used for states you want indexed

Filters that put their state after a hash sign cannot produce indexable landing pages. If a selection deserves a page, it needs a path or a query string that the server understands.

Heavy scripts that slow rendering

Large filter libraries and search widgets can delay rendering, hurt interaction metrics and leave Google waiting. Responsiveness is also part of the experience that Core Web Vitals measure, as we describe in our Core Web Vitals guide.

Architecture choices for a filter system that works for search

Option 1: Server-side rendering with progressive enhancement

The server returns full pages for every address, including filtered ones, and a script enhances them. It is the most robust approach, because it does not depend on the crawler's rendering and works for every client. The cost is development effort, especially if the store was built as a client-only application.

Option 2: Hybrid, with static landing pages and script-driven filters

The whitelisted landing pages, from the strategy in our landing page guide, are server-rendered with clean URLs. All other combinations are handled by script, without producing crawlable addresses. This keeps crawlers away from the noise, while making sure the pages that matter are fully visible. For many large stores, it is the best balance.

Option 3: Client-side rendering with careful safeguards

If the store must remain a client-side application, pre-rendering or server-side rendering for the important routes, real links, real URLs through the History API, server-provided metadata and correct status codes bring it much closer to the other options. It requires discipline and testing.

About dynamic rendering

Dynamic rendering means serving crawlers a pre-rendered version and visitors the client-side version. Google has described it as a workaround, not a recommended long-term solution, because it adds complexity and the risk of the two versions drifting apart. If you already use it, check that the versions match. If you are choosing now, prefer server-side rendering or static generation.

URLs, the History API and fragments

When a filter changes the page without a reload, the address bar should still change, for several reasons. Shoppers can share, bookmark and return to the filtered view, the back button works as expected and, where the view is a landing page, crawlers can find it.

  • Use the History API (pushState and replaceState) to update the address to a real URL, not a fragment, for any state that should be a page.
  • Make sure the URL works on its own. Open it in a new window with no history. It should return the same selection, with a 200 status and a complete page.
  • Use fragments for states that must stay private to the browser, such as the open state of a panel or a transient sort, where you want crawlers to ignore them.
  • Keep the parameter policy consistent with the rules in our article on clean versus parameterized URLs: fixed order, standard format, no duplicates.
  • Update the head when the URL changes. If the view is meant to be a page, its title and canonical should match the new address, ideally supplied by the server for direct requests.

A pitfall to watch is the mismatch between what a script shows after a click and what the server returns for the same URL. If clicking "waterproof" shows 74 products but loading the address directly shows the whole category, the landing page has no real existence for a crawler. Always test the direct request.

How to test what Google sees

You do not need to guess. A handful of tests reveal what a crawler gets.

  1. View the raw HTML. Use view-source or a command-line fetch. Look for the product names and links. If they are not there, the page depends on rendering.
  2. Use the URL Inspection tool in Search Console. Run a live test and view the tested page. The rendered HTML, the screenshot and the list of resources that could not be loaded show what Googlebot produced. If the products are missing from the rendered HTML, you have found the problem.
  3. Disable JavaScript in your browser and reload. Whatever remains is what a non-rendering crawler sees. It is a quick, if rough, test of progressive enhancement.
  4. Check the links. Inspect the filter and navigation elements. Are they anchors with href attributes that contain real URLs? A crawler extracting links from the rendered HTML will find only those.
  5. Request each landing page directly in a fresh session, and confirm the status code, title, canonical and product list.
  6. Run a rendering crawl. Crawlers such as Screaming Frog and Sitebulb can render JavaScript and compare the raw and rendered versions of every page, highlighting differences in links, titles and content.
  7. Check blocked resources. The URL Inspection tool lists resources that Google could not load. Compare them with your robots.txt rules.
  8. Look at the search results. Search for a distinctive phrase from a filtered page in quotation marks, with a site operator, to see whether the content is indexed.

Repeat the tests after each release. A new version of a filter widget or a change in the build process can quietly alter what crawlers receive.

Performance, interaction and the shopper

JavaScript filters affect more than crawlers. They sit at the center of the shopping experience, and their performance shows in the Core Web Vitals, especially Interaction to Next Paint, which measures how quickly the page responds after a tap or a click.

  • Give instant feedback. When a shopper ticks a filter, show a loading state or update the interface at once, and complete the heavy work afterward.
  • Keep the filter code lean. Large libraries and heavy search widgets can add hundreds of milliseconds of blocking time. See our article on apps and Total Blocking Time for how to measure it.
  • Avoid layout shifts. Reserve space for the grid and for the filter panel, so that updates do not move content unexpectedly.
  • Debounce requests. If several filters change in quick succession, send one request, not many.
  • Cache responses for common combinations, so repeated selections are fast.
  • Handle failure gracefully. If the data request fails, show an error and allow a retry, not an empty grid.

A filter system that is fast and reliable for shoppers is also easier for search engines to process, because it carries less script and fewer failure points.

Laptop open to code at an angle in a dark workspace
Test the rendered HTML with Search Console's URL Inspection, not only your own browser.

Pagination, "load more" and infinite scroll

Listings that load more products on demand are a special case, because Google will not scroll or click. If the only way to reach product 25 is to scroll or press a button, crawlers will probably never see it.

  • Provide paginated URLs. Each page of results should have its own address, such as ?page=2 or /page/2/, reachable through real links, and return a full page when requested.
  • Layer the experience on top. If you want infinite scroll for shoppers, drive it from the same paginated URLs and update the address as the shopper moves through the pages with the History API.
  • Use self-referencing canonicals on each paginated page, and avoid pointing them all at page one.
  • Return a 404 for pages beyond the last one, to avoid creating an endless space, as described in our crawl traps article.
  • Link to the first few pages from the category, so that deeper products are not buried.

Using script to keep noise out of the crawl

The same techniques that can hide valuable pages can also protect you from crawl waste, if used deliberately. Filters whose states have no value as pages, such as multi-select combinations, sort orders and price sliders, are good candidates for script-only handling that creates no crawlable links.

  • Use buttons or form controls for options that should not be links, and update the grid through a script.
  • Keep the state in the fragment or in memory, so that no new crawlable address is created.
  • Do not duplicate the same option as both a link and a button. If a state should be a landing page, link to it. If it should not, do not.
  • Keep robots.txt and canonical rules in place as a safety net, in case a link slips into a template or is shared externally.

This is the hybrid model in practice: real links for the pages you want, script-only controls for everything else. It reduces the reliance on after-the-fact controls such as those in noindex versus disallow, though it does not remove the need for them.

Third-party search and filtering tools

Many stores use a search and filtering service, from a platform app to a dedicated search provider, that renders the product listing inside the page. These tools vary widely in how well they serve SEO. Ask the following before you adopt one, and test the answers yourself.

  • Does it support server-side rendering or provide an HTML version of the listing for crawlers?
  • Does it output real links, with proper URLs, for the combinations you want indexed?
  • Can you control which URLs it generates, and add canonical and robots directives?
  • Does it return correct status codes for empty or invalid combinations?
  • How much JavaScript does it add, and how does it affect interaction metrics?
  • Does it work when its scripts are slow or blocked?

Test a sample category in a rendering crawler and in the URL Inspection tool before you commit, and again after any major update. A tool that looks excellent in a demonstration may leave the product grid empty in the rendered HTML.

Notes for common front-end frameworks

Teams often ask whether a particular framework is "good for SEO". The honest answer is that frameworks are tools, and the outcome depends on how they are configured.

  • React, Vue and similar libraries render in the browser by default. Used alone, they produce the empty-shell problem. Their meta-frameworks, such as Next.js and Nuxt, support server-side rendering and static generation, which is the usual solution. Choose a rendering mode for each route deliberately.
  • Static generation suits landing pages and categories whose content changes slowly, and delivers fast, complete HTML. It needs a rebuild or incremental regeneration when stock changes, so check that prices and availability stay accurate.
  • Server-side rendering suits pages that change often, at the cost of server work for each request. Cache sensibly to protect performance.
  • Hydration costs. Server-rendered pages still need JavaScript to become interactive. Large bundles can leave the page looking ready and unresponsive, which hurts interaction metrics. Split code by route and load filter logic only where it is needed.
  • Routing libraries that handle navigation in the browser should still generate real anchors with href values. Check that links are rendered as anchors, not as clickable elements without addresses.
  • Head management. Use the framework's supported way to set titles, canonicals and robots tags on the server for every route, and test that they appear in the raw HTML.

Whatever the stack, the test is the same: request a page with no browser and see whether it contains what you want indexed.

Accessibility, usability and SEO point the same way

Building filters with semantic HTML helps everyone. Real links and buttons work with keyboards and screen readers, announce their purpose and behave predictably. Custom elements built from generic containers with click handlers are harder to use for people with disabilities and invisible to crawlers, which is a rare case where one change solves two problems.

  • Use anchors for navigation to a different page or selection and buttons for actions that change something in place.
  • Give filter groups labels and ensure that each option has a clear text name.
  • Announce updates, for example with a live region that states how many products match, so that assistive technology users know the grid has changed.
  • Keep focus management sensible after an update, so that keyboard users do not lose their place.
  • Make sure the filter panel works on small screens, with large tap targets.

When a development team asks why SEO cares about markup details, this is a persuasive answer: the same decisions that make a filter crawlable make it accessible and often make it simpler to maintain.

How to brief the development team

A clear brief prevents a lot of rework. Include the following.

  1. The goal in business terms: "Shoppers must be able to filter without page reloads, and the selections on our approved landing page list must be crawlable and indexable."
  2. The whitelist of landing page URLs, with the filters they correspond to.
  3. Required behavior for those URLs: server-rendered products, unique title and canonical in the response, a 200 status and a link from the category page using an anchor with an href.
  4. Required behavior for all other filter states: no crawlable links, or links that carry the agreed controls, and a fragment or in-memory state.
  5. Pagination and scroll rules: paginated URLs with real links, self-referencing canonicals and a 404 beyond the last page.
  6. Performance targets: an interaction response goal on mobile and no new layout shifts.
  7. Acceptance tests: raw HTML check, URL Inspection live test, direct-request test and rendering crawl for a defined sample of URLs.
  8. Do not: change robots.txt, add routes without review or inject critical metadata by script alone.

Agree the acceptance tests in advance, and run them together after each release. It turns a vague concern about "SEO-friendly filters" into a short, checkable specification.

Monitoring after launch

JavaScript behavior can change without anyone intending it, whether through a library update, a build configuration change or a new third-party script. Put simple monitoring in place.

  • Scheduled rendering crawls of a sample of category and landing pages, comparing raw and rendered content, links and metadata against a baseline.
  • Search Console checks: the Pages report for new exclusions, and the page experience and Core Web Vitals reports for regressions.
  • Alerts on key landing pages: a drop in impressions for a landing page can be the first sign that it has stopped rendering for crawlers.
  • Release gates: block a release that changes the head, routing or filter code unless the acceptance tests have passed.
  • Periodic manual spot checks in the URL Inspection tool for the most valuable pages.

Treat these checks as insurance. The failures they catch are usually invisible to the people who work on the site every day, because the pages look perfect in a browser.

A worked example: a store that lost its filtered pages

A fictional home goods retailer moves from a traditional platform to a headless build with a client-side filter system. Within a few months, organic traffic to its filtered landing pages, such as "ceramic dinner sets" and "stainless steel cookware sets", falls sharply, though the category pages hold up.

The investigation begins with the URL Inspection tool. For a landing page, the rendered HTML shows a category title and an empty grid. The raw HTML is an application shell with no products. The address for "ceramic dinner sets" is a hash fragment appended to the category address, so Google sees only the category. The filter options are buttons with click handlers, and the sitemap lists only the category.

The fix is a hybrid approach. The development team adds server-side rendering for category and landing routes, so that the products are in the initial HTML. The whitelisted combinations get clean paths, such as /dinner-sets/ceramic/, which return full pages with their own titles and canonicals. The category page links to them with ordinary anchors, and they join the sitemap. The remaining filters stay as script-only controls with fragment-based state, so no new crawlable URLs appear. Robots.txt is updated to allow the data endpoint that the scripts use.

Over the next two to three months, the landing pages are recrawled and reindexed, impressions return and then exceed their earlier level, and the team adds a rendering crawl and a direct-request test to its release checklist, so that the next front-end change cannot silently remove them again.

A release checklist for filter changes

  • The raw HTML of each category and landing page contains the product list and links.
  • Filter options that should be crawlable are anchors with real hrefs, and those that should not are not links.
  • Every landing page URL returns the same selection when requested directly in a fresh session.
  • Titles, canonicals and robots directives are in the server response and match the rendered page.
  • Impossible combinations return a real 404, or carry noindex.
  • Pagination uses real URLs with self-referencing canonicals, and pages beyond the end return a 404.
  • Robots.txt does not block scripts, styles or data endpoints needed for rendering.
  • The URL Inspection live test shows the products in the rendered HTML for a sample of pages.
  • A rendering crawl shows no unexpected differences between raw and rendered links and titles.
  • Interaction metrics have been measured on mobile, and no new layout shifts have appeared.

Common mistakes

  1. Assuming Google will click. It does not, so content behind interaction is not seen.
  2. Using buttons where links are needed. Crawlers follow anchors with real hrefs.
  3. Relying on fragments for indexable states. Crawlers ignore them.
  4. Shipping an empty HTML shell. Rendering is slower and less reliable than server-provided content.
  5. Injecting critical metadata with script only. Keep titles, canonicals and robots directives in the server response.
  6. Returning 200 for every route. Impossible combinations should be real 404 pages.
  7. Blocking required resources. Check robots.txt against the files and endpoints the page needs.
  8. Trusting a vendor's demonstration. Test the rendered output yourself.
  9. Ignoring performance. Slow filters hurt shoppers and can hurt rendering.
  10. Not retesting after releases. Front-end changes are the usual cause of silent regressions.

How we help

At AH SEO Agency we test filter systems the way a crawler meets them: raw HTML, rendered HTML, links, status codes and blocked resources. We work with your developers to choose an architecture that keeps your valuable landing pages fully visible and your noise out of the crawl, specify the URL and metadata behavior and set up the rendering checks for every release. See our technical SEO service, the faceted navigation SEO page or request a free SEO audit. You can also read the rest of this series, starting with noindex versus disallow.

Summary

  • Google renders JavaScript, but it does not click, scroll or hover, so content behind interaction and filters built as non-link elements are generally not discovered.
  • Aim for progressive enhancement: complete HTML and real links from the server, with scripts improving the experience.
  • Give every filtered state that should be a page a real URL that works when requested directly, with server-provided metadata and the right status code.
  • Use script-only controls and fragments for states that should not be pages, and keep canonical and robots rules as a safety net.
  • Provide paginated URLs behind "load more" and infinite scroll, and return a 404 beyond the last page.
  • Test with raw HTML, URL Inspection, rendering crawls and direct requests, and repeat after every release.
FAQ

Frequently asked questions

Can Google see content loaded by JavaScript filters?

Google can render JavaScript, so content present after scripts run on page load can be indexed. It does not click, scroll or hover, so content that appears only after an interaction is generally not seen.

Do filter options need to be real links?

For anything you want crawlers to discover, yes. Google follows anchor elements with href attributes that contain real URLs. Buttons and elements with click handlers are not treated as links.

Can I use URL fragments for filters?

Fragments, the part after the hash sign, are generally ignored by Google, so filtered states cannot be indexed. They are useful for states you want crawlers to ignore, and unsuitable for landing pages.

How do I test what Google sees on a filtered page?

Compare the raw HTML with the rendered HTML using the URL Inspection tool's live test, disable JavaScript in your browser, request the URL directly in a fresh session and run a rendering crawl.

Is dynamic rendering a good solution?

Google describes dynamic rendering as a workaround and not a long-term recommendation. Server-side rendering or static generation, with progressive enhancement, is a more robust choice.

Does infinite scroll work for SEO?

Only if paginated URLs sit behind it. Google does not scroll, so provide real, linked, paginated pages and update the address as shoppers move through them.

Not sure where your site stands?

Send us your website and we will tell you what we would fix first, including if SEO is not your best next step.