@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