Non-JS Crawler Support for Product Listings¶
Status
This page documents a reference technique — a pattern already implemented and validated on a comparable Flooris storefront. It is not yet designed or built for Zwerfkei. It is documented here as the technical baseline for the outstanding action item described on the Product Listing Overview organism.
The problem¶
The Zwerfkei Product Listing Page (category pages, search results, special collections) renders its product grid exclusively client-side, through Vue InstantSearch inside a VueJS component. On page load:
- The server returns HTML for the page shell (header, sidebar structure, breadcrumbs, static content).
- The browser downloads and executes the Vue application bundle.
- The Vue component mounts.
- Only after mounting does Vue InstantSearch fire an asynchronous XHR request to Algolia.
- Algolia responds; Vue renders the product cards into the DOM.
Any device or crawler that does not complete step 2–5 sees a raw HTML response with no product links at all:
| Consumer | Executes JS? | Waits for async Algolia response? | Sees product links in raw HTML? |
|---|---|---|---|
| Modern browser (real user) | ✅ | ✅ | ✅ (after client-side render) |
| Googlebot (evergreen Chromium, full render budget) | ✅ | ✅ (usually) | ✅ (after render, but at extra crawl-budget cost) |
| Googlebot under tight render budget / large site | ✅ | ⚠️ Not guaranteed | ⚠️ Risk of an empty snapshot |
| Bing, GPTBot, PerplexityBot, most non-Google crawlers | ❌ / limited | ❌ | ❌ |
| Link-discovery-only crawlers, accessibility tools, "reader" modes | ❌ | ❌ | ❌ |
Why this matters
If the raw HTML never contains a <a href="..."> to a Product Detail Page (PDP), crawlers that don't run JavaScript (or don't wait for the async round-trip) cannot discover the product catalog through category pages at all. This is a link-discovery problem, not just a rendering/UX problem — it can suppress indexing of the entire catalog for those crawlers, regardless of how good the eventual rendered page looks to a browser.
The technique: a server-rendered fallback grid with a JS-driven swap¶
The pattern solves this with progressive enhancement: render a real, minimal product grid server-side, and let JavaScript replace it with the rich Algolia experience once (and only once) real results have actually arrived.
sequenceDiagram
actor Crawler as Crawler / Browser
participant Server as Backend (SSR)
participant DOM as Page DOM
participant Vue as VueJS component
participant Algolia as Algolia (InstantSearch)
Crawler->>Server: GET /category-page
Server-->>Crawler: HTML incl. server-rendered product grid
(real hrefs + JSON-LD)
Note over Crawler: Non-JS / non-waiting crawler stops here.
Product links are already in the DOM.
opt Only for clients that execute JS
DOM->>Vue: Vue mounts
Vue->>Algolia: async XHR search request
Algolia-->>Vue: search results
Vue->>DOM: render real Algolia hit cards
Vue->>DOM: detect real hit cards present
Vue->>DOM: hide server-rendered grid (display: none)
end
Key design decisions¶
| Decision | Rationale |
|---|---|
| Server-rendered grid uses the same data source and page size as the Algolia index (not a separate/divergent query). | Keeps SEO-visible content and user-visible content in parity — avoids "showing crawlers a different site" concerns. |
The server grid is hidden only once real hit cards are detected in the DOM (via a MutationObserver on the component root), not simply "on mount". |
Vue InstantSearch's results arrive asynchronously after mount — hiding on mount risks a flash of an empty grid between hiding the fallback and the real results arriving. |
| If the Vue mount fails, or Algolia returns no results, the observer simply never fires. | Fail-open: the server-rendered grid stays visible and fully functional — progressive enhancement, not a hard dependency on JS working. |
Visibility swap uses direct DOM manipulation (style.display), not a CSS class relying on a global "JS booted" cloak attribute. |
A global cloak mechanism (display: block triggered by an ancestor's mount-attribute) can silently override a CSS Grid layout with a block layout — the swap must be scoped and must not fight the fallback grid's own layout. |
Product links, including per-variant colour swatch links, are real <a href> elements, not <noscript>-only content. |
<noscript>-only links are a known-fragile pattern: some crawlers don't parse <noscript> content as equivalent to real content, and it previously caused unrelated rendering issues on comparable pages. Real DOM links, kept in sync with the client-side experience, are the safer baseline. |
Pagination is plain ?page=N query-string pagination with real <a href> links per page, independent of the client-side InstantSearch routing state. |
Guarantees every page of results — not just page 1 — is reachable via crawlable links, satisfying link-discovery for large catalogs. |
Every listed item carries Product structured data (JSON-LD), nested inside the page's existing ItemList. |
Lets search engines associate price/availability with each listed product directly from the crawlable HTML, independent of client-side rendering. |
Functional breakdown¶
1. Server-rendered grid¶
- Built from the same underlying dataset used to populate the Algolia index (same category resolution logic, same page size), so the crawlable snapshot matches what a JS-executing visitor eventually sees.
- Renders enough product information to be useful on its own: brand, name, size range, price, and colour variants — not just a bare name + link.
- Deliberately excludes product images to keep the fallback lightweight; images are not required for link discovery or for the structured data fields actually used.
- Every colour variant is its own real, individually crawlable link (
?color=...), following the same URL pattern used elsewhere on the PDP for colour-variant links.
2. Visibility swap¶
- The fallback grid is not wrapped in a generic "hide until JS boots" cloak. It is deliberately kept visible until the actual Algolia results are present in the DOM.
- Detection happens by observing DOM mutations on the component's own root element and checking for the presence of real hit-card elements — not by relying on component refs that may not exist yet, since the search-results child component only populates its slot once the search engine has connected and returned a (possibly empty) initial state, which happens asynchronously after the parent component's own mount lifecycle.
- This ordering detail matters: a naive implementation that checks a ref once, on mount, will silently and permanently fail to ever hide the fallback grid's replacement trigger, because the ref doesn't exist yet at that point in time.
3. Pagination¶
- Uses a plain, 1-based
?page=Nquery-string parameter for the server-rendered grid, decoupled from whatever page-state routing the client-side search library manages internally. - Each page carries the correct
<link rel="canonical">(self-referential, including?page=Nfor pages beyond the first), plus<link rel="prev">/<link rel="next">where applicable — omitted on the first/last page respectively. - A JS-executing user who lands on a deep, indexed
?page=NURL and then hydrates into the client-side experience may see the client-side search state reset to its own paging — an accepted trade-off, since the primary audience for this query parameter is non-JS crawlers, and the fallback grid is superseded the moment real client-side results render anyway.
4. Structured data¶
Productstructured data is nested inside the page'sItemList, one entry per listed item, sourced from the same dataset as the visible grid (not a separately maintained query) — so position numbers and content stay correct across pagination (e.g. page 2 correctly starts at position 33, not 1).- Only fields that are genuinely visible on the fallback grid are included (name, offer price, currency). Fields for content that isn't rendered on the fallback card (e.g. an image, a long description) are deliberately omitted — adding them would violate the "don't mark up content that isn't visible to readers of the page" principle from Google's structured data policies.
SEO impact¶
What this technique buys you
- Link discovery independent of JS. Any crawler — regardless of JS support or render budget — can follow real
<a href>links from every category page to every PDP, and across every paginated page of results. - No crawl-budget tax. Search engines that can execute JS no longer need to spend rendering budget just to discover that a link exists; the link is already present in the initial HTML response.
- Resilience. Because the swap is fail-open, a broken JS deploy or a slow/failing Algolia response degrades gracefully to a fully functional, linkable, real HTML product grid — instead of a blank page.
- Richer SERP eligibility. Per-item
Productstructured data makes each listed product eligible for Google's product snippet enhancements, without over-claiming content that isn't actually visible.
What it does not solve by itself
- It does not replace the need for correct canonical/prev/next tags on the paginated, client-side experience itself.
- It does not fix any pre-existing data-parity gaps between the fallback dataset and the live search index (e.g. a hardcoded availability assumption that also exists elsewhere in the codebase would still need a real stock signal to be fully accurate).
- It is a mitigation for link discovery, not a substitute for validating actual render outcomes — a Search Console URL inspection and a non-JS crawl (e.g. Screaming Frog with JS rendering disabled) are still the way to confirm it works end-to-end on a live environment.
Recommended validation once implemented¶
- Crawl the PLP templates with a non-JS user agent (e.g. Screaming Frog, JS rendering disabled) and confirm every listed product resolves to a real, crawlable PDP link, across all paginated pages.
- Confirm the raw HTML response contains no
<noscript>-only product links — links must exist in the regular DOM. - Validate the JSON-LD
Product/ItemListoutput with Google's Rich Results Test. - Confirm pagination tags (
canonical,prev,next) are correct on the first page, an intermediate page, and the last page. - Confirm the fallback grid is visually hidden (no layout regression, no flash of duplicate/empty content) once real Algolia results render for JS-executing visitors.
- Baseline Search Console index coverage and crawl frequency for the PLP/PDP templates before rollout, to measure impact after deploy.