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/socialcard

v0.2.0

Published

Share images for EmDash posts - renders a PNG card on publish and sets it as the post's SEO image

Readme

@plugdash/socialcard

@plugdash/socialcard - posts shared on X, LinkedIn, Slack or Facebook show up as a bare link when they have no share image. On publish, this plugin draws a 1200x630 PNG card with the post title, author and date, stores it in your media library, and sets it as the post's SEO image. Your template's existing og:image tag picks it up with no template changes. The EmDash equivalent of Social Image Generator / Yoast's OG image feature.

Install

pnpm add @plugdash/socialcard

Register

It is a native plugin, so it goes in plugins, not sandboxed.

// astro.config.mjs
import { defineConfig } from "astro/config";
import emdash from "emdash/astro";
import { socialcardPlugin } from "@plugdash/socialcard";

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

How it works

  1. You publish a post (or a page, or any collection with SEO enabled).
  2. After the publish response goes out, the plugin builds the card as SVG and rasterises it to PNG with resvg-wasm.
  3. The PNG is uploaded as og-<id>.png (image/png) through ctx.media.upload, so it lives wherever your media lives (local uploads/, R2, S3).
  4. The plugin sets the entry's seo.image to the media URL, for example /_emdash/api/media/file/01M3....png. This is an SEO-only update: it goes live at once and does not create a draft.
  5. getSeoMeta() turns that root-relative path into an absolute URL using the siteUrl you pass it. The blog template passes Astro.url.origin, so og:image always matches the host the page was served from. EmDashHead then writes the og:image and twitter:image tags.

If your template does not use getSeoMeta(), read post.seo.image and prefix it with Astro.url.origin.

What it skips

  • Unchanged posts. The plugin hashes title, author, publish date, your config and the plugin version, and keeps the hash in KV. Re-publishing without a change logs card unchanged, skipping render and does nothing else.
  • Autosaves and draft saves. Only content:afterPublish is hooked.
  • A hand-set SEO image. If someone sets seo.image in the admin, the plugin leaves it alone unless you pass overwriteSeoImage: true.
  • Collections without SEO support. Logged once per collection, then ignored.

When a card is redrawn (say the title changed), the new PNG is uploaded first and the old media item is deleted, so cards do not pile up in your media library.

Configuration

socialcardPlugin({
	template: "bold",
	background: "#111827",
	foreground: "#fef3c7",
	logo: "https://example.com/logo.png",
});

| Option | Type | Default | Description | | ----------------- | -------------------------------------- | ----------- | ------------------------------------------------------------ | | template | "default" | "minimal" | "bold" | "default" | Card layout | | width | number | 1200 | Card width in pixels | | height | number | 630 | Card height in pixels | | background | string (hex) | "#0f172a" | Background colour | | foreground | string (hex) | "#f8fafc" | Text (and, on minimal, paper) colour | | logo | string | none | Logo in the top-left corner: http(s) URL or file path (Node) | | fontFiles | string[] | none | Extra TTF/OTF fonts: http(s) URLs or file paths (Node) | | overwriteSeoImage | boolean | false | Replace an SEO image someone set by hand |

Changing any option changes the hash, so every post gets a new card on its next publish.

Templates

  • default - large title, author and date byline, subtle gradient
  • minimal - title only, clean paper background
  • bold - large type against a strong colour block

Long titles (over 80 characters) are cut with an ellipsis. The author comes from a data.author field (string or { name }), then the post's primary byline. A missing author or date drops that half of the byline.

Fonts

The package bundles Lexend Regular and Bold, Latin subset (about 24 KB each, SIL Open Font License, see OFL-Lexend.txt). Titles in other scripts need a font that has those glyphs, or they render as empty boxes. Add one with fontFiles; it is used as a fallback for any character Lexend does not have:

socialcardPlugin({
	// Hindi titles
	fontFiles: ["./fonts/NotoSansDevanagari-Bold.ttf"],
});

Use a file path on Node, or an http(s) URL on any runtime. Prefer a subset or a single weight: the font is loaded on every render.

Performance

Measured on Node 24, Apple M-series, default 1200x630:

  • First render after a server start: about 80-110 ms (includes compiling the WASM module)
  • Later renders: about 20-45 ms
  • PNG size: about 310-340 KB for default (the gradient does not compress well), about 37 KB for minimal, about 56 KB for bold

The work runs after the publish response, so editors do not wait for it. The hook timeout is 30 seconds, mostly to bound a slow logo or font fetch.

resvg and the fonts are loaded with await import() inside the hook. A normal page request never loads them.

What it does not do

  • Does not write metadata.ogImage any more (0.1.x did). Use seo.image.
  • Does not draw cards for drafts or on autosave
  • Does not delete the card when a post is unpublished
  • Does not fetch Google Fonts by name. Pass a font file with fontFiles.
  • Has no admin page. Configure it in astro.config.mjs.

Cloudflare Workers

The Workers path is not verified yet. workerd does not allow compiling WASM from bytes, so on Workers the plugin imports @resvg/resvg-wasm/index_bg.wasm?module and expects the bundler to hand it a compiled module. File paths in logo and fontFiles do not work there; use URLs.