How to prerender static pages with pagination in SvelteKit

Posted on:

SvelteKit supports prerendering pages at build time. A page will be prerendered if:

  • the entire site is statically generated (all pages are prerendered)
  • the page is listed in the prerender.entries field of the config file
  • explicitly configured to be prerendered on a per page basis

TIP: You can find the completed code example at meowdevx/svelte-pagination-example.

Prerender in SvelteKit

Static Generated Site

SvelteKit provides an static adapter that can prerender all the pages within the site at build time. To use it, install it with npm i --save-dev @sveltejs/adapter-static then update the svelte.config.js file:

import adapter from '@sveltejs/adapter-static';

export default {
  kit: {
    adapter: adapter()
  }
};

This mode is suitable if all the pages will be rendered the same for everyone. The advantage of this mode includes:

  • great for SEO since all the content will be server side rendered during build time
  • performant, only static files are served
  • easier to deploy

Explicitly list which pages should be prerendered

If only a subset of the pages can be prerendered, you can configure SvelteKit to only prerender those pages using the prerender config option:

export default {
  kit: {
    prerender: {
      entries: [
        '/product/123',
        '/product/234'
        // ...
      ]
    }
  }
};

With the above config, SvelteKit will prerender pages located at /product/123, /product/234. Of course, it's not really feasible to list all the pages here explicitly if you have thousands of them. Luckily, it's pretty easy to run a script to generate the paths during build and import it here. Reece May has an excellent blog post covering details on how to programmatically generate the list of pages to prerender.

Prerender a specific page

The third option to configure SvelteKit to prerender a page is to configure it explicitly on the page. Example:

index.svelte

<script>
  export const prerender = true;
</script>

When SvelteKit process this page, it will try to prerender this page at build time. By default, SvelteKit will also crawl the page to find all the links within the page and prerender them as well. This allows us to create a "dynamic" route that is rendered at build time by simply enumerate those pages from a "static" page.

The rest of the post will create a simple blog site the demonstrate how to create a statically rendered site with pagination support.

Static site with pagination

The routes

- src
  -- routes
     -- index.svelte // home page
     -- posts
        -- [page].svelte // render a specific page of posts

In this example site, we will have an index page that links to all the pages of blog posts. Because index links to all the paginated pages, SvelteKit will also prerender all those pages even though page like posts/1 are rendered with a dynamic page number.

Fake API

In this example, we will use {JSON} Placeholder as the API that provide data for the blog.

In lib/api.ts file, we export two functions:

// Returns the total count of posts and the per page limit
export async function getPostStats(fetch: Fetch): Promise<PostStats> {
  //...
}

// Returns an array of post for the requested page
export async function getPosts(page: number, fetch: Fetch): Promise<Array<Post>> {
  //...
}

Complete source: lib/api.ts

Index page

routes/index.svelte

<script context="module" lang="ts">
  import { getPostStats } from '$lib/api';
  import type { Load } from '@sveltejs/kit';

  export const load: Load = async ({fetch}) => {
    const { maxPages } = await getPostStats(fetch);
    return {
      props: {
        maxPages
      }
    };
  }
</script>

In SvelteKit, load function are called before the component/page are rendered, on both server side and client side. The above load function calls the API to find out how many pages there are and then pass that as a prop to the page.

In a second <script></script> section, we can receive the props returned by the load function as:

<script lang="ts">
export let maxPages: number;

// create an array filled with [1...maxPages]
const pages = [...Array(maxPages).keys()].map((p) => p + 1);
</script>

Finally, we can render links to each separate page in the template section like:

<ul>
  {#each pages as page}
    <li><a href={`/post/${page}`}>Page {page}</a></li>
  {/each}
</ul>

Complete source: routes/index.svelte

Post page

On post page, we will render a list of blog posts for that specific page. Since the page number is a dynamic, we need to use the [] naming convention in SvelteKit so that when a user request post/10, SvelteKit matches the 10 in the URL to the page param and route the request to be handled by routes/post/[page].svelte page. This is very similar to Next.js and Nuxt.js.

routes/post/[page].svelte:

<script context="module" lang="ts">
  import { getPage, getPostStats, Post } from '$lib/api';
  import type { LoadInput } from '@sveltejs/kit';

  export const load: Load = async ({params, fetch}) => {
    const pageNumber = parseInt(params.page);
    const posts = await getPage(pageNumber, fetch);
    const { maxPages } = await getPostStats(fetch);
    return {
      props: {
        page: pageNumber,
        maxPages,
        posts
      }
    };
  }
</script>

load function declared in the page is called with the dynamic page param passed in via page object. Remember, the value passed in is of string type so we need to parse it as an int before using it as a number.

Again, we fetch the data using the API then pass the data to the page using props:

<script lang="ts">
  export let maxPages: number;
  export let page: number;
  export let posts: Array<Post>;
  $: hasPrev = page > 1;
  $: hasNext = page < maxPages;
</script>

Finally we can render the page using the props. See the complete source for more detail.

Not totally static

If we run npm run build and deploy our application at this point, everything seems to work as expected. However, if we inspect the network tab of the page in dev tools, we will see that the page is still making an HTTP request to the placeholder API. That is unexpected. What's going on? The page should be completely static.

Turns out, even for prerendered pages, SvelteKit still inject some Javascript on the page to enable hydration and client side routing. And remember early we mentioned that load functions run on both server side and client side. In this case, there is no server side rendering but the client side rendering is still happening. To fix this and make the page completely static, we have two options:

  • disable hydrate, router site wide in svelte.config.js

    export default {
      kit: {
        //...
        hydrate: false,
        router: false
      }
    };
    

    This option is suitable if the entire site is static and prerendered.

  • disable hydrate, router on a per page basis via page export

    <script context="module" lang="ts">
      export const hydrate = false; export const router = false;
    </script>
    

    This option is suitable if you only need some specific page to be rendered without any javascript on the page.