Import a site's articles (via its RSS/Atom feeds) into the Publications section of a Sifa ID profile, with deduplication of what already exists (Warning : STILL WIP !!!)
README.md

sifa-rss-import #

Import a site's articles (via its RSS/Atom feeds) into the Publications section of a Sifa ID profile, with deduplication of what already exists.

Under the hood: Sifa is an AppView that reads the records from a user's atproto PDS. This tool therefore writes id.sifa.profile.publication records straight into your repo — they then show up automatically on your Sifa profile.

Warning

Records written directly to your PDS may not be indexed by the Sifa AppView yet. Sifa indexes via the firehose and has a known firehose-gap with no reconcile backstop in place, so a bulk import can land valid records in your PDS that never surface on your profile (getProfileView returns publications: 0). This is a Sifa-side limitation, not a bug in this tool — your data is correctly stored and owned by you. Tracking issue: singi-labs/sifa-workspace#399.

How it works #

  1. Discovery — reads the <link rel="alternate" type="application/rss+xml"> tags from the site (fallback: /index.xml, /feed, … for Hugo & co).
  2. Parsing — RSS 2.0 + Atom (rss-parser).
  3. Deduplication — lists the publications already present (com.atproto.repo.listRecords) and compares by normalized URL.
  4. Writing — creates only the missing ones (com.atproto.repo.createRecord), with a concurrency limit and retries.

RSS → id.sifa.profile.publication mapping:

RSS/Atom Sifa field
<title> title
<link> url
feed title publisher
<description> description
<pubDate> publishedAt (YYYY-MM-DD)
— createdAt (now)

Requirements #

  • Node.js ≥ 20
  • An account on an atproto PDS (your Sifa/Bluesky handle)

Install #

npm install

Authentication (two modes) #

Dev loopback mode — default, no hosting required. The OAuth client_id is a loopback client (http://localhost?redirect_uri=...&scope=...): the authorization server recognizes it without fetching any URL. This is the ideal mode for personal use from your machine — nothing to drop on your site.

Hosted mode — for automation. atproto then requires the client_id to be a public HTTPS URL serving client-metadata.json. See Automation below.

1. Authorize (once) #

npm run login -- une-tasse-de.cafe

A browser opens for authorization; the callback comes back to http://127.0.0.1:8765/callback. The session (refresh token + DPoP key) is persisted in ~/.config/sifa-rss-import/sessions.json — subsequent runs refresh without a browser. The command prints your DID to reuse.

2. Import #

# Preview without writing anything:
npm run import -- https://une-tasse-de.cafe --handle did:plc:hqnyog7skad6m4aejb2yujxy --dry-run

# For real:
npm run import -- https://une-tasse-de.cafe --handle did:plc:hqnyog7skad6m4aejb2yujxy

Options: --dry-run (writes nothing), --limit <n> (caps the number of creations), --include <s> / --exclude <s> (repeatable URL filters, see below). Re-running is idempotent: only new entries are created.

Filtering out non-article pages #

A site's root feed often aggregates standalone pages (an "about" page, a CV, an archive index…) alongside real articles. Restrict the import with --include (keep only URLs containing the substring) and/or --exclude (drop matching URLs); both are repeatable and match anywhere in the URL:

# Keep only /blog/ and /expresso/ articles:
npm run import -- https://une-tasse-de.cafe --handle did:plc:hqnyog7skad6m4aejb2yujxy \
  --include /blog/ --include /expresso/

# Or drop specific pages:
npm run import -- https://une-tasse-de.cafe --handle did:plc:hqnyog7skad6m4aejb2yujxy \
  --exclude /cv/ --exclude /talks/

Scope #

The default requested scope is atproto transition:generic (works everywhere, covers repo writes). The clean, granular scope is the id.sifa.authProfile permission set (it grants exactly id.sifa.profile.* + id.sifa.graph.follow); its support on atproto authorization servers is still rolling out. To try it:

SIFA_OAUTH_SCOPE="atproto include:id.sifa.authProfile" npm run login -- une-tasse-de.cafe

Automation (Tangled) #

For a recurring, non-interactive import, switch to hosted mode:

  1. Host client-metadata.json at a public HTTPS URL — on your Hugo site, drop it into static/ and deploy (→ https://une-tasse-de.cafe/client-metadata.json). The file's client_id must match that URL exactly.
  2. Set SIFA_CLIENT_ID=https://une-tasse-de.cafe/client-metadata.json: this enables hosted mode (loads client-metadata.json).
  3. Run login once in this mode to obtain a hosted session.

Since the refresh token is persisted, subsequent runs work without a browser. On Tangled, use a pipeline (not a GitHub Action): inject the contents of sessions.json as a secret (with SIFA_DATA_DIR pointing to a directory rebuilt at run time) and run npm run import -- <site> --handle <did>.

License #

MIT