@atprism/noat
v0.0.2
Published
Node + AT protocol
Readme
noat
Node + AT protocol. Publish your blog posts to bluesky via a command line tool.
The noat command (pronounced like "note") reads posts from a git repo and
publishes any markdown files that do not have AT_URL in frontmatter.
It uploads a post excerpt plus a backlink to your blog URL.
I made this for a specific project. I am going to be publishing a collection of images, and I wanted a copy both on my website and on Bluesky.
Install
npm i -S @atprism/noatExample
npx noat publishThis will publish all posts to bluesky using the defaults for everything.
This assumes you have a noat.config.js file in the root with a
value for handle, and a .env file with a variable
NOAT_BLUESKY_APP_PASSWORD defined.
Publish
State is kept in the markdown files' frontmatter. Any file with a field
AT_URL is considered to have been published already.
- Requires a clean git state (no uncommited changes)
- Posts with an
AT_URLfrontmatter field are treated as already published. - Only posts missing
AT_URLare published. - All local state is kept in the markdown frontmatter
- After publishing,
noatwritesAT_URLinto the frontmatter for each file, and creates a commit:AT proto publish <n>.
CLI
npx noat publishHelp
npx noat helpConfig
noat auto-loads the first file found in this order:
noat.config.tsnoat.config.jsnoat.config.json
Options
noat reads config first, then applies CLI flags on top.
If both are set, the CLI value wins.
--config <path>: Explicit config file path (CLI-only).--handle <value>: Bluesky handle.--pds-url <value>: Bluesky PDS URL.--posts <path>: Markdown posts directory.--password-env-var <value>: Env var name containing app password.--post-text-field <value>: Frontmatter field used for post text.--base-url <value>: Base URL prepended to frontmatterslug.--dry-run: Show what would publish without sending API requests.--verbose: Print resolved config details.--cwd <path>: Run as if launched from another working directory.
Example noat.config.js:
export default {
cwd: '.',
handle: 'your-handle.bsky.social',
pdsUrl: 'https://bsky.social', // <-- default
posts: './posts', // <-- default
postTextField: 'post', // <-- excerpt field, default is post text
baseUrl: 'https://blog.example.com/blog',
passwordEnvVar: 'NOAT_BLUESKY_APP_PASSWORD', // <-- default
dryRun: false,
verbose: false
}Config fields
handle(required): account handle.pdsUrl(optional): defaults tohttps://bsky.social.passwordEnvVar(optional): defaults toNOAT_BLUESKY_APP_PASSWORD.posts(optional): defaults to./posts.postTextField(optional): frontmatter field used for post text, defaults topost.baseUrl(required): base URL prefixed to frontmatterslugto build the post backlink.cwd(optional): working directory for publish operations.dryRun(optional): same behavior as--dry-run.verbose(optional): same behavior as--verbose.
Config-file keys and matching CLI flags
handle->--handlepdsUrl->--pds-urlposts->--postspasswordEnvVar->--password-env-varpostTextField->--post-text-fieldbaseUrl->--base-urlcwd->--cwddryRun->--dry-runverbose->--verbose
Path resolution rules:
postsis resolved relative to the config file directory.- If no config file is loaded,
postsis resolved relative to current working directory.
Environment Variables
Store the app password in a local .env file (not committed):
NOAT_BLUESKY_APP_PASSWORD="xxxx-xxxx-xxxx-xxxx"Post format
Posts are markdown files with YAML frontmatter.
Example:
---
title: Launch post
post: "Git is now the source of truth for publishing."
slug: "launch"
---
Body content for your static site.
Publishing rules
- Bluesky text comes from frontmatter field
post(or your configuredpostTextField). - If that field is missing, fallback text is derived from markdown body with image markdown removed.
- A backlink to the blog post is inserted into every Bluesky post.
- Backlink URL is built as
<baseUrl>/<slug>whenslugexists. - If
slugis missing, backlink URL falls back to the markdown file's local path + filename (without extension) relative toposts. - If no backlink URL can be resolved, publish fails for that post.
- If the backlink is already in the text, it is not duplicated.
AT_URLis the publish marker. If present, that post is skipped.- After a successful publish,
noatwritesAT_URLwith the Bluesky app URL (for examplehttps://bsky.app/profile/<handle>/post/<id>). - The first markdown image in the body (
) is used as the uploaded blob. - Image alt text comes from that markdown image's alt text.
- Only repository file paths are supported for images
(no
http://,https://, ordata:URLs). - Supported image types:
.jpg,.jpeg,.png,.gif,.webp,.avif.
Frontmatter after publish includes:
AT_URL: "https://bsky.app/profile/your-handle.bsky.social/post/3laz2abc"Backlink examples:
baseUrl: "https://abc.com/blog"with frontmatterslug: "foo"becomeshttps://abc.com/blog/foo.baseUrl: "https://abc.com/blog/"with frontmatterslug: "/weekly note/"becomeshttps://abc.com/blog/weekly%20note.- With no
slug,posts/2026-02-01-launch.mdbecomeshttps://abc.com/blog/2026-02-01-launch.
Example
See ./example.
Run from repo root:
node ./dist/cli.js publish --config ./example/noat.config.js --dry-run --verboseThen run without --dry-run to publish.
Notes
- Requires Node.js 18+ (uses global
fetch). - If git is dirty, publish exits with an error and does not post.
