DevLog: Setting Up SvelteKit for a Personal Weblog
Published October 9, 2025
Written by Carl Edward Lyons
Hey! You found my blog.
There may be other places to find my content, but if you're reading this, you're most likely reading it on my personal website. If you're not, go check it out. This is where I host my weblog articles — mostly about my ongoing projects — and experiment with new web technologies (new to me, anyway).
I built the application powering this website myself. I wanted a project to practice design architecture skills; I wanted to take my ideas and experience from past projects and apply them without the practical constraints of delivering a commercial product. These development logs (DevLogs) are part of that process, documenting my experiences, challenges, and solutions as I continue to tinker with the code. These logs are mainly an exercise in self-reflection, but I also hope that sharing my achievements and failures will help inspire others when facing similar challenges in their own projects.
Laying the Groundwork
To build the application, I decided to use the framework Svelte, and its companion meta-framework SvelteKit, because of its simplicity and performance. I wanted a modern, fast technology stack that could support server-side logic. SvelteKit provides a streamlined server-side rendering experience out of the box, with plenty of options for customizing server-client interactions. Since Svelte can compile components down to portable JavaScript, it's also a safe bet for a long-standing project that will inevitably migrate to new technologies over time. I haven't used either of these well-endorsed frameworks before myself, so I was also just excited to try something new.
With any new project, the fist step was setting up the development environment and application infrastructure — the "scaffolding" of the application that will support development. To start, I set up a new SvelteKit project with just the necessary tools to provide:
- observability — functions and integrations to log events and track errors;
- configuration management — processes to load and retrieve application settings;
- and testing strategies — the dependencies and structure to easily write and run maintainable tests.
Taking the time to create these internal interfaces early on made the application code simple and consistent right from the start.
To keep the setup simple, I began initializing the SvelteKit project in the standard JavaScript run-time environment, Node.js. Because some of the content will be dynamic, the compiled server-side application will need to be run in a Node.js environment as well. SvelteKit provides an adapter specifically for this scenario, providing the necessary build in a Node.js environment for a Node.js server.
These days, I like to use Vite whenever possible. It provides a fast and reliable development environment with a lot of useful tools:
- a development server with hot module reloading;
- Vitest, a jest-based testing framework;
- no-hassle TypeScript integration;
- and a very useful plugin ecosystem.
Using Vite for React development made my life so much easier, so I was happy to see that SvelteKit is built on top of Vite.
The initial setup was painless. Using the Vite template wizard, I created a new SvelteKit project with:
- TypeScript support,
- ESLing pre-configured,
- and Vitest installed.
Normally, I use Yarn as my package manager because I have found it more flexible when installing local packages (which can be nice for experimenting with custom dependencies). However, it seems that SvelteKit doesn't quite support Yarn yet (or at least, I couldn't get it to work). So, I used npm which gave me no issues.
From that point, I was ready to start building; I could locally host a development server and run tests. Later I would have to add a few more dependencies and tweak some package configurations. But with this initial setup being so simple and concise, all the later changes to the project configuration were easy to manage.
Setting Up the Scaffolding
I was careful to keep the initial setup minimal, only adding what I knew I would need right away. The goal was really to follow the "Don't Repeat Yourself" (DRY) motto early on to avoid unnecessary rework later. I bundled the necessary tools into modules and functions that could be easily imported and used throughout the application. Abstracting any common application logic into light wrappers and utility functions also has the added benefit of decoupling the application code from third-party dependencies.
Keeping Tabs on Things
After the initial setup, my first priority was establishing good observability — the ability to monitor the application and track errors from the outside. Debugging can often be challenging at the beginning of a project while the code settles into the framework and runtime; often times new dependencies and configurations can introduce unexpected issues that are hard to trace. Having a way to investigate the application state beyond breakpoints and simple console logs can be a real lifesaver.
One tool I always like to use is Sentry. It's a popular error-tracking service that's hard to ignore if you're a fan of Syntax.fm. I've consistently found it very easy to use, and in the case of SvelteKit, there is a simple integration that lets you handle errors both on the server and client, with automatic source map integration for easier debugging.
To handle all non-error-related logging events, I simply sent logs to the application standard output via our friend, the console. This works great during development, and since I plan to host the application in my own server environment, I can easily aggregate these logs and handle them at the system level. I later implemented the log collection using Grafana Alloy when I set up the deployment environment. To make sure the logging was efficient and consistent, I installed Pino, a fast JSON-friendly logging library, to structure and format the logs. Using the Pino-Pretty plugin, I could also format the logs nicely during development to save me from squinting at compressed JSON strings all day.
Testing, Testing, Testing
Ask any developer and they'll tell you how important testing is (at least while their boss is listening). I believe most developers understand the value of testing, but every developer knows how time-consuming and thankless writing tests can be. So, in practice, there's often compromise between comprehensive testing and shipping features. In the end, the best way to ensure tests are written consistently is to make the process as easy and painless as possible.
Since this is a personal project with the goal of learning and experimentation, I saw this as a great opportunity to try out different testing strategies. Ideally, I wanted to establish a solid testing foundation that would fully ensure the function and presentation of the application (at least against known points of failure; there's never a guarantee).
In my opinion, comprehensive testing goes beyond just code coverage — checking to see whether every line of code is exercised by a test. Even when completely covering all logic branches with unit tests, it's still possible that the application could not behave as intended. When dealing with complex, stateful applications, like a component-based web application, it can also be exhausting to unit test every possible interaction and state change.
Instead, my goal was to implement complete testing strategies that would go beyond just unit testing to satisfy code coverage, and provide confidence in the application while still being fun to develop against. Some of the tests I implemented to test package configuration and middleware are quite simple and possibly moot. In that case, the goal is just redundancy — making sure that any code I accidentally type in the wrong file doesn't torpedo deployment. In other cases, I focused on testing the code from the consumer's perspective, making sure each component does what has been promised.
Unit Testing
Unit tests are great for testing utility functions and other isolated pieces
of logic.[1]
Since most of the SvelteKit interface is functional, it's easy to write unit
tests for data loading functions and state management
utilities.[2]
Testing the data loading functions did require some additional consideration to
mock SvelteKit-specific modules,
such as the environment variable wrapper $app/environment.
Also, there is no easy way to mock SvelteKit stores
(at least without writing a full mock implementation).
So to test the stores, I chose simply to reset the application state between
tests to prevent data from bleeding over.
Beyond testing application logic, I also included unit tests for package and application configuration.[3] These tests don't directly test application functionality, but instead test that application configuration methods are called correctly during build and startup. Mocking Vite and SvelteKit modules made it easy to verify calls to configuration functions are made with the expected parameters. In practice, these tests simply preserve the exact configuration expected to deploy and run the application.
Component Testing
Component testing — by that I mean unit testing for components — instead focuses on testing functional components as a whole. These tests are especially useful for verifying user interface components, where presentation and interaction can be intertwined.
Typically when writing component tests, I find it helpful to categorize components into different types to separate concerns. In this project, I defined 3 types of components:
- material components,[4] which are simple components that bind styling, structure, and basic interaction (like buttons and form inputs);
- functional components,[5] which combine multiple material components or other functional components to provide more complex functionality (like modals and dropdowns);
- page components,[6] which are a requirement of SvelteKit for defining routes (generally prefixed with '+').
The functional[5] and page[6] components were implemented to only interface with other Svelte components and the SvelteKit framework. This made it easy to test with a the Svelte testing library, which can render Svelte components in isolation. These tests borrow heavily from component-based testing strategies, primarily focusing on the "wiring" of components together with properties and state.
Material components,[4] on the other hand, are responsible for interfacing directly with the DOM — defining DOM element composition and styling, and handling user interaction. These components are more challenging to test because they often rely on CSS for important layout and presentation. On their own, Vitest and the Svelte testing library don't provide a full browser environment to accurately compute and apply styles. Fortunately, a Vitest Browser plugin was recently released for Svelte to provide a more complete browser-like environment for testing these types of components using Playwright (or another compatible browser automation tool). Since in-browser tests are generally slower to run, these tests will follow a more state-based approach, grouping assertions into tests that verify each unique state of the component. These test then focus on capturing the intended presentation of the component, opposed to testing every individual property.
NOTE
There are no interactive elements currently planned for this project, so for now these will not need to test state transitions.
The only tricky part about component testing in SvelteKit is mocking child
components to substitute the component's dependencies during the tests.
Neither Svelte nor SvelteKit provide a built-in way to mock components.
I was able to work around this by manually mounting test components with the
createRawSnippet function
Generated components can be used to mock comprised child components with
vi.mock
and can also be passed directly as a children property.
If you want to learn about these techniques in more detail, I documented them in
a guide on mocking Svelte 5 components.
I was not able to find a reliable way to substitute for named slots, so for now
I simply avoided using them in any components.
End-to-End Testing
End-to-end (E2E) tests, unlike unit and component tests, test the entire application as a whole. These tests are useful to verify that each individually tested piece of the application works together as expected. Also, E2E tests are great for testing elements of the application that are difficult to isolate, such as routing and middleware interactions that can't be run in isolation. However, E2E tests are generally the slowest to run and difficult to maintain if the specifications are too rigid. Each test case requires a full page render, including all network requests and application logic. Small changes to the application components can easily break tests if the test assertions are too specific. So, for this project, I simply wanted to test that the application builds and starts correctly (at least within the test environment), wraps the rendered application components is the correct HTML structure, and can handle a simple HTTP request.
It would also be nice to test each route to make sure the basic functionality is in place. However, Playwright only provides an API for mocking browser requests, not requests from the server to third-party services. This will be a problem for testing any dynamically generated page that relies on external data. This is a problem I left for another day, since there does not seem to be an easy solution to put in place. It appears this will require some custom middleware to intercept server-side requests or intercepting network requests in the test environment.
Knobs and Dials
Early on, I made the decision to abstract all personalized content away from the application code. It didn't make much sense to hard-code any content that I would likely want to change. Providing configuration options as static JSON files seemed like the best way to separate content that I may want to change frequently away from my rigorously tested application logic. The configuration data are loaded during each request and made available throughout the application via SvelteKit data stores. Bundling these settings into utility modules provided a simple interface to retrieve configuration values throughout the application. Abstracting all the visual theming and text content (including localization) also meant that this application could be easily re-branded for any other long-form content site.
The implementation of the configuration management system was quite simple. But as development progressed, I found abstracting the application content was a very important part of the application structure and design.
Localization
Generally, it's always good practice to abstract out text that may need to be changed — either because the application has changed, or because the application is being used in a different language or dialectic region. Many applications use full-featured localization libraries to manage text translations and formatting. Unless the structure of the text is fluid (like placing the currency symbol before or after a price), it's usually sufficient to simply map tokens to localized strings. In this project, this can easily apply to any text being displayed, allowing the application to be completely customized to display any text content.
To achieve this with little overhead, I implemented a simple utility that creates a simple object proxy. The proxy object looks for custom text and simply falls back to placeholder text if none is provided. This means that very little customization is required to generate a page that feels complete.
Theming
While working on the Batched platform, I spent a lot of time recently sitting with the question of how to style an application. If your brand is very rigid and unlikely to change, it fine to hard-code styles directly into the web pages. However, in many cases, users expect to be able to customize the look-and-feel of their applications — be it to match their preferred light or dark mode, or to re-brand the application to match their own identity. Building white-labelled software, I learned the challenges of providing flexible theming options that enable customization while maintaining aesthetic quality. There are many ways to approach theming, but I found that creating a quality generic design requires defining a theme data structure that guarantees a consistent and complete set of style properties.
The theme structure I defined for this project defines "sections" of the application that may (or may not) be visually distinct (like the main content compared to the footer, for example). This allows the content creator to specify the complexity of the broader applications visual composition — a single theme section could be applied to the entire application, or each section could have its own distinct style definition. Within these sections, using key-referenced properties for colours, fonts, spacing, and other common settings reduces redundancy and ensures consistency across the theme.
{
"themes": {
"default": {
"sections": {
"default": {
"palette": "regal",
"background": "liner"
}
},
"palettes": {
"regal": {
"text": "#FCFAED",
"background": "#111818"
}
},
"backgrounds": {
"liner": {
"img": {
"src": "/cel-tile.svg",
},
"fill": "background"
}
}
}
}
}
The full theme definition for my my personal website is included in the deployment packages. These definitions are compiled to dereference the keyed properties into a predicable structure with concrete values.
{
"sections": {
"default": {
// section properties are dereferenced from theme definitions
"palette": {
"text": "#FCFAED",
"background": "#111818"
},
"background": {
"img": {
"src": "/cel-tile.svg"
},
// fill is resolved from the referenced section palette
"fill": "#111818"
}
}
}
}
I chose to also define backgrounds and accents graphics as part of the theme. Since it's often better to store assets separately from code repositories, it seemed like a natural step to define these assets with the theme. This makes it easy to swap out images and graphics or even host them externally if desired. The application also supports SVG images that can be styled using the colour palette defined in the theme, so coupling graphics with the theme works well.
{
"themes": {
"default": {
"sections": {
"default": {
"palette": "regal",
"graphics": "default"
}
},
"palettes": {
"regal": {
"background": "#111818",
"accent-primary": "#534260",
"accent-secondary": "#FF9F1C",
"accent-shadow": "#FCFAED"
}
},
"graphics": {
"default": {
"titleAccent": {
"src": "/chevron.svg",
// map fill of SVG class to palette colours
"colourMap": {
"img-fill-primary": "accent-primary",
"img-fill-secondary": "accent-secondary",
"img-shadow": "accent-shadow"
}
}
}
}
}
}
}
The colourMap property is compiled to CSS class definitions that apply a
fill colour to the corresponding SVG elements.
The theme definition is compiled from the JSON data to a predictable JavaScript object structure and a set of class-scoped CSS variables. I chose to expose both interfaces to provide flexibility in how styles are applied throughout the application. CSS variables are simple and efficient, separating style from both structure and function.
.theme-default .section-default .typography-body {
--font-family: 'Public Sans', sans-serif;
--font-size: 1.125rem;
--font-weight: inherit;
--font-style: inherit;
--line-height: inherit;
--letter-spacing: inherit;
--text-decoration: inherit;
--text-colour: #FCFAED;
--text-shadow: none;
}
Occasionally, it's useful to access theme properties directly in JavaScript, when style values are needed to provide functionality not possible with CSS alone.
const { getSection } = useTheme();
const section = getSection('default');
console.log(section.typography.body);
// {
// fontFamily: "'Public Sans', sans-serif",
// fontSize: '1.125rem',
// fontWeight: 'inherit',
// fontStyle: 'inherit',
// lineHeight: 'inherit',
// letterSpacing: 'inherit',
// textDecoration: 'inherit',
// textColour: '#FCFAED',
// textShadow: 'none'
// }
The contextual theming by section, typography, or graphic is established with a custom helper that conditionally synchronizes the CSS class context with the SvelteKit context.
<script lang="ts">
import useThemes from '$lib/hooks/useThemes';
const { makeProvider } = useThemes();
const { children } = $props();
// set the theme section context for child components
const { provider } = makeProvider({ sectionKey : 'error' });
</script>
// apply the theme section CSS class to root component element
<div class={provider.className}>
{@render children}
</div>
Implementing this system was less complicated than it sounds. Most of the trouble was defining an intuitive system for resolving missing properties with sensible fallbacks or defaults.
Both the CSS and JavaScript interfaces test well. CSS variables can be set on the browser test container to simulate different themes. The JavaScript theme object can be easily mocked to provide different test values without needing to load a full theme definition.
The biggest challenge with this theming approach will be coordinating multiple concurrent themes. The server generated pages will need to be resolved with the client's selected theme. To present a personalized theme to each user based on their preferences, either the server must render each page with the user's requested theme, or the client must re-style the default page rendered by the server before painting it to the screen (something that SvelteKit does not like). If the server does not render the page with the user's requested theme and the client does not successfully re-style the page before painting, the user may see a flash of unstyled content (FOUC) before the application finally resolves the user's settings.
An Empty Application
With all the necessary tools in place, it was time to create a simple proof-of-concept. To avoid getting bogged down in content creation, I wanted to create a simple page that could be rendered to verify that everything was working correctly. Instead of a meaningless "Hello, World!" notice, I decided to implement some simple error handling, and add a route that would intentionally trigger an error. With the logging and error tracking systems in place, I could fully implement this feature while verifying that errors were being captured and reported. And by forgoing any actual content, the application was functionally empty yet still complete.
Fully implementing this test error route could also validate the rest of the
application infrastructure, besides just observability.
Each error code returns a different error message, defined by the localization.
The error page is styled using the application theming system, with colours,
fonts, and a graphic defined by the theme (using the error section).
Learning to Love SvelteKit
This first iteration of the application was undoubtedly a success, despite its underwhelming presentation. The interfaces I had implemented and testing strategies I had developed made building out the application extremely manageable. Since then, adding content to the SvelteKit application has been frictionless.
However, setting up the SvelteKit application was not without its challenges.
It was frustrating at times to work around limitations of the framework,
especially when the documentation was vague or just incomplete.
For example, mocking SvelteKit-specific modules for testing was not obvious from
the documentation.
The solution I found was pieced together from Stack Overflow answers
and many examples of the createRawSnippet
function from various issues (like this one).
And I'm sure the solution I implemented is not officially supported, so I expect
it may break in future releases.
Beyond the challenges of mocking components, it was disappointing that SvelteKit
doesn't provide an elegant way to isolate state between tests.
Overall, though, I do see the appeal of SvelteKit over other popular front-end frameworks. The simplicity of Svelte's component model makes it easy to implement — so long as you don't need fine-grained control over rendering or component state. It's also worth noting that SvelteKit, and the significantly redesigned Svelte 5, are both relatively new. So, hopefully, some of these more inconvenient deficiencies will be addressed as the frameworks mature.
To separate universal and server-only code, utilities are organized into their own respective directories within the
src/lib/directory. ↩︎Data loading for the application is handled when generating the main layout by a data loading function that is also tested in an adjacent test file. ↩︎
To test that configurations are correctly applied to the Vite project and middleware, Vite configuration tests and tests for both client and server SvelteKit hooks are included in the source repository. ↩︎
The initial iteration included several material components to populate the error pages, each with their own tests. ↩︎ ↩︎
The first release of the application didn't include any functional components. However, version 0.0.2 includes examples with tests. ↩︎ ↩︎
The Svelte components defined in the routes folder are used to define the SvelteKit routing. The error page is a simple example that also has its own test file. ↩︎ ↩︎
Edited by Renata Soljmosi