@mastrojs/atproto

June 15, 2026 · View on GitHub

Helper scripts to integrate with the Atmosphere.

Create and update standard.site records from your existing website without the headache. No need to store rkeys in your YAML frontmatter or database. Instead, we derive them from the URL paths of your existing website. For more info, see our blog post.

How?

The easiest way to get started, is to set up a new Mastro project and select the blog template.

But you can use this library with any stack that allows you to get your blog posts into a JavaScript variable. Simply call createOrUpdateStandardSite(session, publication, docs) whenever your blog posts changed – e.g. for a statically generated site, on each deploy.

Install

Deno

deno add jsr:@mastrojs/atproto

Node.js

pnpm add jsr:@mastrojs/atproto

Bun

bunx jsr add @mastrojs/atproto

Usage

Create a new file, e.g. publishToAtmosphere.ts, with the following content and adjust with your settings:

import fs from "node:fs/promises";
import { createOrUpdateStandardSite, type Publication } from "@mastrojs/atproto";
import { readMarkdownFiles } from "@mastrojs/markdown";

const identifier = "your.bsky.social";
const password = process.env.ATPROTO_PASSWORD;
const pubUrl = new URL("https://example.com/news/");

const publication: Publication = {
  url: pubUrl,
  name: "Peter's News",
  description: "",
  // Optional square image for the publication, should be at least 256x256:
  icon: {
    blob: await fs.readFile("icon.png"),
    mimeType: "image/png",
  },
  // Optional RGB colors, make sure you have enough contrast:
  basicTheme: {
    background: { r: 255, g: 255, b: 255 },
    foreground: { r: 23, g: 24, b: 28 },
    accent: { r: 0, g: 0, b: 0 }, // button color
    accentForeground: { r: 255, g: 255, b: 255 }, // button text
  },
};

const posts = await readMarkdownFiles<{title: string; date: string}>("data/posts/*.md");
const docs = posts.map((p) => ({
  title: p.meta.title,
  publishedAt: new Date(p.meta.date),
  url: new URL(p.slug + "/", pubUrl),
}));

await createOrUpdateStandardSite({ identifier, password }, publication, docs);

Then create an app password and run the above script with it:

Deno

ATPROTO_PASSWORD=xxxx-xxxx-xxxx-xxxx deno run -A publishToAtmosphere.ts

Node.js

ATPROTO_PASSWORD=xxxx-xxxx-xxxx-xxxx node publishToAtmosphere.ts

Bun

ATPROTO_PASSWORD=xxxx-xxxx-xxxx-xxxx bun publishToAtmosphere.ts

If you confirm to the script that everything looks good, it will create a publication in the Atmosphere, and write a file to routes/.well-known/site.standard.publication (The routes prefix is what may be public in other frameworks than Mastro – use the opts argument of createOrUpdateStandardSite to customize).

After that, run it a second time to publish your documents to the Atmosphere. Optionally, you can then set up your CI/CD pipeline to run the script on each deploy.

Don't forget to add the following link tag to your document detail pages using import { rkeyFromUrl } from "@mastrojs/atproto";

<link rel="site.standard.document"
  href={`at://${agent.did}/site.standard.document/${rkeyFromUrl(doc.url)}`}>

You can use e.g. https://site-validator.fly.dev or pdsls.dev to verify standard.site records on the PDS. To browse and edit records manually, use Taproot.

To see all functions @mastrojs/atproto exports, see its API docs.

Contribute

This project is happy to accept bug reports and/or contributions! Just open a GitHub issue or talk to us on Bluesky