npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@immich/svelte-markdown-preprocess

v0.8.0

Published

Readme

@immich/svelte-markdown-preprocess

Renders markdown as Svelte components, and serves the collected front matter and headings back to the app through a virtual module.

The package has two halves, and they are used together:

| Export | Kind | Configured in | | -------------------------- | ------------------- | ------------------ | | svelteMarkdownPreprocess | Svelte preprocessor | svelte.config.js | | svelteMarkdownVite | Vite plugin | vite.config.ts |

The preprocessor turns each .md file into a Svelte component and wraps it in an optional layout. The Vite plugin scans the same files and serves them as virtual:docs, so a layout is handed the parsed doc for its own page and the app can list every doc without importing any markdown into the browser.

Setup

Four changes are needed to start using them.

1. svelte.config.js

Register the preprocessor and tell SvelteKit that .md files are routes.

import { svelteMarkdownPreprocess } from '@immich/svelte-markdown-preprocess';
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';

const config = {
  extensions: ['.svelte', '.md'],
  preprocess: [
    svelteMarkdownPreprocess({
      layouts: {
        default: '$lib/components/MarkdownPage.svelte',
      },
    }),
    vitePreprocess(),
  ],
};

export default config;

layouts.default is used for every markdown file. A file can pick another one with a layout key in its front matter, matching a key in layouts.

2. vite.config.ts

import { svelteMarkdownVite } from '@immich/svelte-markdown-preprocess';
import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vite';

export default defineConfig({
  plugins: [sveltekit(), svelteMarkdownVite()],
});

3. src/app.d.ts

Pull in the ambient declaration for virtual:docs.

/// <reference types="@immich/svelte-markdown-preprocess/virtual" />

4. The layout component

The preprocessor looks up the current page and passes it as a doc prop.

<script lang="ts">
  import type { ClientDoc } from '@immich/svelte-markdown-preprocess';
  import type { Snippet } from 'svelte';

  type Props = {
    doc?: ClientDoc;
    children?: Snippet;
  };

  const { doc, children }: Props = $props();
</script>

<h1>{doc?.attributes.title}</h1>

{@render children?.()}

doc is optional because markdown outside of src/routes still gets a layout, but has no entry in the collection.

The virtual module

import { getDoc, getDocs } from 'virtual:docs';

const all = getDocs();
const one = getDoc('(shell)/blog/(posts)/sync-v2/+page.md');

getDoc takes the doc's path - the file's location relative to src/routes, layout groups included. Both are generic over the doc type, so pass yours to get it back:

const posts = getDocs<BlogPost>();

Every doc is at least a ClientDoc:

type ClientDoc = {
  path: string;
  attributes: FrontMatterAttributes; // parsed front matter
  headers: DocHeader[]; // level 2 and 3 headings, ids matching the rendered anchors
};

Markdown bodies are never serialized into the module, so no post content reaches the browser.

Validating and reshaping docs

onDoc runs at build time for each file and decides what ships. It receives a ServerDoc, which is a ClientDoc plus the markdown body. Throwing fails the build, and returning undefined leaves the doc out of the collection.

svelteMarkdownVite({
  onDoc: ({ path, attributes, headers }) => {
    const parsed = FrontMatterSchema.safeParse(attributes);
    if (!parsed.success) {
      throw new Error(`${path} has invalid front matter`);
    }

    return { path, attributes, headers, ...parsed.data };
  },
});

A doc may gain properties, but never lose the ones on ClientDoc - the return type is constrained to T extends ClientDoc, and getDoc keys the collection on path.

Options

svelteMarkdownPreprocess(options)

| Option | Default | Purpose | | ------------ | ----------------- | ---------------------------------------------- | | extensions | ['.md', '.mdx'] | file extensions to treat as markdown | | layouts | {} | layout component per front matter layout key |

svelteMarkdownVite(options)

| Option | Default | Purpose | | ------------ | ----------------- | ------------------------------------------- | | extensions | ['.md', '.mdx'] | file extensions to collect | | onDoc | strips body | validate and reshape a doc, or leave it out |

The module id (virtual:docs) and the scanned directory (src/routes) are fixed, and exported as VIRTUAL_ID and DOCS_DIR.

Utilities

| Export | Purpose | | ---------------------------- | ------------------------------------------------------------- | | getHeaders(body, levels?) | headings of a markdown body, with anchor ids | | getHrefFromPath(path) | route of a page, with layout groups removed | | getIdFromText(text) | anchor id used for a heading, so links match what is rendered | | isMarkdownPath(path, ext?) | whether a path is markdown | | parseFrontMatter(content) | { attributes, body } of a markdown file | | markedText(markdown) | markdown rendered to plain text, for search indexes |

Development

Editing a markdown file invalidates virtual:docs and triggers a full reload, so headings and front matter stay current without restarting the dev server.