Skip to content
Module 06 of 09 · Advanced

Why Your Dispensary Menu Is Invisible to Google

Why an embedded dispensary menu never reaches the index, the age gate that quietly blocks the crawler, the filter bar that eats your crawl budget, and the diagnostic order that names the cause in twenty minutes.

Why Your Dispensary Menu Is Invisible to Google
Key takeaway

Your menu is almost certainly not in the index, and nothing in Search Console will say so, coverage reports the page as indexed either way. The fix is rarely rebuilding the menu. It is fixing the age gate, stopping the filter bar generating URLs, and building a dozen real category pages the platform cannot take with it.

Open a dispensary website and the menu is the largest thing on it. Four hundred products, live stock, prices that update through the day. It is the reason the site exists, it is what the customer came for, and it is very often completely absent from what Google received.

Not penalised. Not ranking badly. Absent. And nothing in Search Console will tell you, because there is no error report for “the page rendered, but the part you cared about was not in it”. Coverage says indexed.

The page loads perfectly in your browser. The only symptom is that you never appear for a product search, and that symptom is indistinguishable from ordinary poor rankings, which is why this goes unnoticed for years on sites that are otherwise well run.

This is the technical module, and it is the last of the six. It covers what happens between your menu platform and Google’s index, the age gate that quietly breaks it, the filter bar that eats your crawl budget, and the diagnostic order that gets you from “something is wrong” to one specific cause in about twenty minutes.

It is the layer under everything else in cannabis retail SEO: none of the previous five modules matter on a page a crawler cannot read.

The searches your menu should be winning

Before the mechanics, the size of the thing. It is worth knowing what is actually at stake here, because “the menu is not indexed” sounds abstract until you look at the queries it costs you.

Six queries. Four are product searches with local and buying intent, and each is currently answered by the menu platform, a competitor blog, the brand site or a directory rather than by the dispensary. Only the two plain brand and city searches belong to the store.
Four of these six have local intent, buying intent, and an answer sitting in your menu. None of the four is yours.

Product-plus-place searches are the highest-intent queries in this industry. Somebody searching a specific product with a place name attached has decided what they want and is deciding where to get it. That is a customer three minutes from a purchase, and your menu holds the answer.

There is a second edge to it that owners find harder to hear. Where the menu platform’s own domain ranks for those searches, that traffic is real and it is not yours.

You are paying for the inventory integration and somebody else is holding the search result it earns. That is not the platform behaving badly; it is the natural outcome of the architecture, and it is fixable only by changing the architecture.

Three ways a menu gets onto a page

Everything downstream depends on which of three integrations you have, and most people running a dispensary site do not know which one it is. It is a five minute question with a permanent answer.

An iframe serves the menu from the provider domain, so the products belong to that domain. A client side script injects them into your page after load, so they may or may not be rendered. A server side integration puts them in your HTML, where they are indexed and attributed to you.
Three integrations, three completely different outcomes. Two of them take ten minutes to install, which is why almost everyone is on them.
  • An iframe is a second document embedded in your page, served from the provider’s domain. The products in it are their document, on their URL. For planning purposes treat that content as belonging to them entirely: it is not on your page in any way that helps you, and no amount of surrounding optimisation changes that.
  • A client-side script is better in principle. The products end up on your domain, in your document, injected by JavaScript after the page loads. Whether Google ever sees them is a separate question with a genuinely uncertain answer, which the rendering section deals with.
  • A server-side integration pulls the products through an API at request time and prints them into the HTML before it leaves your server. Google sees them in the first fetch, attributes them to you, and there is nothing to go wrong. It also takes a developer and an ongoing maintenance commitment, which is exactly why almost nobody has it.

Most dispensary sites are on the first two, and the reason is not ignorance. It is that both are a ten-minute install with a support article, and the third is a project.

The point of knowing which you have is not to feel bad about it. It is that the fix is different for each, and half of the fixes people attempt are for a problem they do not have.

Four minutes to know which one you have

You can settle this now, on any dispensary site, without access to anything.

Search the page source for a product name. If it is absent, search the rendered document object model. If it is still absent, look for an iframe pointing at another domain. Finally, use URL Inspection in Search Console, which is the only view showing what Google actually received.
Four checks, in order. Each one eliminates a possibility, and the fourth is the only ground truth.

View source, the actual source rather than the inspector, and search it for a product name you can see on the page. Present means the products are in the HTML and you are on a server-side integration; that is the good outcome and you can skip most of this module. Absent means keep going.

Open the element inspector and search the rendered document for the same product name. Present now, absent in the source, means a script built it after load. Still absent means look for an iframe tag pointing at another domain, which will be sitting exactly where the menu appears.

Then the step nobody does, and it is the only one that settles anything: URL Inspection in Search Console, live test, and read the rendered HTML tab. Your browser is not Googlebot.

It has your cookies, your consent state, your location, a warm cache and no crawl budget.

A menu that renders perfectly for you can be entirely missing from what Google actually got, and the rendered HTML tab is the only place that difference is visible.

If you want the crawler-side view without Search Console access, counting how many of your products are actually reachable gets you to the same answer from the outside.

Two further symptoms worth knowing, because they show up before anyone runs a diagnosis. A page that renders 400 products but reports a suspiciously low word count in any crawler is on an iframe or a script.

And a site where an embedded menu never reaches the index at all will typically rank fine for its own name and nowhere for anything it sells, a pattern distinctive enough to diagnose from a rank report alone.

The age gate that also gates the crawler

Module two covers the age gate as a penalty risk. This section is the narrower question of which gate designs leave a crawler with a page and which leave it with nothing.

Deal with it before rendering, because it sits upstream of everything and it is the single most common cause of a cannabis site being invisible for reasons nobody can find.

A redirect to a gate page, a server side content swap, a cookie wall and a user agent branch all leave the crawler with the gate rather than the page, and the user agent branch is cloaking as well. Only an overlay drawn over content that is already in the HTML satisfies the regulation and stays indexable.
Five builds. Four of them hand the crawler a gate. One of them is also cloaking.

A gate that redirects every URL to a verification page gives the crawler the verification page, every time, for every URL on the site. A server-side swap that replaces the page HTML with gate HTML leaves nothing to index.

A cookie wall that requires a confirmation cookie before serving content fails for the same reason a crawler has no cookies.

All three are common, all three are usually built by a developer who was told to comply with a regulation and did.

The fourth build is the one to look hardest for. Somebody notices the rankings have gone, works out that the gate is the cause, and makes the gate skip for Googlebot. It works. Rankings return.

It is also, by definition, serving different content by user agent, which is cloaking, and it converts an indexing problem into a manual action, which is module two’s subject and a considerably worse month.

How a gate built for a regulator ends up hiding the page is worth reading before you inherit a site rather than after.

There is exactly one build that satisfies both requirements. Serve the complete page in the HTML. Draw the gate over it as an overlay, applied by a script after load, blocking interaction until confirmed.

A visitor cannot read the page until they confirm their age. A crawler, which does not run your gate script in a way that blocks it and does not need to click anything, receives the page.

Age gating requirements differ by state and change, so confirm what yours actually requires before you build. What no rule of that kind asks for is that the page be hidden from an index, and conflating those two is where this goes wrong.

What Google actually does with a JavaScript page

Google indexes a JavaScript page in two passes. It crawls and indexes the raw HTML first, then renders the page later from a queue, and only after that second pass can anything a script built be indexed at all.

The client-side script case needs its own model, because “Google can render JavaScript now” is true and is also the source of most of the wrong conclusions in this area.

The crawl fetches raw HTML and indexes whatever was in it. Rendering happens later from a queue that can take minutes, days, or never run. Only then can script-injected products be indexed. Anything already in the raw HTML survives both passes.
Two passes, and the second one runs from a queue. Everything in the raw HTML skips the queue entirely.

Indexing happens in two passes. The crawl fetches raw HTML and indexes what is in it. Rendering happens later, from a queue, when resources allow. Only after that second pass can anything a script built be indexed. Google is clear that rendering happens; what it does not promise is when, or that a given render will succeed.

For a menu specifically that delay is a problem of its own, separate from whether rendering works at all. Prices and stock change daily. Render queues do not run daily on most sites.

So even in the successful case, the menu Google holds is a snapshot from some days ago, and the gap between the indexed menu and the live one is permanent rather than transient.

What survives both passes without any of this uncertainty is anything in the raw HTML: your headings, your local copy, your questions, your structured data.

Which is the same conclusion module three reached from the content side and this module reaches from the technical side.

The writing around the menu is the part that has to carry the page, because it is the part that is definitely there.

Six ways rendering fails without an error anywhere

When the render does not produce the menu, it fails quietly. These are the six causes worth checking, roughly in order of how often I find them.

A blocked script in robots.txt, a render timeout from chained API calls, fragment routing, a consent or geolocation wall, lazy loading that requires scrolling, and a different response served to Googlebot. Coverage still reports the page as indexed, so the only symptom is that product queries return nothing.
None of these produce an error. Coverage says indexed, the page loads for you, and the products are gone.
  • A blocked resource is the most common and the easiest to fix. A blanket disallow on a scripts directory, added years ago for reasons nobody remembers, means the script that draws the menu is never fetched. The render then completes successfully with no menu in it, which is not an error condition as far as anything is concerned.
  • A render timeout comes next. Menu widgets often chain requests: config, then categories, then products, then inventory. Each round trip adds latency. Rendering does not wait indefinitely, and a widget that takes eight seconds on a warm cache in your browser may not complete at all in a render that has no cache and no patience.
  • Fragment routing is architectural. If categories and products live behind a hash rather than a path, there is no distinct URL for anything, so there is nothing to index separately no matter how well it renders.
  • Consent and geolocation walls withhold content until a condition is met that a crawler never meets.
  • Lazy loading tied to scroll withholds content until an event that never fires.
  • A different response for Googlebot, which at this point is usually accidental, whether an edge rule, a bot-management setting or a caching layer, is still cloaking regardless of intent.

The filter bar and the crawl trap

The opposite problem, and it frequently sits on the same site. Where the menu is invisible, the filters around it are hyper-visible, and they generate more URLs than the site has products.

Eight categories, twenty four brands, six effects, three strain types, four price bands and five sort orders combine into just under seventy thousand URLs, each linked from the filter bar. Crawl budget goes to near duplicates, most of which return no products, and real pages are recrawled less often as a result.
Six filters. Sixty-nine thousand crawlable URLs, most of which return nothing at all.

The arithmetic is unforgiving because it is multiplicative. Six filters with a modest number of values each produce tens of thousands of combinations, every one of which is a link in the filter bar, and therefore every one of which is discoverable.

Sort order is the most galling contributor: it multiplies the entire set by five while changing nothing whatsoever about the content.

What it costs is not a penalty. It is attention. Crawl budget spent on near-duplicate URLs is budget not spent on your location pages, so they get recrawled monthly instead of weekly and new pages are found late.

And because most filter combinations return an empty result, a large share of that budget goes to pages with no products on them at all.

There is no such thing as a fifty dollar indica pre-roll from one particular brand in that store. How a handful of real pages becomes thousands is the same mechanism seen from the content side.

The tell is in the coverage report. Filter to the two “currently not indexed” states, discovered and crawled, and look at the URL patterns. If the counts are in the tens of thousands and the URLs all carry query strings, you have found it, and you now have it sized.

What actually to do about parameter URLs

There are five standard responses, they do different things, and they are frequently applied in the wrong order, including one combination that actively prevents the fix from working.

Disallowing in robots.txt stops the crawl but leaves indexed URLs in place. A canonical is a hint that is ignored when pages differ. Noindex removes them but they must still be crawled. Not linking them at all is the real fix, with a small deliberate set of category and brand URLs kept and indexed.
Five responses, and only one of them removes the cause rather than managing the symptom.

Disallowing in robots.txt stops the crawling and does not remove anything already indexed. Those URLs stay, listed without a description, indefinitely.

A canonical is a hint rather than a directive, and it is most likely to be ignored precisely when the filtered page differs enough from the clean one to look like its own page.

Noindex does remove them, but the URL has to be crawled for the noindex to be seen, so the budget is still being spent while it drains.

The combination that cancels itself out

Disallow and noindex together does nothing useful, because a disallowed URL is never fetched and the noindex on it is never read. That combination is applied constantly, in good faith, and it leaves the URLs in the index permanently.

The fix that removes the cause

The real fix is upstream. Stop generating crawlable links for filter states: run the filters client-side so the URL does not change, or render them as controls rather than anchors. Nothing to discover, nothing to crawl, no problem to manage.

Then deliberately keep a small set as real, linked, indexable URLs. Category and brand are the two with genuine search demand and genuinely different content. Twelve of those is a content asset. Sixty-nine thousand is a liability.

The order matters: stop the links first, then noindex what is already out there and let it drain over some weeks, and only consider robots.txt at the very end, if at all.

Schema: what to mark up and what to leave alone

Structured data on a menu page is where good technical instincts do damage, because the obvious thing to mark up is the one thing you should not.

Mark up the business: a Store node per branch with its own address, an Organization once on the homepage, breadcrumbs where they exist, and questions that are visible on the page. Do not mark up products, offers with prices, restaurant menu types, or aggregate ratings you assembled yourself.
Describe the business and the page. Do not describe the products, the prices, or a rating you assembled.

Mark up the business. A Store node for the branch with its own address, coordinates, telephone and opening hours; the Organization declared once on the homepage; breadcrumbs where breadcrumbs exist; and questions only where those questions are visible on the page.

That is the complete list, and a generator that builds the branch node for you is faster and less error-prone than hand-writing it forty times, which is how a phone number ends up right on the page and wrong in the markup.

Do not mark up the products. This is worth stating flatly because it feels like a missed opportunity and it is not one: Product markup returns nothing here, cannabis products being ineligible for the rich results it exists to produce.

So the markup is invisible, and what it does do is state a price and a product claim in machine-readable form on a category of site that already gets read more closely than most.

All cost, no return, which is an unusual enough combination to be worth recognising when you see it.

Two others to leave alone. The Menu type describes food service and does not mean this kind of menu, however much the word matches. And an aggregate rating you assembled yourself from reviews you chose is not a rating; it is a claim, and it belongs to the testimonial family in module four.

Core Web Vitals with somebody else’s widget

Worth a short section rather than a long one, because the honest position here is that this matters less for rankings than for whether the page feels broken, and the second reason is the better reason.

Largest Contentful Paint suffers because the embed is usually the biggest element, which you can fix by reserving space and loading it late. Interaction to Next Paint is controlled by the provider script and largely outside your control. Cumulative Layout Shift is caused by the embed pushing content down and is fixed with a fixed height container.
Two of the three are yours to fix. The middle one belongs to the provider’s script.

Largest Contentful Paint suffers because the embed is usually the biggest element on the page, and that one is yours: put real content above it so the largest painted element is something you control, and load the widget on intersection rather than immediately.

Cumulative Layout Shift is the easiest win on any dispensary site and almost nobody takes it. The embed loads late, appears, and pushes everything below it down the page. A fixed-height container reserving the space fixes it in one line of CSS.

Interaction to Next Paint is mostly not yours. Filter taps and product clicks execute in the provider’s script, and your options are to raise it with them or to change providers. Worth measuring anyway, because it is real evidence in a conversation with a vendor, and vendors respond to evidence better than to complaints.

Four architectures, and the one I actually recommend

Everything above eventually points at one decision, and the tempting answer is the wrong one for most chains.

An iframe takes ten minutes and returns nothing indexable. A client side script takes thirty minutes and renders sometimes. Prerendering the embed takes a week and returns stale but indexed content. An API integration into your own templates is a project and returns fresh content attributed to you.
The fourth row is the technically correct answer and the wrong recommendation for most of the sites that need this.

Rebuilding the menu as a server-side integration is the right answer on paper. It is also a development project, a maintenance commitment and a dependency on an API that will change without asking you.

For a large chain with an engineering team it is worth doing. For everybody else it gets scoped, costed, deferred, and the situation stays exactly as it was.

So the recommendation I would actually make is different, and it splits the problem in two. Keep the embed for what it is genuinely good at: live stock, live prices and the ordering flow, none of which you want to rebuild.

Then separately build a small set of real pages for the categories and brands you want to rank for: twelve or so, written and maintained as pages, with your own copy about what you carry, who it suits and how it compares, linking into the menu for the live inventory.

Twelve good category pages beat four hundred product URLs nobody can index. They rank for exactly the queries the first section identified. They are cheap enough to actually happen.

And they are the part of the site the menu platform cannot take with it when you change providers, which on a three-year view is the argument that matters most.

The diagnostic order

Six steps, and the order is the useful part. Each one removes a family of causes, so by the third you are looking at one problem instead of six.

Fetch the page as a crawler, search that response for a product name, check the rendered HTML in URL Inspection, run a site search for your product terms, filter the coverage report to parameter URLs, and finally sample a week of server logs. Each step removes a family of causes before the next one starts.
Six steps. Start at the last one and you will read a week of logs without knowing what you are looking for.
  1. Fetch the page the way a crawler does. Not your browser. This one step rules out the age gate, the user-agent branch and a server error in about thirty seconds, and it is why fetching it the way a crawler would is the first thing to run on any cannabis site you inherit.
  2. Search that response for a product name. Rules out server-side rendering and tells you which of the three integrations you are dealing with.
  3. URL Inspection, live test, rendered HTML tab. The ground truth. Everything about whether rendering works is settled here and nowhere else.
  4. A site: search for your own product terms. Removes any remaining doubt about what is actually in the index, as opposed to what should be.
  5. Coverage report, filtered to parameter URLs. Finds the crawl trap and sizes it, which turns it from an opinion into a number.
  6. A week of server logs. Shows where crawl budget is really going. Last, because it is the most work and because the first five steps tell you what to look for in it.

Twenty minutes for the first four on a site you have never seen. That is usually enough to name the cause precisely, which is the whole point. This class of problem is not hard to fix once it is named, and almost impossible to fix while it is still “the menu doesn’t rank”.

What fixed looks like, and what will not change

Set the expectation before the work starts, because the number the owner will check is not the number this work moves.

Indexed URLs, parameter URLs in the coverage report, product term impressions and time to index all move. Whether the menu appears in rendered HTML becomes a deliberate decision rather than an accident. Rankings for the store own name do not change, because that was never the problem.
Five rows move. The sixth is the one the owner checks, and it does not move at all.

Parameter URLs in the coverage report should fall from tens of thousands to a few hundred within a month or two of stopping the links. Time to index a new page falls from weeks to days as budget frees up. Indexed URL count goes up, but by twelve, not by four hundred. The category pages, not the products.

The number that tells you it worked is impressions on product and category terms in Search Console, filtered to queries containing product words. That was near zero and it should not be near zero any more. It is the only metric here that corresponds to money.

And the honest row: none of this moves your ranking for your own store name. That was never the problem, it was always fine, and it is the number an owner checks on a Tuesday. Say so at the start.

What this work opens is a category of demand you were not in at all: new searches rather than better positions on searches you already had. That is a better outcome, described accurately, rather than a worse one described badly.

That is the six modules. The map pack and the profile, recovering from a manual action, a content system that survives store fifteen, what you are allowed to say, how to produce it at a rate you can hold, and, here, making sure a crawler can actually read the result.

None of it is clever. It is a sequence of unglamorous things done in order, which is why most dispensaries in most markets are beatable, and why the ones that do it compound.

If you would rather have the technical layer looked at properly, that is what the technical SEO work we do on cannabis sites covers, and the free audit runs the first four diagnostic steps above and tells you which of these six modules you actually need.

Check your own site

Run the free check to see how your menu, age gate, and location pages appear to a search engine.

Run the Free Check

A menu rendering fix is usually a development project rather than a setting. How that work is scoped →