Programmatic SEO on Shopify comes down to three moving parts: a data source (metaobjects, metafields, or a spreadsheet you push through the Admin API), one Liquid template that renders that data, and a URL for every row. Shopify server-renders Liquid, so there's no JavaScript indexing problem. The hard part isn't the build. It's deciding which 300 of your 1,400 possible combinations deserve a URL, and holding the other 1,100 back.
We've built these sets for catalogue brands in India and the GCC, and the failure mode is always the same. Not a technical error. Someone generated a page for every fabric × occasion × city, published 1,400 URLs in a week, and six months later 90% of them had never received a single click. Google's scaled content abuse policy exists precisely for that pattern, and even without a manual action, a set of near-duplicate pages will just sit unindexed while your genuinely useful pages get crawled less often.
Where the URLs can actually live on Shopify
Shopify won't give you arbitrary URL paths. You get four realistic homes for bulk pages, and the choice constrains everything downstream:
- Collections (/collections/handle). The strongest option when the page's job is to list products, because you inherit sorting, pagination and the collection object. Automated collection rules let one condition set fill the page as inventory changes. Downside: a collection with a rules mismatch shows zero products and you won't notice.
- Pages with an alternate template (/pages/handle). Create a template like page.city-guide.json, then set template_suffix on each page record when you import. This is the workhorse for anything that isn't a product list — size guides, city delivery pages, comparison pages, occasion guides.
- Metaobject entries with the storefront web page capability turned on. Shopify generates the route and a metaobject/<type> template. Cleanest data model of the lot: one definition, typed fields, entries you can bulk-import and validate. Check the entry in admin for the exact path it produces before you commit to it in internal links.
- Filter URLs (?filter.p.…). Not a home. These are query strings, they're not reliably indexed, and you shouldn't try to force them to be. If what you actually need is better navigation on a big catalogue rather than 300 landing pages, install a filter and search app such as FilterPro and skip the whole exercise. That's a cheaper, faster win than a programmatic set and it's the honest recommendation more often than you'd think.
Tag-based collection URLs (/collections/handle/tag) get canonicalised back to the parent collection by Shopify. People still try to build programmatic sets on them. Don't.
Do the demand arithmetic before the template
Pick your axes, then kill most of the grid. A typical apparel setup: 14 fabrics × 6 occasions × 12 cities is 1,008 combinations. Pull search volume for all 1,008 (Keyword Planner in bulk, or a rank tracker's keyword list import). In every set we've done, the distribution is brutal — a couple of dozen combinations with real volume, a long middle with 10-40 monthly searches, and a tail with nothing at all.
Here's the calculation worth doing. Take an existing collection page of yours that ranks and check its numbers in GA4. Say it pulls 40 organic sessions a month, converts at 1.6%, AOV ₹2,400. That's 40 × 0.016 × 2,400 = ₹1,536 a month. Now be realistic about the tail: assume the median programmatic page gets 6 sessions, not 40. 6 × 0.016 × 2,400 = ₹230 per page per month. Across 300 pages that's ₹69,000 a month, or roughly ₹8.3 lakh a year, and it takes four to six months to get there.
Against that, the build. The template is 20-30 hours if it's doing anything interesting. The data model and import pipeline, another 10-15. Copy and QA is where it explodes: 300 pages at 15 minutes of human editing each is 75 hours. Call it 130 hours. At a blended ₹1,500 an hour that's ₹1.95 lakh, and that's before the ongoing maintenance. The maths works. It works much less well at 30 pages, and it stops working entirely if you skip the 75 hours of editing, which is exactly what most people skip. Our Shopify development cost breakdown has the wider ranges if you're budgeting.
The quality gates we run before a programmatic page gets published
These are pass/fail, checked in the import script where possible so a bad row never reaches production.
- Minimum inventory: four in-stock products. Below that the page is a dead end and the bounce is instant. Under four, we render it with a noindex tag and keep it out of internal links until stock recovers.
- Unique visible text, and not the LLM-shuffled kind. Our floor is around 40% of the page's visible words unique to that URL, and the unique part has to be information, not adjectives. Price range from live variants. Count of products. Actual delivery windows for that pincode. A size chart that differs by fabric. If the only difference between two pages is the noun in the H1, you have one page, not two.
- The query has one answer, and this page is it. Two combinations that resolve to the same answer get merged, with a redirect from the loser. We usually merge 10-15% of the grid at this stage.
- Every page has something a competitor's page doesn't. Usually structured data you already own and nobody bothered to surface: making charges, GST split, FSSAI details on food SKUs, COD availability by region, care instructions.
- A human reads a random 20. Not the 20 you're proud of. Random sample, roughly 7% of the set, read end to end on a phone. If two of the twenty are embarrassing, the whole batch goes back.
- Two inbound internal links minimum, in crawlable HTML. Covered below, because it's the part that gets skipped.
We get the unique-content threshold wrong on the first pass about a third of the time. The template looks varied in preview because you're checking three entries with rich data, then you scroll to row 260 and half the fields are empty and the page collapses into boilerplate. Sort your data by field completeness and QA from the bottom.
Internal linking is what separates 300 indexed pages from 300 orphans
Shopify's sitemap will list every published page, and Google will crawl a chunk of them once. Whether it keeps them depends almost entirely on internal links, because a page that only exists in a sitemap reads as low priority and behaves like it.
What works:
- A hub page per axis, linked from the main nav or the footer, listing every child page with descriptive anchor text. One hub per fabric, not one hub for all 300.
- Sibling links inside the template. On a fabric × city page, link the same fabric in three nearby cities and the same city across two other fabrics. Six to eight sibling links, rendered from the same metaobject list, is enough. Forty is a link farm.
- Links back up from product pages. If a product qualifies for a programmatic page, the product template should link to it. This is the single highest-value link you can add and it's usually two lines of Liquid over a metafield lookup.
- Breadcrumbs with BreadcrumbList JSON-LD, so the hierarchy is machine-readable rather than implied.
What doesn't: a giant HTML sitemap page with 300 links and no context, links injected by JavaScript after load, and "related pages" widgets that pick at random on every render so no link is stable.
Duplicate content, canonicals, and cannibalising yourself
The duplication risk isn't between your programmatic pages and someone else's site. It's between your programmatic pages and your existing collections. If you already rank for "silk sarees" with a collection, and you publish a programmatic page targeting "silk sarees online", you've split your own signals for no gain. Map the new set against your current GSC query data first and cut anything that overlaps an existing top-10 position.
Self-referencing canonicals on every page, always. Use noindex rather than a robots.txt disallow for pages you want held back, because a disallowed URL can't be crawled, so Google never sees the noindex and the URL can still surface. Drive it off a boolean metafield, so switching a page off is a data change, not a code deploy:
One more thing that catches people. Shopify's automatic sitemap doesn't offer per-page exclusion, so noindexed pages will keep appearing in it. That's ugly but harmless. If you need real control, a custom sitemap route through robots.txt.liquid pointing at a page template that outputs XML is possible, but it's rarely worth the maintenance.
Publish in waves, not all at once
We ship the first 40-50 pages, then stop for six to eight weeks. What you're looking for in GSC: are they indexed, are they getting impressions on the query you targeted, and is the average position moving or flat at 40+. Flat at 40 after two months on a low-competition term means the template is thin, and building the other 250 will just multiply the problem.
Watch the crawl stats report too. If Google's daily crawl requests spike and then your product pages start taking longer to get recrawled, you've made the trade in the wrong direction. At 300 URLs on a store with a few thousand products, that's unlikely. At 3,000, it's a real risk.
Speed matters more here than on a normal page, because programmatic templates tend to fire extra Liquid loops and metafield lookups. A template that does three nested loops over collections to build sibling links can add 400-600ms to server response time, and it'll be worse on the pages with the most data. Cache what you can into a single metaobject field at import time rather than computing it on every render.
What breaks in month three
Data drift. Products get unpublished, tags get renamed by whoever is doing merchandising that week, and a page that had eleven products has two. Nothing alerts you. Build a weekly job that flags any programmatic page under the inventory threshold, or at minimum a Shopify admin report you check on the first of the month.
Theme updates. A programmatic template lives in your theme. If someone duplicates the live theme, updates it from the theme store and publishes, your custom template can go with it. Keep the template files in version control and treat the theme as code, not as a setting. If you don't have that discipline in-house, that's a good reason to bring in a developer who does.
And copy rot. The "delivery in 2-3 days" line you hardcoded across 300 pages in October is wrong by the festive peak, when your 3PL is running five days late in tier-2 cities. Anything time-sensitive should come from a single metaobject field, referenced everywhere, editable in one place.
When to not do this at all
If your catalogue has fewer than about 200 SKUs, there aren't 300 distinct queries to serve and you'll be manufacturing pages for demand that doesn't exist. If you have no proprietary data — no pricing feed, no location data, no spec sheets — you have nothing to fill the unique 40% with, and you'll fall back on generated prose, which is worse than not publishing. And if your existing collection pages don't rank, fixing those is a better use of the same 130 hours. A collection with a proper H1, an intro that answers the query, and clean filters will usually outperform six thin programmatic pages aimed at the same intent.
Start with the grid, not the template. Export your possible combinations to a sheet, pull volume for all of them, and see how many survive a floor of 20 monthly searches plus four in-stock products. If the answer is 60 rather than 300, build 60. That's still a good project. If you want a second pair of eyes on which combinations are worth the build, a store audit is the fastest way to get there, or read how we approach SEO and content for Shopify catalogues.

