This repository shows how to implement reusable Codeblock components inside of a SvelteKit project. Syntax highlighting is implemented via Shiki.
This is an alternative to the more common approach with markdown (see for example SvelteKit Shiki Syntax Highlighting: Markdown Code Blocks by Rodney Johnson).
Demo: https://codeblocks-shiki.netlify.app/
The snippets are located in the folder $lib/snippets
. This folder can be changed. For example:
/* $lib/snippets/style.css */
.section {
padding-block: 1rem;
color: #222;
}
Saving each snippet, even when it is just one line, in a separate file may sound overkill, but this is necessary for the method presented here, and it also has the advantage that your editor does the code formatting for you. (Shiki only highlights the code.)
We install Shiki with npm i shiki
.
The file codes.ts
exports a function which uses Shiki to compute an object with all HTML codes of the snippets. The keys are the file names. Here you can also adjust the supported languages and themes as well as the snippet path folder (this has to be a string literal, hence we cannot make it into a variable).
// $lib/server/codes.ts
import { getHighlighter } from "shiki";
export async function compute_codes() {
const highlighter = await getHighlighter({
theme: "dark-plus",
langs: ["html", "js", "css", "svelte"],
});
const snippets = import.meta.glob("$lib/snippets/*", {
as: "raw",
eager: true,
});
const codes = Object.fromEntries(Object.entries(snippets).map(transform));
function transform([path, file_content]: [string, string]) {
const file_name = path.split("/").at(-1)!;
const lang = file_name.split(".").at(-1);
const code = highlighter.codeToHtml(file_content, { lang });
return [file_name, code];
}
return codes;
}
To explain this a little bit, notice that Vite's import.meta.glob
returns an object whose keys are the file paths and whose values are the file contents. In the transform
function, we let Shiki operate on the file content and replace the file path by the file name. Shiki needs the language, which we can extract from the file extension.
The layout server load uses this function to make the codes available as page data.
// +layout.server.ts
export const prerender = true;
import { compute_codes } from "$lib/server/codes";
export const load = async () => {
const codes = await compute_codes();
return { codes };
};
Notice that pages with code blocks need to be prerendered, and Shiki needs to run on the server only. Otherwise there will be an error. This is why we set prerender = true
here.
When the code blocks are located only on a single page, you can also use a page server load instead.
These codes are then used in the Codeblock.svelte
component. It exports a prop snippet
and computes the rendered code via codes[snippet]
.
<!-- $lib/components/Codeblock.svelte -->
<script lang="ts">
import { page } from "$app/stores";
export let snippet = "";
const code = $page.data.codes?.[snippet];
</script>
{#if code}
<div>{@html code}</div>
{/if}
This produces rather crude-looking code blocks, though. You can improve the styling here as follows.
div :global(pre) {
font-size: 1rem;
padding: 1.25rem;
border-radius: 0.5rem;
margin-block: 1rem;
overflow: auto;
}
The last property is important, since it adds scrollbars when the code is too wide. We attach a padding to the pre
element generated by Shiki in order to keep the background color determined by the theme.
When an invalid snippet is passed to the component, nothing is rendered. In order to catch bugs during development, you can expand the if-block as follows:
{:else}
<div><strong>Invalid code snippet: {snippet}</strong></div>
{/if}
The Codeblock.svelte
component accepts the file name of one of these snippets as a prop.
<!-- +page.svelte -->
<Codeblock snippet="index.html" />
<Codeblock snippet="script.js" />
<Codeblock snippet="style.css" />
<Codeblock snippet="Counter.svelte" />
Thanks to karimfromjordan
and Patrick
on the Svelte Discord server for their help with the implementation.