SvelteKit FAQ Page SEO: add JSON-LD Schema

Rodney Lab - Dec 1 '21 - - Dev Community

🔑 Creating an FAQ Page with SEO Metadata in SvelteKit

In this article on SvelteKit FAQ Page SEO, we see how easy SvelteKit makes it to create an FAQ page. As well as that, we generate search engine optimised metadata. This is the stuff that increases user experience on search engine result pages and so is loved by Google, meaning your page will rank higher.

Frequently asked question (FAQ) pages are helpful when you are looking for a quick solution to a current issue and you don't want to have to read through reams of docs to extract the snippet you need. As an extra step when you create an FAQ page, it is worth also creating some metadata which tells Google about the questions and answers on the page. This is because user interaction (on the search results page) is an important ranking factor meaning your page will appear higher in search results. In this article we see how you can add the right markup to your FAQ page in SvelteKit. I hope you will find the code useful and try it out on a suitable project that you are working on.

🌟 Our Goal: FAQ Featured Snippet

SvelteKit FAQ Page SEO: image search Google result page for the query 'What is narcissus backendless'.  At top of the results is a paragraph  of text as a feature snippet directly answering this question

FAQ metadata on your page will prove especially useful to users who form their search query as a question. Supporting this type of search becomes increasingly import as search using mobile devices and electronic personal assistants (the likes of Alexa, Siri and friends) becomes more widespread. For example, we see in the capture above of a desktop Google search, the top result is a featured snippet. Featured snippets may also take the form of a How To. They appear large, above all results and most of all; users like to click them.

SvelteKit FAQ Page SEO: How To Example capture shows Google search results to query Create sveltekit component library.  The top result dominates listing 6 of the 10 steps needed to Create s SvelteKit Component Library

Google will experiment with shuffling up search results and throwing your page to the top. If it does not get enough interaction though, it can quickly lose that prime position. It is important to remember that most users will not look beyond the first few results. So the top spaces are extremely valuable for getting users to your site.

Next we will take a peek at the SvelteKit FAQ page we are going to build featuring the meta needed for it to be considered for a featured snippet.

🧱 SvelteKit FAQ Page SEO: What we're Building

We will create a single page app. This builds on the earlier SEO tutorials where we saw how to add basic SEO metadata for search engine optimisation in SvelteKit, sharing cards for social networks and next level Schema.org SEO meta to delight search engine users and get them onto your site.

SvelteKit FAQ Page SEO: Image shows an F A Q page with the first question expanded and another two question collapsed

We will source our question data from a JSON file so that we can take advantage of SvelteKit JSON data imports. Once we have built the site, we will see how you can test it with Google's own SEO tools.

⚙️ SvelteKit FAQ Page SEO: Setting up the Project

To get going, spin up a skeleton SvelteKit project:

pnpm init svelte@next sveltekit-faq-page-seo && cd $_
pnpm install
Enter fullscreen mode Exit fullscreen mode

You can answer no to the TypeScript prompt but, select ESLint and prettier formatting. Next we just need a couple of extra packages for this project:

pnpm add -D @fontsource/dm-sans dotenv
Enter fullscreen mode Exit fullscreen mode

Then let's create a .env file in the project's root directory:

VITE_SITE_URL=https://example.com
Enter fullscreen mode Exit fullscreen mode

Add the url where you will deploy the site, or just keep example.com for now, if you are not yet sure what this will be. Finally add dotenv configuration to svelte.config.js

import 'dotenv/config';

/** @type {import('@sveltejs/kit').Config} */
const config = {
  kit: {
    // hydrate the <div id="svelte"> element in src/app.html
    target: '#svelte',
    vite: {
      define: {
        'process.env.VITE_BUILD_TIME': JSON.stringify(new Date().toISOString()),
      },
    },
  },
};

export default conig;
Enter fullscreen mode Exit fullscreen mode

The code in line 10 lets us get build time for use in the SEO meta.

⚙️ SvelteKit FAQ Page SEO: Questions

It wouldn't be much of an FAQ page without some questions, so let's create a src/lib/data folder and put an faqs.json file in there. We will import the data directly into our Svelte markup. If you want to learn more about how this works, there is a tutorial which cover s a couple of different data shapes. Anyway either paste in these questions, or add your own, keeping the same structure:

[
  {
    "question": "What is the deal with SvelteKit?",
    "answer": "SvelteKit is an awesome tool for building fast websites and web apps.  Its secret powers lie in the compiler at Svelte's heart.  This lets Svelte generate pure JavaScript which is interpreted by the user browser.  This is contrast to other frameworks where an additional step of generating JavaScript in the browser is required.  SvelteKit is to Svelte what Next is to React, it is a tool for creating Svelte apps."
  },
  {
    "question": "Is SvelteKit ready for production?",
    "answer": "SvelteKit is already used in a number of production website globally."
  },
  {
    "question": "Does SvelteKit use vite?",
    "answer": "Yes. vite, like snowpack is a next generation bundler, building on advances earlier made by more established tools like WebPack.  Among the advantages are faster hot module reloading and better tree shaking.  These benefits come from using ES Modules."
  },
  {
    "question": "Is SvelteKit still in beta?",
    "answer": "Yes, SvelteKit is still in beta. Despite that it is already used in a number of production websites."
  },
  {
    "question": "How do you deploy SvelteKit?",
    "answer": "You can deploy SvelteKit apps as static sites or Server Side Rendered apps.  Leading hosting services like Cloudflare, Netlify, Render and Vercel all support SvelteKit."
  }
]
Enter fullscreen mode Exit fullscreen mode

You will need at least three questions for Google to consider the page to be a valid FAQ page.

🏠 SvelteKit FAQ Page SEO: Home Page

You'll see the Svelte markup is going to be quite lightweight, making it easier for you to rip this code out and insert it as a component or page in your own project. For that reason the FAQ aspects of SEO are fully working and tested but the rest of SEO is not fully fleshed out. Replace the code in src/routes/index.svelte:

<script>
  import Question from '$lib/components/Question.svelte';
  import SEO from '$lib/components/SEO.svelte';
  import website from '$lib/configuration/website';
  import faqs from '$lib/data/faqs.json';
  import '@fontsource/dm-sans';

  const { siteUrl } = website;
  const title = 'SvelteKit FAQ Page with SEO';
  const url = `${siteUrl}/`;
</script>

<SEO
  {url}
  {title}
  {faqCount}
  seoMetaDescription="Demo site for FAQ page using SvelteKit with SchemaOrg FAQ metadata"
/>
<main class="container">
  <div class="content">
    <h1>SvelteKit FAQs</h1>
    <section role="feed">
      {#each faqs as { question, answer }, index}
        <article aria-posinset={index + 1} aria-setsize={faqCount}>
          <Question id={`question-${index + 1}`} {question} {answer} position={index + 1} {url} />
        </article>
      {/each}
    </section>
  </div>
</main>
Enter fullscreen mode Exit fullscreen mode

If you're not new to these tutorials, you can skip to the next paragraph. Vite lets us import our JSON file and use it in the JavaScript code. You will notice the JSON file has an array at the top level and so the import in line 5 results in faqs holding that array. We can treat this just like any other array in JavaScript. In lines 2327 we iterate over the elements of the array producing a question for each one. If you are more familiar with React, consider this analogous to an array.map() method, though in Svelte there is no need to add a key to each element. Let's look at the first line of the each block. Recall each element of the array is an object with a question and answer field. We can destructure those fields from each element and also access the index using the concise notation in line 23.

This code doesn't yet work… we will define the missing SEO and Question components next. Before that here is some optional styling which you can paste at the bottom of the same file:

<style>
  :global(html) {
    background-image: linear-gradient(
      to bottom right,
      hsl(var(--colour-dark-hue) var(--colour-dark-saturation) var(--colour-dark-luminance)),
      hsl(
        var(--colour-dark-hue) var(--colour-dark-saturation)
          calc(var(--colour-dark-luminance) * 0.8)
      )
    );
    color: hsl(
      var(--colour-light-hue) var(--colour-light-saturation) var(--colour-light-luminance)
    );

    font-family: DM Sans;
  }

  :global(h1) {
    font-size: var(--font-size-7);
    margin-top: var(--spacing-0);
    margin-bottom: var(--spacing-2);
  }

  :global(:root) {
    /* paradise pink */
    --colour-brand-hue: 345;
    --colour-brand-saturation: 76%;
    --colour-brand-luminance: 58%;

    /* coral */
    --colour-alt-hue: 14;
    --colour-alt-saturation: 100%;
    --colour-alt-luminance: 70%;

    /* charleston green */
    --colour-dark-hue: 120;
    --colour-dark-saturation: 1%;
    --colour-dark-luminance: 17%;

    /* nickel */
    --colour-mid-hue: 44;
    --colour-mid-saturation: 7%;
    --colour-mid-luminance: 41%;

    /* eggshell */
    --colour-light-hue: 49;
    --colour-light-saturation: 60%;
    --colour-light-luminance: 93%;

    --spacing-px: 0.0625rem;
    --spacing-px-2: 0.125rem;
    --spacing-0: 0;
    --spacing-1: 0.25rem;
    --spacing-2: 0.5rem;
    --spacing-4: 1rem;
    --spacing-6: 1.5rem;
    --spacing-8: 2rem;
    --spacing-12: 3rem;
    --spacing-20: 5rem;

    --font-size-1: 1rem;
    --font-size-2: 1.25rem;
    --font-size-7: 3.815rem;
    --mobile-font-size-2: 1.125rem;

    --font-weight-medium: 500;

    --line-height-relaxed: 1.75;
  }

  .container {
    display: flex;
    min-height: 100vh;
    width: 100%;
  }

  .content {
    display: flex;
    flex-direction: column;
    margin: var(--spacing-4) var(--spacing-4) var(--spacing-8);
    width: 100%;
  }

  @media (min-width: 768px) {
    :global(h1) {
      font-size: var(--font-size-7);
    }

    .content {
      margin: var(--spacing-8) var(--spacing-20) var(--spacing-12);
    }
  }
</style>
Enter fullscreen mode Exit fullscreen mode

😕 SvelteKit FAQ Page SEO: Question Component

Svelte (like Astro) lets us directly add elements to the HTML head section without the need for something like React Helmet. You will see again that the code ends up being quite lightweight. Create a src/lib/components folder and add a Question.svelte file with the following content:

<script>
  export let question;
  export let answer;
  export let position;
  export let id;
  export let url;
  export let language = 'en-GB';

  const schemaOrgQuestionObject = {
    '@context': 'https://schema.org',
    '@type': 'Question',
    '@id': `${url}#${id}`,
    position,
    url: `${url}#${id}`,
    name: question,
    answerCount: 1,
    acceptedAnswer: {
      '@type': 'Answer',
      text: answer,
      inLanguage: language,
    },
    inLanguage: language,
  };
  let jsonLdString = JSON.stringify(schemaOrgQuestionObject);
  let jsonLdScript = `
        <script type="application/ld+json">
            ${jsonLdString}
        ${'<'}/script>
    `;
</script>

<svelte:head>
  {@html jsonLdScript}
</svelte:head>

<section class="container">
  <details {id} class="question">
    <summary class="summary">{question}</summary>
    <div class="answer">
      <p>
        {answer}
      </p>
    </div>
  </details>
</section>
Enter fullscreen mode Exit fullscreen mode

In lines 929, we construct the JSON-LD metadata object. This converts our questions and answers into a form which Google and other search engines can easily interpret. It follows the Schema.org Question structured data pattern. We build up a JSON object and then place it into a script tag in lines 2628. The code in line 28 is just a workaround to ensure our script tag is created as intended.

In Svelte to add something to the HTML head section, we just wrap it in a svelte:head tag. You can see this in lines 3234. Since we have oven-ready HTML we use the @html directive. You will see when we add a simple title meta as plaintext, later, this is not needed. The figure below shows how your finished meta will look in dev tools.

SvelteKit FAQ Page SEO: Dev Tools: Screenshot shows browser window wiht Dev Tools covering the bottom half.  We can see part of the HTML head section.  Shown is a snippet: &lt;script type=&quot;application/ld+json&quot;&gt;<br>
            {&quot;@context&quot;:&quot;https://schema.org&quot;,&quot;@type&quot;:&quot;Question&quot;,&quot;@id&quot;:&quot;https://sveltekit-faq-page-seo.rodneylab.com/#question-3&quot;,&quot;position&quot;:3,&quot;url&quot;:&quot;https://sveltekit-faq-page-seo.rodneylab.com/#question-3&quot;,&quot;name&quot;:&quot;Does SvelteKit use vite?&quot;,&quot;answerCount&quot;:1,&quot;acceptedAnswer&quot;:{&quot;@type&quot;:&quot;Answer&quot;,&quot;text&quot;:&quot;Yes. vite, like snowpack is a next generation bundler, building on advances earlier made by more established tools like WebPack.  Among the advantages are faster hot module reloading and better tree shaking.  These benefits come from using ES Modules.&quot;,&quot;inLanguage&quot;:&quot;en-GB&quot;},&quot;inLanguage&quot;:&quot;en-GB&quot;}<br>
        &lt;/script&gt;

Although we add the meta markup to the question component here, you may want to refactor so that all the SEO markup is included in a single script tag for each page. This will depend on the scale and complexity of your site. Keen to hear your philosophy on the ideal structure for different use cases.

If you want the page to look prettier, add some optional styling:

<style>
  .container {
    display: flex;
    background-image: linear-gradient(
      to top left,
      hsl(var(--colour-brand-hue) var(--colour-brand-saturation) var(--colour-brand-luminance)),
      hsl(
        var(--colour-brand-hue) var(--colour-brand-saturation)
          calc(var(--colour-brand-luminance) * 0.95)
      )
    );
    border-radius: var(--spacing-1);
    margin: var(--spacing-6) auto;
    width: 100%;
  }

  .summary {
    padding: var(--spacing-4);
    cursor: pointer;
    font-weight: var(--font-weight-medium);
    font-size: var(--mobile-font-size-2);
  }

  .question {
    display: flex;
    width: 100%;
  }

  .answer {
    background-image: linear-gradient(
      to bottom right,
      hsl(var(--colour-mid-hue) var(--colour-mid-saturation) var(--colour-mid-luminance)),
      hsl(
        var(--colour-mid-hue) var(--colour-mid-saturation) calc(var(--colour-mid-luminance) * 0.8)
      )
    );
    border: solid var(--spacing-px)
      hsl(var(--colour-alt-hue) var(--colour-alt-saturation) var(--colour-alt-luminance));
    border-radius: var(--spacing-px-2);
    margin: var(--spacing-0) var(--spacing-4) var(--spacing-6);
    padding: var(--spacing-0) var(--spacing-4);
    line-height: var(--line-height-relaxed);
  }

  @media (min-width: 768px) {
    .summary {
      font-size: var(--font-size-2);
    }
  }
</style>
Enter fullscreen mode Exit fullscreen mode

🧩 SvelteKit FAQ Page SEO: SEO Component

<script>
  export let datePublished = process.env.VITE_BUILD_TIME;
  export let dateModified = process.env.VITE_BUILD_TIME;
  export let seoMetaDescription;
  export let url;
  export let language = 'en-GB';
  export let faqCount;
  export let title;

  const schemaOrgWebPage = {
    '@type': ['WebPage', ...(faqCount > 0 ? ['FAQPage'] : [])],
    '@id': `${url}#webpage`,
    url,
    name: title,
    isPartOf: {
      '@id': `${url}/#website`,
    },
    primaryImageOfPage: {
      '@id': `${url}#primaryimage`,
    },
    ...(faqCount > 0
      ? {
          mainEntity: Array.from({ length: faqCount }, (_, index) => index + 1).map((element) => ({
            '@id': `${url}#question-${element}`,
          })),
        }
      : {}),
    datePublished,
    dateModified,
    description: seoMetaDescription,
    inLanguage: language,
    potentialAction: [
      {
        '@type': 'ReadAction',
        target: [url],
      },
    ],
  };
  const schemaOrgArray = [schemaOrgWebPage];
  const schemaOrgObject = {
    '@context': 'https://schema.org',
    '@graph': schemaOrgArray,
  };
  let jsonLdString = JSON.stringify(schemaOrgObject);
  let jsonLdScript = `
        <script type="application/ld+json">
            ${jsonLdString}
        ${'<'}/script>
    `;
</script>

<svelte:head>
  <title>{title}</title>
  <html lang="en-GB" />
  <meta name="description" content={seoMetaDescription} />
  {@html jsonLdScript}
</svelte:head>
Enter fullscreen mode Exit fullscreen mode

It is important that you include the FAQPage type here (line 11) for Google to recognise it as an FAQ page. The code in lines 2127 is also essential to this end. We won't look at rest in detail, so the post doesn't get too long. Open up the other SEO posts mentioned earlier for more details and explanation of what we have here.

That was the last component which we needed to add. Let's do some testing next.

💯 SvelteKit FAQ Page SEO: Testing

Everything should be working now. You can see the JSON LD markup for your questions if you open up your browser dev tools and go to Inspector, then expand the head section. To see the data clearer right click on a script tag (containing application/ld+json) and select Copy / Inner HTML. This should copy just the JSON, so you can paste it into your code editor and format it to take it easy to read.

Next, deploy a test site to your favourite hosting service then crack open Google's Rich Results Test. Paste in your site's link and check Google has spotted the FAQ meta. If there are issues, Google can be quite pedantic with this particular Schema.org type so check line by line that the meta code we added in both the Question and SEO components matches.

🙌🏽 SvelteKit FAQ Page SEO: What we Learned

In this post we looked at:

  • why you would want to add Schema.org FAQ data to your FAQ page,

  • how simple and lightweight the Svelte code for an FAQ page can be,

  • adding and testing Schema.org FAQ data to your SvelteKit FAQ page.

I do hope you can rip out this code and graft it into your own projects. There is a SvelteKit FAQ Page SEO Demo Page at sveltekit-faq-page-seo.rodneylab.com/. As well as this, you can see the full completed code for this tutorial on the Rodney Lab Git Hub repo.

🙏🏽 SvelteKit FAQ Page SEO: Feedback

Have you found the post useful? Would you prefer to see posts on another topic instead? Get in touch with ideas for new posts. Also if you like my writing style, get in touch if I can write some posts for your company site on a consultancy basis. Read on to find ways to get in touch, further below. If you want to support posts similar to this one and can spare a few dollars, euros or pounds, please consider supporting me through Buy me a Coffee.

Finally, feel free to share the post on your social media accounts for all your followers who will find it useful. As well as leaving a comment below, you can get in touch via @askRodney on Twitter and also askRodney on Telegram. Also, see further ways to get in touch with Rodney Lab. I post regularly on SvelteKit as well as other topics. Also subscribe to the newsletter to keep up-to-date with our latest projects.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .