Shopify Builds>Three Ships>Tabbed multi-type search results page with faceted filtering

Tabbed Multi-Type Search Results Page With Faceted Filtering

One query, three kinds of answer: a search section that counts products, articles and pages before it renders any of them, tabs between them, and keeps a set of SKUs out of the grid entirely.

Shoppers search Three Ships for ingredients and concerns as often as for products, and Shopify returns one flat result set that cannot be told to leave a gift-with-purchase SKU out of it. The search body is Palo Alto vendor code with tabs per result type; our work on it is the tag-based product suppression, the facet configuration behind it and the promo blocks that render inside the grid.

Fact Strip

  • Client: Three Ships
  • Surface: Search
  • Templates served: 1 — the search template
  • Complexity: High
  • Attribution: Built on top of Palo Alto 5.8.0 by Presidio Creative, a paid premium theme. The tabbed section, the pagination ladder, the facet rail and the result-card snippets are the vendor's code. This is extension and configuration work on that section — the tag suppression, the live facet setup and the promo blocks — not authorship of the search template.
  • Status: Live, verified 2026-09-06
  • Evidence: Two code artifacts plus five client working records
  • Confidence: Moderate
  • Platform primitives: 4

The Problem

Three Ships sells skincare, and a skincare catalog is searched in two languages at once. People type product names, and they type ingredients, concerns and how-to questions — retinol, rosacea, "how to layer" — which on this storefront are answered by blog articles as often as by SKUs. One query has to return both kinds of thing in a way a shopper can separate rather than a single blended list. Underneath that sits a plainer requirement. Any catalog of this age carries items that exist for operations rather than for shoppers: gift-with-purchase SKUs, staged products, items held back for a launch. They are real products in Shopify's index, and a raw search result set surfaces every one of them.

The Constraint

Shopify's search object returns a single flat result set per request. It cannot be told to exclude a product by tag before the theme sees it, so any suppression has to happen inside the render loop — after the platform has already matched and counted. The vendor theme adds its own shape on top: each result type renders into its own tab container, and switching tabs repaints by re-fetching the section with a type parameter rather than holding every type in the DOM. That means any per-type behavior has to exist twice, once in the all-types grid and once in the single-type tab. And the theme's own infinite scroll is switched off in this storefront's settings, so paging runs through Liquid's paginate tag rather than the vendor's scroll loader.

What We Built

The live search body is search.liquid, roughly 1,800 lines of vendor Liquid wired in through the search template. It walks search.results once to pre-count products, articles and pages, then renders a tab navigation whose links carry the type they select. The theme's own JavaScript, registered against a search-template behavior, reads the current type and the available types off the section root and re-fetches with the type parameter when a tab is clicked.

Each grid variant is wrapped in {% paginate search.results by pagination_limit %}, where the limit is derived rather than configured — desktop row width times twelve — with a separate ladder for articles keyed off the article column count. Products render through the vendor's product-grid-item, articles through blog-post-grid-item, and pages through search-result-page.liquid, a text-only card with its own animation attributes. Facets and sorting come from search.filters and search.sort_options, rendered into a sticky bar plus a filter drawer, with a price filter that can be a range slider or fixed buckets.

Product results are gated by a single {%- unless item.tags contains 'hidden' or item.tags contains '#hidden' -%} around the card. That is a small piece of Liquid doing a large piece of merchandising: it is the line that keeps gift-with-purchase and staged SKUs out of search results, and it uses the same tag conventions the merchandising team already applies on the collection grids, so nobody has to learn a second vocabulary to hide something.

What is ours here. The tabbed architecture, the pagination ladders, the facet rail, the drawer and the card snippets are Presidio Creative's. Our identifiable contribution on this section is the product suppression described above, the live facet configuration the rail renders — products and articles on, page results off, fixed price buckets, the filter rail open by default — and the filter-triggered promo blocks that render inside this grid, which have their own page because they are their own mechanism. We are not claiming the search template.

Why This Way

The decision visible in this configuration is to stay on Shopify's own search index and Search & Discovery facets rather than bolt a hosted search product onto the storefront. For a catalog this size, native search means no crawl lag, no second copy of the product data, and filtered result URLs that render server-side.

Product visibility went to tags for the same reason: the merchandising team already hides items from collections that way, and one convention that works in two places beats a search-specific mechanism nobody remembers. The cost of that is structural and worth being clear about. Suppression is a render-loop convention rather than an index-level exclusion — the platform has already matched and counted a product by the time the theme decides not to draw it. The rendered cards and Shopify's own result count are therefore two different numbers, which is the price of not maintaining a second index.

Counting the three result types before rendering anything is what lets the section hide the tab bar entirely when only one type is enabled. On this storefront page results are turned off, so without that pre-count a shopper would be looking at a single tab labelling itself.

Why Not an App

The alternative here is a hosted search app, and it is a real one: exclusion at index time, synonyms, and ranking control are exactly what those products sell, and this build has none of them. What the store keeps instead is a single source of truth. There is no crawl or sync lag between what merchandising publishes and what search returns, no per-query cost, no external script on the results page, and no second product model to reconcile when a SKU changes. The catalog is not large enough, and the query mix not adversarial enough, to make ranking control worth taking on a synchronized index. If the day comes when it is, that is a conversation about a different architecture — not a setting.

Implementation Notes

  • The section root carries the search state as data attributes — whether a search ran, the available types, the current type and the drawer's default state — and the theme's registered behavior reads them from there.
  • pagination_limit is derived, not a setting: desktop products per row times twelve, with a separate 5/10/9/12 ladder for articles keyed off the article column count.
  • The live configuration on the search template runs products and articles on, page results off, fixed price buckets rather than a slider, and the filter layout open.
  • The vendor's infinite-scroll markup is emitted but its setting is unset in this storefront's configuration, so the classic paginated path is the one that runs.
  • Image sizes strings are computed in Liquid from the row density, the gutter widths and whether the filter rail is showing, so the srcset stays honest when a merchandiser changes grid density.
  • The number of requested types drives the current tab: more than one means the all-types view, otherwise the single requested type, so a deep link to an article search lands on the right tab.

Edge Cases

  • Products tagged hidden or #hidden are skipped in every grid pass, so the rendered card count can be lower than search.results_count.
  • Turning all three result types off short-circuits to the no-results block rather than rendering an empty tab shell around nothing.
  • A search returning no filters at all renders the no-results copy rather than an empty filter rail, so the page never shows controls with nothing to control.
  • Every tab link carries a <noscript> anchor and the sort and filter forms carry <noscript> submit buttons, so search still works with JavaScript disabled.
  • Price-range reset labels are money-formatted with or without the currency code depending on the store setting, and strip trailing decimal zeros for both the Canadian and United States markets.
  • Article cards build their animation anchor from the current page number plus the item index, so animations do not re-fire incorrectly after a page change.

Platform Primitives Used

  • Storefront search — the entire result set, its count and its type breakdown come from Shopify's own search; the section's job is presenting and gating it.
  • Storefront filteringsearch.filters and search.sort_options drive the sticky bar and the drawer, with the facet vocabulary configured in the admin rather than in the theme.
  • Section Rendering API — a tab switch re-fetches the section for the requested type instead of holding every result type in the DOM at once.
  • Liquid pagination — the paginate tag is what actually pages this storefront's results, with the per-page number derived from the grid density.

Where It Runs

One template, and it is the whole of it. This section is the entire /search body on the Three Ships storefront, in both the Canadian and United States markets, so every query on the store — header search, no-results link, external referral — lands on this page.

What This Demonstrates

Primary: On-site search, predictive search and external search services — the native end of that range, and the useful counterweight to the storefronts in this corpus that moved search off Shopify entirely. It is also an instance of filters and faceted navigation, configured rather than built.

How We Know

The search section and the page-result snippet were read out of the live theme, alongside the live search template configuration and five client working records from a search-bar optimization program that also covers filters sourced from metafields and merchandising follow-ups on grid density and result imagery. Those records tell us the scope was real and ours. They do not record who chose the tab architecture or the pagination ladder, because those arrived with the theme. Documented from the code; no client-side record of the decision survives. The line between vendor and Deploi on this page is drawn from what the code shows, and where it is uncertain we have claimed less.

Related Builds

The Buy-vs-Build Question

This page is the version of the site-search decision where the answer is "neither, yet": native search, native facets, and theme code for the two things the platform will not do. Where the threshold sits — the point at which ranking control is worth a synchronized index — is set out in the site search decision.

Does Your Search Page Know What You Sell?

If your search results include things no shopper should ever find, that is a fixable afternoon. Contact us today and let's look at what your search is returning.

More builds