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

@plugdash/fromsubstack

v0.2.0

Published

Import a Substack export into EmDash - posts, metadata, and images

Readme

@plugdash/fromsubstack

Migrate from Substack to EmDash in minutes. Registers Substack as an import source in EmDash's admin importer: upload the export ZIP you get from Substack's Settings > Exports, and this plugin reads posts.csv plus each post's HTML, converts bodies to Portable Text, and reports images so EmDash can bring them in too.

Install

pnpm add @plugdash/fromsubstack

Register

// astro.config.mjs
import { defineConfig } from "astro/config";
import emdash from "emdash/astro";
import { fromsubstackPlugin } from "@plugdash/fromsubstack";

export default defineConfig({
	integrations: [
		emdash({
			plugins: [fromsubstackPlugin()],
		}),
	],
});

The plugin registers a Substack import source (createSubstackSource()). EmDash 0.41 and 1.0 have no import screen for sources other than WordPress, so drive the source from your own script until the admin UI supports it. Remove the plugin after the import.

Configuration

Pass options to fromsubstackPlugin():

fromsubstackPlugin({
	targetCollection: "newsletter",
	status: "published",
	importImages: true,
	preserveSlugs: true,
	paidPostsAs: "draft",
});

| Option | Type | Default | Description | | ---------------- | -------------------------------- | --------- | ------------------------------------------------------------------------------------------------- | | targetCollection | string | "posts" | Collection posts import into | | status | "draft" \| "published" | "draft" | Status for posts Substack had marked published | | importImages | boolean | true | Report body images so EmDash imports them | | preserveSlugs | boolean | true | Keep the slug from the Substack post URL | | paidPostsAs | "draft" \| "publish" \| "skip" | "draft" | What to do with paid posts (only_paid, founding). Applied even when status is "published" |

Warnings (skipped rows, empty bodies) are returned as warnings on the analyze() result and logged with console.warn during fetchContent().

status defaults to draft on purpose - review imported posts before they go live. A post that was still a draft on Substack always imports as a draft, no matter what status is set to.

What gets imported

From each row in posts.csv with a matching HTML file:

  • Title, subtitle (as excerpt), publish date, and slug (from the post's Substack URL, or re-slugified from the title if preserveSlugs: false)
  • The body, converted from HTML to Portable Text - headings, bold/italic, links, images, blockquotes, fenced code with language detection, and lists
  • The first body image as the featured image
  • Substack boilerplate is dropped: the subscribe widget, subscribe buttons, share and comment bars, and footnote anchors (footnote text stays)
  • Paid/subscriber-only status, recorded in metadata (meta.substackPaid, meta.substackAudience) rather than hidden - EmDash doesn't have a native concept of a Substack paywall, so this is left for you to act on

Podcast episodes and other non-article post types in the export are skipped with a warning - there's no article body to convert. A CSV row with no matching HTML file, or a slug that collides with one already imported, is also skipped with a warning rather than failing the whole run.

What it does not do

  • Does not create content on its own - it's a source for EmDash's importer, which does the actual writing
  • Does not guarantee pixel-perfect HTML - unknown tags are unwrapped and their text kept, not preserved as-is
  • Does not resolve Substack's paywall into an EmDash paywall - see @plugdash/paygate if you need one
  • Does not deduplicate against posts already in EmDash from a previous, different import path - only duplicates within the same export are caught