build a SvelteKit/strapi website

build a SvelteKit/strapi website

It is spring 2021 and somehow SvelteKit got released in an early stage. I am really thrilled about the next version of svelte/sapper because in the last months my excitement about Svelte/Sapper only got bigger.

Today (or yesterday) the SvelteKit docs got public and even SvelteKit is in an early stage public beta and should not be used on production, I updated my sapper/strapi example to work with SvelteKit too.

project setup

To get the example project running, you need to prepare strapi first. After a checkout run npm run setup to configure all jwts strapi needs.
Install strapi npm packages and build strapi's backend, create a admin user, setup public role permissions to navigation & page and create some pages and the navigation.

Now travel to the SvelteKit directory install npm packages and type npm run dev and see the result in your browser.

If you are interested into strapi things, please have a look at my last blog post about this topic build a sapper/strapi website.

SvelteKit setup

I started the SvelteKit directory from scratch with the npm init svelte@next command.

The concept how SvelteKit consumes strapi:
* src/routes/[...slug].svelte catchall route calling strapi to load page content
* src/routes/$layout.svelte calls strapi to load navigation entries
* src/routes/_strapi/index.js provides api calls
* src/lib/Strapi.svelte handles the content components from strapi

data loading / error handling / redirecting

An improvement over sapper is the renamed load() function in the script context="module" block, and fetch is now a property of the loadOptions Object and not anymore bound the the component itself. This helps you to write a capsulated api call class/function.

As return object the load() function should return a loaded object. You can create errors with your return, as well as injecting property data into your component:

<!-- returning property data into your component -->
<script context="module">
export async function load() {
  …
  return {
    // remember to return component props within the props property
    props: {
      property1,
      property2,
    },
  };
};
</script>

</script>
  export let property1;
  export let property2;
</script>
/* create an error page */
export async function load() {
  …
  return {
    status: res.status,
    error: new Error(`Could not load ${url}`);
}
/* redirect to another page */
export async function load() {
  …
  return {
    redirect: '/',
  };
};

loadingOptions object

The load() function is called with the loadOptions object only. This way you can destructure everything you need and leave everything else aside. This is really nice, no more parameter ordering to remember.

type LoadOptions = {
	page: {
		host: string;
		path: string;
		params: Record<string, string | string[]>;
		query: URLSearchParams;
	};
	fetch: (url: string, opts?: {...}) => Promise<Response>
	session: any;
	context: Record<string, any>;
};

private modules

Usually all files in the /routes directory will used for request mapping. With private modules you can store a file in your routes folder but it will be ignored for request mapping.
In my example, I used /routes/_strapi/index.js (the leading underscore makes a module private) to capsulate my api call code.

components

Like before, script context="module" is only provided in route components, so I need to load the navigation entries at $layout level an not within the Nav component.

Compared to the sapper code of this example, I needed to adjust my Nav component to get the store running again, where I listen to the current page to highlight the navigation entry:

  import { getStores } from '$app/stores';
  export let navPages = [];

  const { page } = getStores();

  page.subscribe(({ path }) => {
    navPages = navPages.map((page) => ({
      ...page,
      active: activePage(page.url, path),
    }));
  });

  function activePage(slug, path) {
    if (path === undefined && slug === '/') {
      return 'page'
    }
    if (path === slug) {
      return 'page';
    }
    return undefined;
  }

Other adjustments to my components were not needed - this project is focussed to be a small proof of concept, on more complex components there can be more changes from sapper to SvelteKit.

strapi content component

The content component mapping from strapi to SvelteKit components is all done in the src/lib/Strapi.svelte component.

Every strapi page content component has an equivalent SvelteKit component in /src/lib/strapi/<strapi-component-name>.svelte. These components are all imported into /src/lib/Strapi.svelte and used if needed:

<!-- /src/lib/Strapi.svelte -->
<script>
  import Text from './strapi/Text.svelte';
  import Image from './strapi/Image.svelte';

  const componentMap = {
    'page.text': Text,
    'page.image': Image,
  };

  export let contents = [];
</script>

{#each contents as content}
<svelte:component
  this={componentMap[content.__component]}
  {content}
/>
{/each}

adapters

This example is designed to run as a serverside rendered (SSR) app with rehydration. If you want a static rendered app, you can change the adapter and you are done. Take a look at the documentation to see all adapaters available, or how to build your own adapter.

my impression of SvelteKit

SvelteKit is a great step forward from sapper. The local development is blazingly fast thanks to vite, the bundle size still is ultra small and developing with SvelteKit feels better than ever.

I did not get lost in the migration process - and on this small codebase I started from scratch and moved the sapper pieces into the sveltekit project. This experience will differ on bigger codebases, to see what is needed to move from sapper to SvelteKit check out the migration guide.

You should wait for your production usage until the developer team of SvelteKit says so. Until than I will follow the SvelteKit process and update this project when things change or break.

Article Image by Alexandru Tudorache via unsplash and ghost