28
Including json+ld structured data in Svelte
Ever wonder how Google search results include shopping results for products, or location results for local businesses? Their search bot is constantly being improved to better find that kind of content on your site, but ultimately if you care at all about SEO on your site it's important to include structured data on the page.
Structured data allows you to give search bots scrubbing your page a helping hand by adding metadata that describes your business, product, or even your blog posts.
tl;dr; Check out this Svelte REPL example for a working example in JS. Keep reading to get full TypeScript validation for your Schema objects!
Let's take a look at a basic example first. I pulled this straight from the JSON-LD Playground - it's an excellent reference for quickly messing around with the different schema and data types.
{
"@context": "http://schema.org/",
"@type": "Person",
"name": "Jane Doe",
"jobTitle": "Professor",
"telephone": "(425) 123-4567",
"url": "http://www.janedoe.com"
}
Nothing crazy going on there (yet). When a search bot comes by to index a page with this JSON, it's guaranteed to find Professor Jane Doe's contact information. This contact information is probably included elsewhere on the page, say in a sidebar or page footer, but chances are the way the HTML is setup could prevent the search from connecting the link from a phone number to the person's name.
This is where JSON+LD really stands out compared to other structured data formats that require special HTML attributes.
<script type="application/ld+json">
{
"@context": "http://schema.org/",
"@type": "Person",
"name": "Jane Doe",
"jobTitle": "Professor",
"telephone": "(425) 123-4567",
"url": "http://www.janedoe.com"
}
</script>
Yep, that's really all there is to it. Wrap the JSON object in a script
tag, mark it as a type of application/ld+json
and you're all set. There's way more to the different kinds of schemas you may want to include - check out the JSON-LD docs for a much better walk through of all the details than I would ever fit into a single blog post.
The beauty of Svelte is how closely it follows standard web technologies. Components are written in a single file with a script
tag for all the JavaScript, style
tag for the component's CSS, and one or more HTML elements for the actual DOM elements.
While that makes authoring Svelte components a nearly frictionless experience for web developers, it can throw a wrench in the JSON+LD department. Tooling built for Svelte like the VS Code plugin, and even the Svelte compiler itself, can cause headaches if you try to write an application/ld+json
script in your components.
This may very well get cleaned up in the future, but for now you'll likely run into warnings and errors related to having more than one script
tag in a component. A Google search will lead you down a rabbit hole of different combinations of Svelte's @html expressions and string literals to get it to compile.
There's a much better way to do this though, and even better it can be combined with TypeScript to add extra type validations for your JSON schema objects.
The first part of the magic trick is getting around issues related to parsing extra script
tags in your Svelte component. This is where many will reach for some combination of {$html}
and string literals.
I prefer to move this out of the Svelte component all together. With the logic in a plain old JavaScript file it can be unit tested and will never run into issues with tools trying to parse .svelte
files.
export function serializeSchema(thing) {
return `<script type="application/ld+json">${JSON.stringify(thing)}</script>`;
}
There's not much magic going on here, the function just takes in a JavaScript object and spits out the ld+json
script tag for it.
We'll lean on the schema-dts package for full Schema.org type definitions.
import type { Thing, WithContext } from "schema-dts";
export type Schema = Thing | WithContext<Thing>;
export function serializeSchema(thing: Schema) {
return `<script type="application/ld+json">${JSON.stringify(
thing,
null,
2
)}</script>`;
}
schema-dts
defines types for all of the different schema objects, see below for a more detailed example with an Organization
object. This is a huge win, it's easy to accidentally structure the JSON wrong or have a typo in one of the property names. Setting it up to use TypeScript definitions we can make sure that our JSON objects are validated at build.
Here's the Organization
object used by this very site.
import site from `$data/site.json`;
export const organizationSchema: WithContext<Organization> = {
"@context": "https://schema.org",
"@type": "Organization",
"@id": `${site.url}#organization`,
url: site.url,
name: site.company.name,
description: site.description,
sameAs: [`https://twitter.com/${site.social.twitter}`],
logo: `${site.url}/favicon.svg`,
};
Data specific to our site is pulled in from a local JSON file. This could be data exposed through a Git-based CMS like Forestry or pulled down from a headless CMS like Sanity. The important thing here is that our Organization
object is defined in TypeScript, verified at build-time, and can be unit tested if you want to make sure the site.json
data is hooked up properly.
What is WithContext
doing? That's a clever setup from schema-dts
, the top-level JSON+LD object should have a @context
property. You can nest objects though, and they even have support for using an object graph format for multiple objects. Any nested objects, or every object in the graph, doesn't need @context
. WithContext
is a TypeScript wrapper for another type, in the example above I could have removed @context
from the object and it would be a valid Organization
type.
Finally, let's add the JSON+LD into the DOM. Most of our projects end up with a LDTag.svelte
component similar to
<script lang="ts">
import { serializeSchema } from "$utils/json-ld";
import type { Schema } from "$utils/json-ld";
export let schema: Schema;
</script>
<svelte:head>
{@html serializeSchema(schema)}
</svelte:head>
Multiple Svelte components on a page can inject their own application/ld+json
script into the document's head. This works great with SvelteKit too, a layout or route component could inject page-level schemas.
Take a look at our Svelte REPL example to see a full working JavaScript example.