When developers build a store-finder page, structured data is the thing they leave for last and handle most carelessly. The map and the opening hours look right on screen, so it feels done. But whether your store surfaces properly in local search (Google Maps, the local pack) is decided not by the visible page but by the markup the crawler reads. And whether you paint that markup in later with JavaScript, or the server emits it from the start, makes a bigger difference than you'd expect.
First, let's clear up one misconception. "If you inject structured data with JS, Google can't read it" is wrong.
Google's official stance: JS structured data is supported
Google says so explicitly. In the words of the official docs:
"Google can read JSON-LD data when it is dynamically injected into the page's contents, such as by JavaScript code or embedded widgets."
— Google Search Central, Intro to Structured Data
So even if you attach an application/ld+json block later with document.createElement('script'), Google reads it from the DOM after rendering. Taken this far, "JS is fine" holds. The question is when, and how reliably, it gets read.
Hands-on check: what survives in the raw HTML
A crawler looks at a page twice. First it fetches the raw HTML as-is (first wave); when resources allow, it executes the JS with a headless Chromium and looks again (second wave). So whether the structured data is already present in the raw HTML at first-wave time is what makes the practical difference.
I built the same LocalBusiness markup two ways. One is static HTML emitted by the server; the other injects it at runtime with JS.
<!-- (A) Server-side: present in the raw HTML as-is -->
<script type="application/ld+json">
{"@context":"https://schema.org","@type":"LocalBusiness","name":"Clear Eyes Optical, Shibuya"}
</script>
<!-- (B) JS injection: absent from raw HTML, created only after execution -->
<script>
const ld = document.createElement('script');
ld.type = 'application/ld+json';
ld.textContent = JSON.stringify({ "@type": "LocalBusiness", name: "Clear Eyes Optical, Shibuya" });
document.head.appendChild(ld);
</script>
I parsed only the actual <script type="application/ld+json"> blocks out of the raw, un-executed HTML.
[A] Server-side: 1 ld+json block, @type=['LocalBusiness']
[B] JS injection: 0 ld+json blocks, @type=[]
The server-side version already carries the block in the raw HTML the first wave fetches. The JS-injection version has zero blocks in the raw HTML. That block only appears in the DOM after the browser runs the JS. To the crawler, it's data that exists only once the second-wave render happens.
Why this gap matters most for local
Google itself acknowledges the risk of dynamically generated markup. The guidance is framed for commerce, but the principle is the same.
"dynamically-generated markup can make Shopping crawls less frequent and less reliable, which can be an issue for fast-changing content."
— Google Search Central, Generate Structured Data with JavaScript
The render queue always depends on resources. If the second-wave render is delayed, or a third-party script fails to load mid-execution, that day's JSON-LD for that page is never generated at all. There's no reason to bet on a successful render every time for data like a store's name, address, and phone (NAP), where accuracy and consistency are the whole basis of trust. Server-side output carries no such dependency.
The honest limit — structured data does not lift ranking
Here's the part you must not skip. Even if you emit structured data perfectly server-side, that alone will not raise your search ranking. Google's docs are clear that structured data only makes you eligible for a rich result; it guarantees neither the display nor a ranking boost. The real drivers of local ranking sit on the Google Business Profile (GBP) side: operation, reviews, category accuracy. Site markup only assists that effect.
One more thing. Structured data must faithfully reflect what's visible on the page.
"don't add structured data about information that is not visible to the user, even if the information is accurate."
— Google Search Central
If the address registered in GBP and the address in your page markup disagree, it doesn't help. It just chips away at trust.
What you can do today
- Emit the store's NAP, coordinates, and URL statically from the server (route/template). Don't depend on JS injection.
- Match those values exactly with GBP. Don't include inconsistent, dummy, or unverified values (a placeholder social URL, for instance).
- Don't mark up information that isn't visible on the page.
- After deploying, verify both the raw and the rendered HTML with the Rich Results Test and URL Inspection. Don't stop at "I injected it with JS, it'll be fine."
- Don't promise a ranking effect. Hold the line that markup is an aid to the crawler's understanding.
To sum up: JS structured data is not a wrong method. But for data like local store info, where certainty is trust, emitting it from the server beforehand is more defensive and predictable than leaving it to the render queue.
If you want to emit a store-finder page's structured data reliably from the server, or have your existing site's local SEO and markup structure reviewed, I take on consulting and implementation work personally. Reach me through the contact on my jangwook.net profile.
Top comments (0)