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

@hedgerow/publish

v0.3.1

Published

Publish markdown posts as standard.site atproto records to a PDS.

Readme

@hedgerow/publish

Publish markdown posts as portable site.standard.* records in your own AT Protocol repository, and read them back. Your writing lives on the open network in a repo you control, rather than inside one host's database.

Part of Hedgerow. This is the author's side of the toolkit — see @hedgerow/comments and @hedgerow/react for rendering the social layer on your site, and @hedgerow/reader for a visitor's own identity.

New applications normally consume the curated hedgerow/site and hedgerow/node entry points. This scoped package remains the lower-level protocol and compatibility surface.

Install

npm install @hedgerow/publish

Node ≥ 20. ESM-only. The generated Astro starter requires Node ≥ 22.12.

Entry points

This package splits deliberately, because half of it must be safe to bundle for a browser:

| Import | Contains | Safe in a browser | | --- | --- | --- | | @hedgerow/publish/site | Record shapes/builders, CID-safe document operations, and unauthenticated publication reads | Yes | | @hedgerow/publish | Legacy compatibility surface, including frontmatter parsing and filesystem reconciliation | Do not use for new browser bundles | | @hedgerow/publish/node | The compatibility surface plus oauthPublisher, openInBrowser, clearSession, and FileStore | No — uses node:http, node:fs |

A Node script or CLI should import @hedgerow/publish/node. A browser integration should normally use hedgerow/site; lower-level browser code can use @hedgerow/publish/site. The bare entry point stays intact for existing consumers, but its historical frontmatter parser is intentionally not part of the browser-focused subpath.

Publishing

Authentication is atproto OAuth with granular publication, document and discussion-post permissions — there is no password or app-password to store. The first publish opens a browser; the session is then cached (in ~/.config/hedgerow) and silently refreshed until you sign out.

import { oauthPublisher, publishSite, parsePost } from "@hedgerow/publish/node";

// Restores a cached session, or runs the browser login the first time.
// `identifier` is an optional hint — omit it to pick the account in the browser.
const publisher = await oauthPublisher({ identifier: "you.bsky.social" });

const posts = [parsePost(markdown, "my-first-post")];
const result = await publishSite(
  publisher,
  { url: "https://you.com", name: "you" },
  posts,
);

Login uses the atproto loopback client flow: a native client id and a throwaway callback server on 127.0.0.1. Nothing to host, no client secret. Because a record write requires a real login, there is deliberately no headless publish path — a human completes the browser step once, then reruns are non-interactive off the cached session.

Sign out with clearSession() (aliased logout()); pass { identifier } to drop just one account.

Persist the state, or you'll create duplicates

publishSite returns a PublishState recording which record key it used per slug. Persist it between runs (e.g. .publish-state.json) — that is what makes reruns idempotent, targeting the same record instead of creating a second one.

import { readFile, writeFile } from "node:fs/promises";
import { emptyState } from "@hedgerow/publish/node";

const state = await readFile(".publish-state.json", "utf8")
  .then(JSON.parse)
  .catch(() => emptyState());

const result = await publishSite(publisher, config, posts, state);

await writeFile(".publish-state.json", JSON.stringify(result.state, null, 2));

Reading it back in is the half that matters — publishSite without a state argument starts from empty and will create a second record for every post.

Two related behaviours worth knowing:

  • updatedAt only moves on a real change. Republishing compares against the existing record ignoring updatedAt; if nothing else changed, the write is skipped entirely. It stays an honest "last edited", not "last ran the script".
  • textContent is always written. Every document mirrors plaintext into textContent alongside the richer content, so a standard.site reader that knows nothing about Hedgerow's markdown member still renders something.

Verify what landed in your repo at pdsls.dev.

Editing safely in a browser

The recommended high-level API is a publication-bound capability:

import { author } from "hedgerow/site";

const writer = author(session, { ownerDid, publicationUri });
await writer.updateDocument({ snapshot: current, document: draft });

It checks the signed-in DID, injects publication membership and exposes no raw collection/rkey selection. The lower-level operations below remain available for integrations that supply their own capability boundary.

createDocument, updateDocument, and deleteDocument own the protocol details an editor should not reimplement. New records receive a TID key; updates keep the same AT URI and use the CID loaded with the document as a compare-and-swap token.

import { readSite, updateDocument } from "@hedgerow/publish";

const site = await readSite(authorDid, fetch, { publicationUri });
const current = site.documents[0];
if (!current?.uri || !current.cid) throw new Error("Document is not published");

const updated = await updateDocument(publisher, {
  uri: current.uri,
  cid: current.cid,
  document: {
    site: current.value.site,
    path: current.value.path,
    title: "Corrected title",
    publishedAt: current.value.publishedAt,
    markdown,
  },
});

The update replaces the value at the same record key; it does not create a second article. If another tab or client changed the record after it was loaded, the write throws RecordConflictError and preserves the newer value.

Always select a publication by publicationUri or publicationUrl when a DID can own more than one site. Reads return the publication and document CIDs. Hedgerow only edits pub.hedgerow.content.markdown; documentMarkdown() throws for an unknown rich-content union member instead of converting and destroying it silently.

Connecting a post to its comments

bskyPostRef is a strongRef on the document pointing at a real Bluesky post that hosts the canonical thread. The document record is not itself the comment target — the conversation lives on Bluesky, and the record names which post to read replies and likes from. That is what lets @hedgerow/comments render a live thread against a post you actually made.

Testing

The full publish → PDS → read loop is tested against an in-process PDS from @atproto/dev-env — no credentials, no Docker, no domain, no live account. What genuinely needs the real world (OAuth login, custom-domain handle resolution) lives in a manual checklist instead. See the repo README.

Stability

Pre-1.0. While this package is on 0.x, a minor version bump may contain breaking changes — npm resolves ^0.1.0 as >=0.1.0 <0.2.0, so a caret range is already safe. Pin exactly (0.1.0) or use ~0.1.0 if you want to be conservative. See CONTRIBUTING.md.

License

MIT