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 is especially relevant to mark products, store information, breadcrumbs and product reviews so that you can achieve rich snippets in search results such as price, stock status and star rating.
Tip: Shoporama automatically adds basic Schema.org markup for products via our built-in SEO features. Read about automatic structured data and SEO in Shoporama. This article is for those who want to build or extend the markup themselves in your theme.
What is structured data?
Structured data is a standardized way of describing page content in a format that search engines understand. JSON-LD is the recommended format and is placed in a <script type="application/ld+json"> tag. It does not affect the visual appearance of the page, but provides search engines with structured information that can be used for rich snippets.
The right safe methods on SafeProduct, SafeWebshop and SafeCompany
Before you build your own JSON-LD markup, it's important to know the safe methods that actually exist 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() and getIsInStock()
- getImage() and getImages() return SafeImage with getSrc($w, $h, $type)
- getAvgRating($no_round), getReviewCount(), getProductReviews($limit)
- getUrl() gives the relative path of the product
On $webshop (SafeWebshop):
- getName(), getDescription(), getCurrency(), getUrl(), getLogo()
- getCompany() returns 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 do not exist as safe methods and your theme will fail. Instead, use getStockCount(), getAvgRating(), getCurrency(), getUrl() and getCompany()->getEmail()/getPhone().
Product markup (Product)
The most important type for an online store is the Product markup. Insert the block in your product template (typically product/view.html):
<script type="application/ld+json"> { "@context": "https://schema.org","@type":"Product","name":"<{$product->getName()|escape:'javascript'}>","description":"<{$product->getDescription()|strip_tags|escape:'javascript'}>","sku":"<{$product->getOwnId()|escape:'javascript'}>", <{if $product->getGtin()}> "gtin": "<{$product->getGtin()|escape:'javascript'}>", <{/if}> <{if $product->getMpn()}> "mpn": "<{$product->getMpn()|escape:'javascript'}>", <{/if}> <{if $product->getBrandName()}> "brand": { "@type":"Brand", "name":"<{$product->getBrandName()|escape:'javascript'}>" }, <{/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": "<{if $product->getStockCount() > 0}>https://schema.org/InStock<{else}>https://schema.org/OutOfStock<{/if}>" } </script>
Three important points in the example:
- Price is retrieved with getRealPrice(), so any sale price and discount are included, and VAT is added.
- Currency is retrieved with getCurrency() on $webshop (returns e.g. "DKK" or "EUR").
- Stock status is determined with getStockCount(). You can also use getIsInStock() if you want to allow selling stocked products with 0 in stock.
Pre-price (Omnibus directive)
If the product is on sale, Google recommends displaying the lowest price of the last 30 days as the reference price. Use getLowest30DayPrice() to retrieve it. It returns null if there is not 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 provide stars in Google 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 true parameter to getAvgRating() gives an unrounded average, which Google prefers (e.g. 4.3 instead of just 4).
Store information (Store / LocalBusiness)
You can also mark the webshop itself, for example in your footer template. Get contact data from $webshop->getCompany() so that the information you have already set under Settings, Company information is automatically included:
<{$company = $webshop->getCompany()}> <script type="application/ld+json"> { "@context": "https://schema.org", "@type":"Store", "name":"<{$webshop->getName()|escape:'javascript'}>", "url": "<{$webshop->getUrl()}>" <{if $company}> , <{if $company->getEmail()}> "email": "<{$company->getEmail()|escape:'javascript'}>", <{/if}> <{if $company->getPhone()}> "phone": "<{$company->getPhone()|escape:'javascript'}>", <{/if}> "address": { "@type": "PostalAddress", "streetAddress": "<{$company->getAddress()|escape:'javascript'}>", "postalCode": "<{$company->getZipcode()|escape:'javascript'}>", "addressLocality": "<{$company->getCity()|escape:'javascript'}>", "addressCountry": "<{$company->getCountry()|escape:'javascript'}>" } <{/if}> <{if $webshop->getLogo()}> , "logo": "<{$webshop->getLogo()->getSrc(400, 400, 'fit')}>" <{/if}> }
Make sure that company details are filled in correctly in admin under Settings, Company details. If you have a physical store, you can use LocalBusiness or a more specific subtype (e.g. ClothingStore) as @type.
Breadcrumbs (BreadcrumbList)
On product and category pages, it's good SEO practice to mark breadcrumbs. Example for a product with main category:
<{if $product->getMainCategory()}> <script type="application/ld+json"> { "@context": "https://schema.org", "@type": "BreadcrumbList", "itemListElement": [ { "@type": "ListItem", "position": 1, "name": "<{$webshop->getName()|escape:'javascript'}>", "item": "<{$webshop->getUrl()}>" }, { "@type":"ListItem", "position": 2, "name": "<{$product->getMainCategory()->getName()|escape:'javascript'}>", "item": "<{$webshop->getUrl()}><{$product->getMainCategory()->getUrl()}>" }, { "@type": "ListItem", "position": 3, "name": "<{$product->getName()|escape:'javascript'}>", "item": "<{$webshop->getUrl()}><{$product->getUrl()}>" } ] } </script> <{/if}
Test your structured data
Use Google and Schema.org tools to validate your markup before you go live:
- Rich Results Test checks if your markup provides rich snippets in Google
- Schema Markup Validator validates the JSON-LD syntax
- In Google Search Console you can see reports of your products and rich results after the pages have been crawled
Tip: If you have already enabled automatic structured data in Shoporama, take care to avoid duplicates. Check what's already on the page before adding your own markup.
Frequently asked questions
Do I even need to code JSON-LD myself?
No, not necessarily. Shoporama has built-in automatic structured data for products and that covers most things. Self-coding is mainly relevant if you want to add additional fields or use a more specific @type, for example for hotels, restaurants or events.
What schema types can I use besides Product?
In a Shoporama theme, you can build any Schema.org type that exists as JSON-LD. The most commonly used for eCommerce are Product, Offer, AggregateRating, Review, BreadcrumbList, Store, LocalBusiness, Organization, WebSite with SearchAction and FAQPage for fixed FAQ pages.
Why do I get errors in the Rich Results Test?
Typical errors are missing fields (e.g. price without priceCurrency), incorrect formatting of numbers or quotes that break the JSON syntax. Always use |escape:'javascript' on all text values in Smarty.
How long does it take before I see stars in Google?
Google needs to crawl the page again before your new markup is used. This can take from a few days to a few weeks. In Search Console, you can request fast reindexing of individual pages.
Does it affect my rankings in Google?
Structured data is not a direct ranking factor, but rich snippets such as price and stars make your search result more visible and can increase click-through rates. In the long run, it helps improve your overall SEO.
How do I handle VAT in the price field?
Use getRealPrice() which returns the price including VAT. This is the price the customer sees in the store and the price Google expects in the Product markup on a B2C shop. If you need to show the price excluding VAT (B2B), use getPriceExVat() instead.
Do I need reviews before showing AggregateRating?
Yes, you do. Only add AggregateRating if the product actually has reviews. The example above checks getReviewCount() > 0, so markup is not added to products without reviews. False or empty ratings can trigger a penalty from Google.
Where do I find my company details for Store markup?
Go into the admin under Settings, Company details. The data you fill in here becomes 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 shop sells in multiple languages. Do I need markup per language?
Yes, names and descriptions in the markup should reflect the page the customer is viewing. Since $product->getName() and $product->getDescription() automatically return the translated text for the active language, this happens automatically when you use the examples here.
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 implement e-commerce tracking in a Shoporama theme with standard data layer, Google Tag Manager and custom events.
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.
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...