Implement structured data (Schema.org) in your Shoporama theme
Complete guide to JSON-LD structured data in a Shoporama theme. Smarty examples for Product, Store, BreadcrumbList and AggregateRating with proper safe methods.
Structured data (Schema.org) helps Google and other search engines understand the content on your pages. For an online store, it’s especially important to mark up products, store information, breadcrumbs, and product reviews so you can display “rich snippets” in search results—including details like price, stock status, and star ratings.
Tip: Shoporama automatically adds basic Schema.org markup for products through our built-in SEO features. Read about automatic structured data and SEO in Shoporama. This article is for those who want to build or expand the markup themselves in their theme.

What is structured data?
Structured data is a standardized way to describe a page’s content in a format that search engines understand. JSON-LD is the recommended format and is placed within a <script type="application/ld+json"> tag. It does not affect the page’s visual appearance but provides search engines with structured information that can be used for rich snippets.
The Correct `safe` Methods for `SafeProduct`, `SafeWebshop`, and `SafeCompany`
Before you build your own JSON-LD markup, it’s important to know which `safe` methods are actually available on the objects in a Shoporama theme. These are the stable methods you can use in Smarty.
On $product (SafeProduct):
- getName(), getDescription(), getOwnId(), getGtin(), getMpn(), getBrandName()
- getPrice(), getSalePrice(), getRealPrice(), getLowest30DayPrice()
- getStockCount(), getIsInStock(), and getAllowNegativeStock()
- getImage() and getImages() return SafeImage with getSrc($w, $h, $type)
- getAvgRating($no_round), getReviewCount(), getProductReviews($limit)
- getUrl() returns the product’s relative path
On $webshop (SafeWebshop):
- getName(), getDescription(), getCurrency(), getUrl(), getLogo()
- getCompany() returns a SafeCompany
On $webshop->getCompany() (SafeCompany):
- getName(), getRegNr(), getEmail(), getPhone()
- getAddress(), getZipcode(), getCity(), getCountry()
Note: Do not use $product->getStock(), $product->getAverageRating(), $webshop->getCurrencyCode(), $webshop->getDomain(), $webshop->getEmail(), or $webshop->getPhone(). They are not available as safe methods, and your theme will fail. Instead, use getStockCount(), getAvgRating(), getCurrency(), getUrl(), and getCompany()->getEmail()/getPhone().
Escaping: Use @json_encode, not escape:javascript
JSON-LD is JSON, and JSON has its own rules for how special characters are written. This is by far the most common reason why an otherwise correct markup is rejected by Google, so it’s worth getting this right before you write your first block.
Do not use |escape:'javascript' in JSON-LD. It converts an apostrophe to \, which is valid in JavaScript but not in JSON, where that escape sequence does not exist at all. A single product with an apostrophe in its name—for example, “Bo’s Kaffe”—is therefore enough for Google to discard the entire block, causing the page to lose its price, stock status, and star ratings all at once. In Search Console, this appears as “Bad escape sequence in string.”
Use |@json_encode instead. It outputs the value as a complete JSON string, including the quotation marks, so you must remove the quotation marks from your template:
Incorrect: "name": "<{$product->getName()|escape:'javascript'}>",
Correct: "name": <{$product->getName()|@json_encode}>,
This ensures that apostrophes, quotation marks, backslashes, and line breaks are handled correctly. Line breaks are worth noting: a review text spanning multiple lines will corrupt the JSON if it isn’t escaped, and this isn’t caught by either |escape or <{strip}>. In addition, the escaper method handles slashes, so a </script> in a product name won’t prematurely close your script block.
- If the field can be empty, insert |default:"" before it, e.g., <{$product->getOwnId()|default:""|@json_encode}>. Otherwise, the result will be null instead of an empty string.
- If you need to remove HTML first, insert |strip_tags:true before it, i.e., |strip_tags:true|@json_encode.
- There is no |escape:'json' in Smarty. If you use it, no escaping will occur at all.
- Numbers and constant values, such as price, currency, and schema.org URLs, should still be enclosed in regular quotation marks. They do not contain special characters.
- |escape:'javascript' is still the correct choice in regular JavaScript, such as in a dataLayer push. It is only incorrect in JSON-LD.
Product Markup (Product)
The most important type for an online store is Product markup. Insert the block into your product template (typically product/view.html):
<{assign var="availability" value=$product->getStructuredDataAvailability()}>
<{assign var="availabilityStarts" value=$product->getStructuredDataAvailabilityStarts()}>
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "Product",
"name": <{$product->getName()|@json_encode}>,
"description": <{$product->getDescription()|strip_tags|@json_encode}>,
"sku": <{$product->getOwnId()|@json_encode}>,
<{if $product->getGtin()}>
"gtin": <{$product->getGtin()|@json_encode}>,
<{/if}>
<{if $product->getMpn()}>
"mpn": <{$product->getMpn()|@json_encode}>,
<{/if}>
<{if $product->getBrandName()}>
"brand": {
"@type": "Brand",
"name": <{$product->getBrandName()|@json_encode}>
},
<{/if}>
<{if $product->getImage()}>
"image": "<{$product->getImage()->getSrc(800, 800, 'fit')}>",
<{/if}>
"offers": {
"@type": "Offer",
"url": "<{$webshop->getUrl()}><{$product->getUrl()}>",
"price": "<{$product->getRealPrice()|string_format:"%.2f"}>",
"priceCurrency": "<{$webshop->getCurrency()}>",
"availability": <{$availability|@json_encode}><{if $availabilityStarts}>,
"availabilityStarts": <{$availabilityStarts|@json_encode}><{/if}>
}
}
</script>
Three important points in the example:
- The price is retrieved using getRealPrice(), so any sale price and discount are included, and tax is added.
- The currency is retrieved using getCurrency() on $webshop (returns, for example, "DKK" or "EUR").
- Stock status is retrieved using getStructuredDataAvailability(), which returns the complete schema.org URL—that is, https://schema.org/InStock, OutOfStock, or BackOrder. This method uses exactly the same logic as our Google Shopping feed, so the product page and the feed always show the same information. Don’t build it yourself using getIsInStock() and getAllowNegativeStock(): that combination overlooks the fact that the store itself determines whether backordered items should be shipped as “backorder,” and that’s exactly the discrepancy you’d want to avoid.
- getStructuredDataAvailabilityStarts() returns the expected delivery date for a backordered item—that is, the availabilityStarts field. It returns an empty value when the item is not on backorder or when you haven’t specified a delivery time in Google Shopping, so wrap it in a <{if}> tag as shown in the example.
Important if you use Google Shopping: Google compares the stock status on the product page with the one you submit in the product feed. If the two don’t match, Google may deactivate the product, causing it to disappear from both Shopping ads and the free product listings. If you use getStructuredDataAvailability(), the two won’t conflict, since the feed and the product page are based on the same method. If you write your own stock status logic, you risk the page showing “out of stock” for an item while the feed reports it as available for purchase. Shoporama’s own themes already handle this. Read more in Set Up Google Shopping Feed.
Previous Price (Omnibus Directive)
If the product is on sale, Google recommends displaying the lowest price from the past 30 days as the reference price. Use getLowest30DayPrice() to retrieve it. It returns null if there isn’t enough price history.
Product Reviews (AggregateRating)
If you use Shoporama’s built-in review system, you can add an average rating directly in the Product markup. This can display stars in Google’s search results. Insert the block inside the Product object:
<{if $product->getReviewCount() > 0}>
,
"aggregateRating": {
"@type": "AggregateRating",
"ratingValue": "<{$product->getAvgRating(true)|string_format:"%.1f"}>",
"reviewCount": "<{$product->getReviewCount()}>"
}
<{/if}>
The parameter `true` for ` getAvgRating()` returns an unrounded average, which Google prefers (e.g., 4.3 instead of just 4).
Store Information (Store / LocalBusiness)
You can also highlight the online store itself, for example, in your footer template. Retrieve contact data from $webshop->getCompany() so that the information you’ve already entered under Company > Company Information is automatically included:
<{$company = $webshop->getCompany()}>
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "Store",
"name": <{$webshop->getName()|@json_encode}>,
"url": "<{$webshop->getUrl()}>"
<{if $company}>
,
<{if $company->getEmail()}>
"email": <{$company->getEmail()|@json_encode}>,
<{/if}>
<{if $company->getPhone()}>
"telephone": <{$company->getPhone()|@json_encode}>,
<{/if}>
"address": {
"@type": "PostalAddress",
"streetAddress": <{$company->getAddress()|@json_encode}>,
"postalCode": <{$company->getZipcode()|@json_encode}>,
"addressLocality": <{$company->getCity()|@json_encode}>,
"addressCountry": <{$company->getCountry()|@json_encode}>
}
<{/if}>
<{if $webshop->getLogo()}>
,
"logo": "<{$webshop->getLogo()->getSrc(400, 400, 'fit')}>"
<{/if}>
}
</script>
Make sure the company information is filled out correctly in the admin panel under Company, Company Information. If you have a physical store, you can use LocalBusiness or a more specific subtype (e.g., ClothingStore) as the @type.
Breadcrumbs (BreadcrumbList)
On product and category pages, it’s good SEO practice to include breadcrumbs. Example for a product with a main category:
<{if $product->getMainCategory()}>
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "BreadcrumbList",
"itemListElement": [
{
"@type": "ListItem",
"position": 1,
"name": <{$webshop->getName()|@json_encode}>,
"item": "<{$webshop->getUrl()}>"
},
{
"@type": "ListItem",
"position": 2,
"name": <{$product->getMainCategory()->getName()|@json_encode}>,
"item": "<{$webshop->getUrl()}><{$product->getMainCategory()->getUrl()}>"
},
{
"@type": "ListItem",
"position": 3,
"name": <{$product->getName()|@json_encode}>,
"item": "<{$webshop->getUrl()}><{$product->getUrl()}>"
}
]
}
</script>
<{/if}>
Test your structured data
Use Google’s and Schema.org’s tools to validate your markup before going live:
- The Rich Results Test checks whether your markup generates rich snippets in Google
- The Schema Markup Validator validates the JSON-LD syntax
- In Google Search Console, you can view reports on your products and rich results after the pages have been crawled
Tip: If you’ve already enabled automatic structured data in Shoporama, be sure to avoid duplicates. Check what’s already on the page before adding your own markup.
Frequently Asked Questions
Do I need to code JSON-LD myself at all?
No, not necessarily. Shoporama has built-in automatic structured data for products, and that covers most cases. Manual coding is primarily relevant if you want to add additional fields or use a more specific @type, such as for hotels, restaurants, or events.
What schema types can I use besides Product?
In a Shoporama theme, you can implement all Schema.org types available as JSON-LD. The most commonly used ones for online stores are Product, Offer, AggregateRating, Review, BreadcrumbList, Store, LocalBusiness, Organization, WebSite with SearchAction, and FAQPage for static FAQ pages.
Why am I getting errors in the Rich Results Test?
Typical errors include missing fields (e.g., `price ` without ` priceCurrency`), incorrect number formatting, or special characters that break the JSON syntax. By far the most common cause is escaping: run all text values through `|@json_encode` and not `|escape:'javascript'`, which turns an apostrophe into invalid JSON. See the section on escaping above.
How long will it take before I see stars in Google?
Google must first recrawl the page before your new markup takes effect. This can take anywhere from a few days to a couple of weeks. In Search Console, you can request fast reindexing of individual pages.
Does this affect my rankings in Google?
Structured data isn’t a direct ranking factor, but rich snippets—such as those showing price and star ratings—make your search result more visible and can increase your click-through rate. In the long run, this helps improve your overall SEO.
How do I handle sales tax in the price field?
Use getRealPrice(), which returns the price including VAT. This is the price the customer sees in the store, and the one Google expects in the Product markup for a B2C store. If you need to display the price excluding VAT (B2B), use getPriceExVat() instead.
Do I need reviews before I display AggregateRating?
Yes. Only add AggregateRating if the product actually has reviews. The example above checks that getReviewCount() > 0, so that markup isn’t added to products without reviews. Fake or empty ratings can result in a penalty from Google.
Where can I find my company information for the Store markup?
Go to the admin panel under Company, Company Information. The data you enter here will be available via $webshop->getCompany() in your theme, so the Store block will be filled in automatically.
Where should the JSON-LD be placed on the page?
You can place it either in `<head> ` or in `<body>`. Both are valid according to Google. For product pages, it’s common to place it at the bottom of the product template.
My online store sells in multiple languages. Do I need separate markup for each language?
Yes, the names and descriptions in the markup must match the page the customer sees. Since $product->getName() and $product->getDescription() automatically return the translated text for the active language, this happens completely automatically when you use the examples provided here.
Do you need help with structured data? Contact us at support@shoporama.dk.
Related articles
SEO in Shoporama
Complete guide to the SEO features in Shoporama - from meta tags and sitemap to Google Shopping and AI Assistant.
Product reviews
Learn how to use product reviews in Shoporama. Collect customer reviews automatically, moderate them and display star ratings on your product pages.
Variables in a Shoporama theme
Overview of global and page-specific Smarty variables available in Shoporama themes.
Implement tracking in a Shoporama theme
Developer Guide to E-commerce Tracking in a Shoporama Theme: Data Layer, Google Tag Manager, Custom Events, and Correct Product IDs Using...
Blog linked to your shop
Complete guide to the blog feature in Shoporama - create posts, schedule publishing, link products, optimize for search engines and use dynamic...
Price history and the Omnibus Directive
Shoporama automatically logs price changes and calculates the correct pre-price according to the Omnibus Directive (EU 2019/2161). The default...
Set noindex on a page, product or category
Guide to setting noindex in Shoporama so that search engines do not index certain pages.
Set Up a Google Shopping Feed
Guide to Setting Up a Google Shopping Feed in Shoporama: Categories, Custom Labels, Product IDs, and Catalog Match, as well as Country and Language...
Get stars on Google Shopping with product reviews
Here's how to display your product reviews as stars in Google Shopping using Shoporama's built-in review feed: activation, Google requirements, and...
Product IDs and Catalog Match: How to Ensure Your Ads Match Your Product Feed
An explanation of which product ID Shoporama’s tracking sends, why it always matches the Google Shopping feed, what “catalog match” means for your...
Related features
Structured Data - automatic Schema.org markup
Automatic Schema.org markup for products, reviews, breadcrumbs and company info. Rich snippets in Google with stars and awards. Included.
SEO in Shoporama - get visible in Google
Shoporama has built-in SEO features: fast speed, full control over titles and meta descriptions, dynamic tags, automatic 301 redirects, XML...