Emergency situation

In case of emergencies or breakdowns, you can send an SMS to our emergency hotline

On-call phone (SMS only)

+45 29 70 15 95

Send an SMS with the following information:

  • Your name and webshop
  • Description of the problem
  • Your callback phone number

Notes: This service is only for critical situations where your webshop is down or has serious problems. For regular support, please use our normal support channels.

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.

Reading time: approx. {eight} minutes
Shopejer Developer

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.