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

@system76/satteri-relative-markdown-links

v1.1.0

Published

Rewrite relative markdown links to site page paths, as a satteri mdast plugin.

Readme

@system76/satteri-relative-markdown-links

A satteri plugin that turns relative links between Markdown files into the URLs those files actually resolve to.

Designed with Astro in mind. The URLs it produces match what Astro generates for the same file, so it's a drop-in for an Astro content collection. It only depends on satteri though, so it works with anything that uses satteri as its Markdown processor, Astro or not.

If you write a link like [the other post](./other.md) in your Markdown, the raw href points at a file on disk, not at a page. This plugin rewrites it to the real page path at build time:

./other.md            ->  /blog/other
./other.md?tab=2#top  ->  /blog/other?tab=2#top
../guides/intro.md    ->  /guides/intro

Query strings and fragments are preserved. Frontmatter slug, a custom collection layout, a site base path, and a trailing-slash policy are all taken into account.

It's a port of astro-rehype-relative-markdown-links (a rehype plugin for Astro) to satteri's mdast plugin API.

Install

pnpm add @system76/satteri-relative-markdown-links satteri

satteri is a peer dependency and should be satisfied by @astrojs/markdown-satteri or installing it seperatly.

Usage

In Astro

Astro runs satteri through the @astrojs/markdown-satteri integration, which swaps satteri in as the Markdown processor. Register this plugin in that processor's mdastPlugins:

// astro.config.mjs
import { defineConfig } from "astro/config";
import { satteri } from "@astrojs/markdown-satteri";
import { satteriRelativeMarkdownLinks } from "@system76/satteri-relative-markdown-links";

export default defineConfig({
    markdown: {
        processor: satteri({
            mdastPlugins: [satteriRelativeMarkdownLinks()],
        }),
    },
});

The defaults match Astro's own conventions, so a link ./intro.md inside src/content/guides/setup.md resolves to /guides/intro with no configuration. See Options below for the full list; if you set base or trailingSlash in astro.config.mjs, pass the same values here so the URLs match:

satteriRelativeMarkdownLinks({ base: "/docs", trailingSlash: "always" });
// ./intro.md in src/content/guides/setup.md -> /docs/guides/intro/

When a collection's URL doesn't match its directory name, or you route one at the site root, use collections:

satteriRelativeMarkdownLinks({
    collections: {
        docs: { name: "documentation" }, // src/content/docs/ref.md    -> /documentation/ref
        pages: { base: false },          // src/content/pages/about.md  -> /about
    },
});

Standalone

Nothing here depends on Astro — the src/content/<collection> layout is just the default. Pass the plugin to markdownToHtml (or mdxToJs), along with the fileURL of the file you're compiling:

import { readFile } from "node:fs/promises";
import { pathToFileURL } from "node:url";
import { markdownToHtml } from "satteri";
import { satteriRelativeMarkdownLinks } from "@system76/satteri-relative-markdown-links";

const path = "src/content/blog/post.md";
const source = await readFile(path, "utf8");

const { html } = markdownToHtml(source, {
    mdastPlugins: [satteriRelativeMarkdownLinks()],
    fileURL: pathToFileURL(path),
});

The fileURL is required here, outside Astro there's nothing else to hand satteri that file's URL, so without it the plugin has nothing to resolve links against and leaves them untouched.

If you don't organize content into collections, point contentDir at your Markdown root and drop the collection prefix:

satteriRelativeMarkdownLinks({
    contentDir: "./docs",
    collectionBase: false,
});
// ./other.md from docs/api.md -> /other

The URLs still follow github-slugger by default. If your router slugs paths differently, pass slugify to match it — see Custom slugs below.

What gets rewritten

A link is rewritten only when it points at a .md/.mdx file that exists on disk relative to the current file. Everything else is left as-is:

  • absolute URLs (https://…, mailto:…)
  • absolute paths (/docs/x.md)
  • links to non-Markdown files (./data.json)
  • links to files that don't exist
  • anything under a _-prefixed directory or file (the private-file convention)

Both inline links ([x](./y.md)) and reference-style links ([x][y] with [y]: ./z.md) are handled. Images are not touched.

Options

satteriRelativeMarkdownLinks({
    srcDir: "./src",
    trailingSlash: "never",
});

| Option | Type | Default | Description | | --- | --- | --- | --- | | srcDir | string | "./src" | Source directory. Content is resolved at <srcDir>/content. Absolute, or relative to the working directory. | | contentDir | string | <srcDir>/content | Content root, if you don't follow the src/content layout. Set this to point at any directory whose subdirectories are your collections. | | collectionBase | "name" \| false | "name" | "name" prefixes the collection directory name onto the URL (/docs/guide); false treats the collection as the site root (/guide). | | collections | Record<string, { base?, name? }> | {} | Per-collection overrides, keyed by the directory name on disk. base overrides collectionBase; name overrides the name used in the URL. | | base | string | — | Site base path prepended to every generated URL (e.g. /docs). | | trailingSlash | "ignore" \| "always" \| "never" | "ignore" | "always"/"never" force the trailing slash. "ignore" keeps whatever the link (or custom slug) already had. | | slugify | (segment: string) => string | github-slugger | Slugs a single path segment. See Custom slugs. |

Custom slugs

By default each path segment is run through github-slugger, which is the same thing Astro uses for content collections. If your site's router slugs URLs some other way, pass your own function:

satteriRelativeMarkdownLinks({
    slugify: (segment) => segment.toLowerCase().replace(/\s+/g, "_"),
});

It's called per segment. Joining the segments with / and collapsing a trailing /index are handled for you, so you only need to worry about how one segment becomes a slug.

A target file can also override its own slug from frontmatter (YAML or TOML), which wins over whatever slugify would produce:

---
slug: custom-path
---

A link to that file resolves to /<collection>/custom-path.

Environment variables

  • NODE_DEBUG=satteri-relative-markdown-links prints a trace for each link it processes.
  • SRML_MATTER_CACHE_DISABLE=true disables the in-memory cache of target-file frontmatter (it re-reads every file instead). The cache is fine for a normal build; disable it if you're editing files in a long-running process and need fresh reads.

Credits

Port of astro-rehype-relative-markdown-links. The rewrite logic follows that plugin closely.

License

GPL-3.0