Watch out: All SvelteKit page data ends up on the client

My mistake

This site (at the time of writing!) is a SvelteKit app that gets rendered to static files. I write Markdown, and it gets turned into HTML.

Since the number of blog posts is not hardcoded, SvelteKit follows internal <a> links to the blog posts and discovers them that way. Even without this behavior, I'd want links going to all the posts so you can find them. To do this, I crawl a directory containing all the blog posts and create a object for each one:

export type Post = {
  path: string;
  title: any;
  description: any;
  date: Date;
  dateMachine: string;
  dateHuman: string;
  html: string;
};

This type includes all the data needed to do anything blog post related. The path and title help make the <a> link to the post. The date is used to sort them in the blog index, and dateMachine and dateHuman render a <time>. The tricky part is that it also has the entire html content of the blog post. This is used to render the static pages but is not needed in the blog index at all.

In a traditional server rendered web app like one using Django templates, it's no big deal to query all the columns of a model: it may pull down a few more bytes from the DB, but if you don't write things to the template it won't get included in the response and sent back to the client. Using the same muscle memory in a SvelteKit app causes boneheaded things to happen, though. In my case, because I created the full Post objects in the blog index page data, I was accidentally sending the HTML for every single blog post to the client to render that page.

// routes/blog/+page.server.ts

export const load: PageServerLoad = async () => {
  const postDirs = await readdir('content/blog');
  let posts = await Promise.all(postDirs.map((dir) => getBlogPost(dir)));
  // Sort by date descending
  posts = posts.sort((p1, p2) => p2.date.getTime() - p1.date.getTime());

  return {
    posts: posts,
    title: 'Blog'
  };
};

The fix

The fix I ended up on was to split the single type into two:

export type PostMeta = {
  path: string;
  title: string;
  description: string;
  date: Date;
  dateMachine: string;
  dateHuman: string;
};

export type Post = PostMeta & {
  html: string;
};

Then the blog index can make PostMetas to render its listing and the blog page can make a full Post to include the HTML.

The only impact here was sending a couple hundred unnecessary kilobytes, but making this same mistake in a worse context could send sensitive data down to the client if you use your DB types in page data.