This is my personal website, built with SvelteKit.
First install the dependencies:
npm i --force
We are using force because svelte-i18n does not yet officially support Svelte 5
Then you are ready to start the development server:
npm run dev
This project uses Prettier and Eslint for code formatting. You
can npm run format
and npm run lint
to format and lint the code.
It is recommended to configure your editor to run these commands on save.
For webstorm, you can configure this in Settings > Tools > Actions on save
.
├── locales `Locales json for i18n`
│─┬ src
│ ├── components `Reusable components`
│ ├─┬ lib `Used for easy access with $lib/`
│ │ ├── network `Network related code (like fetching data)`
│ │ ├── data `Repository pattern for accessing and manipulating data`
│ │ ├── dtos
│ │ ├── enums
│ │ └── types.ts `Shared types for DI`
│ ├── params `Parameter matching for use with slugs in routes`
│ └── routes `Pages and layouts`
├── .env.example
├── prettier.config.js
├── .eslint.config.ts
├── inversify.config.ts `Used for DI bindings`
├── playwright.config.ts
├── svelte.config.js
├── tailwind.config.ts `Color definitions`
├── tsconfig.json
└── vite.config.ts
Environment variables are defined in a .env
file. You can find an example in .env.example
.
Reusable components are stored in the components
directory.
To allow for type completion when using HTML attributes on custom components, the following pattern can be used:
<script lang="ts">
import type { HTMLAttributes } from "svelte/elements";
interface Props extends HTMLAttributes<HTMLDivElement> {
customProp1: string;
customProp2: string;
}
let {
customProp1,
customProp2,
...props
}: Props = $props();
</script>
<div {...props} {customProp1} {customProp2}>
<slot />
</div>
Here we extend HTMLAttributes<HTMLDivElement>
on the Props
to tell our component that it can accept
HtmlAttributes, any custom props will also need to be defined in the Props
interface.
This should also be done if no HTMLAttributes are required.
This project uses Tailwind CSS for styling. You can find the configuration
in tailwind.config.js
, this is where all the colors are defined. If custom styles are necessary I recommend using scss
instead of CSS.
To enable dark mode for a component use the matching *-themed
class defined in app.scss
.
This project uses svelte-i18n for internationalization. You can find the
locales in the locales
directory.
To use a translation in a component, import the t
function from svelte-i18n
and use it like this:
<script lang="ts">
import { t } from "svelte-i18n";
const tBase = "page.something."
</script>
<div>{$t(tBase + "key")}</div>
Here the tBase
variable is used to define a base path for the translations. This is useful for keeping translation
keys short and readable.
This project makes use of injection with the help of InversifyJS.
Before we can use an object, we need to bind it to a type in the inversify.config.ts
file.
First define a type in the types.ts
file:
const types = {
classType: Symbol.for("ClassType"),
};
Then bind the type to an implementation in the inversify.config.ts
file:
import { container } from "./inversify.config";
container.bind(types.classType).to(ClassImplementation);
// or
container.bind(types.classType).toConstantValue(new ClassImplementation());
const class = container.get<Class>(types.classType);
For interacting with API data, the application uses a repository pattern.
This is to allow for easy mocking of data in tests.
Repositories are defined in the lib/data
directory and use the aforementioned DI.
When creating a new repository, it should extend the BaseRepository
class and implement an interface:
interface ExampleRepository {
getExample(): Promise<ExampleDto>;
}
It is recommended to use a DTO (Data Transfer Object) for the data returned from the API. A standard implementation of the repository would look like this:
class FetchExampleRepository implements ExampleRepository {
private apiClient: ApiClient;
constructor(apiClient: ApiClient) {
this.apiClient = apiClient;
}
async getExample(): Promise<ExampleDto> {
const result = await this.fetcher.get<ExampleDto>("/example");
return result.json();
}
}
Here an ApiClient
can be used to make network requests.
After the repository is created, it should be bound and used like described in the injection section.
This project uses unplugin icons for getting different icons. Unplugin icons use Iconify for getting the icons. If you need to find a specific icon look in the icon sets.
This project uses vitest for unit testing and Playwright for
end-to-end testing.
You can run the tests with npm test
, Or use npm run test:unit
and npm run test:integration
to run the tests separately.
Vitest supports coverage reports, to generate a coverage report
use npm run test:coverage
.
Coverage reports will be placed in test-results/coverage
.
Tests are located in the tests folder; when creating a new test file, it should have the same directory structure in
tests as the file you are testing in src. All test files should end with .test.ts
and integration tests (using
Playwright) should end with .integration.test.ts
.
To create a production version of the app:
npm run build
You can preview the production build with npm run preview
.
For development, the project included the following commands to quickly build and preview the production build:
npm run build:preview
npm run build:start
The following command can be used to start the app after building:
npm run start
This project is set to be deployed in a docker container. The Dockerfile is included in the project root.
To build the docker image:
docker build -t wiemansite .
A docker-compose file is also included in the project root. To start the app with docker-compose:
docker-compose up -d
The following environment variables are required for the app to run:
These are dynamic and are read on runtime.