Sharing each new post by hand is easy to forget, and Bluesky is open enough that a short script can do it for you. This version watches your blog’s feed and posts the title and a clickable link whenever something new appears.
Make a Bluesky app password
In Bluesky, open Settings, then Privacy and security, then App passwords, and add one called Blog poster. Copy it; Bluesky only shows it once. An app password can post for you but can't change your account password.
Find your blog's feed
Most blogs and newsletters have an RSS feed. Try your site's address with
/feed,/rssor/feed.xmlon the end. You should see a page of XML that lists your posts.Start a new script and store the password
Go to script.google.com, click New project and name it Blog to Bluesky.
Click the gear icon (Project Settings), scroll to Script properties, and add a property named
BSKY_APP_PASSWORDwith the app password as its value. Keeping it here means it isn't written in the code.Paste the script
Back in the editor, replace everything with this and fill in your handle and feed address:
const HANDLE = 'you.bsky.social'; const FEED_URL = 'https://example.com/feed'; function postNewItems() { const props = PropertiesService.getScriptProperties(); const seen = JSON.parse(props.getProperty('seen') || '[]'); const channel = XmlService.parse(UrlFetchApp.fetch(FEED_URL).getContentText()) .getRootElement().getChild('channel'); const items = channel.getChildren('item').slice(0, 5).reverse(); // First run: remember existing posts instead of sharing them all. if (seen.length === 0) { props.setProperty('seen', JSON.stringify(items.map((i) => i.getChildText('link')))); return; } const fresh = items.filter((i) => seen.indexOf(i.getChildText('link')) === -1); if (fresh.length === 0) return; const session = bsky('com.atproto.server.createSession', { identifier: HANDLE, password: props.getProperty('BSKY_APP_PASSWORD'), }); for (const item of fresh) { const title = item.getChildText('title'); const link = item.getChildText('link'); const start = bytes(title + '\n\n'); bsky('com.atproto.repo.createRecord', { repo: session.did, collection: 'app.bsky.feed.post', record: { $type: 'app.bsky.feed.post', text: title + '\n\n' + link, createdAt: new Date().toISOString(), facets: [{ index: { byteStart: start, byteEnd: start + bytes(link) }, features: [{ $type: 'app.bsky.richtext.facet#link', uri: link }], }], }, }, session.accessJwt); seen.push(link); } props.setProperty('seen', JSON.stringify(seen.slice(-50))); } function bsky(method, body, token) { const res = UrlFetchApp.fetch('https://bsky.social/xrpc/' + method, { method: 'post', contentType: 'application/json', headers: token ? { Authorization: 'Bearer ' + token } : {}, payload: JSON.stringify(body), }); return JSON.parse(res.getContentText()); } // Bluesky counts link positions in bytes, not characters. function bytes(text) { return Utilities.newBlob(text).getBytes().length; }Run it twice to test
Click Run once and allow access: the first run only remembers your existing posts. To check posting works, remove the newest post's link from the
seenscript property in Project Settings and run it again. That post should appear on Bluesky.Check every hour
Open Triggers, click Add trigger, choose
postNewItems, Time-driven, Hour timer, Every hour, and save.
Paste this into Claude Code, Codex or any coding agent. The agent writes and sets up the automation, then tells you what to click.
Set up automatic Bluesky posts for new items in my blog's feed: <PASTE FEED URL>. My Bluesky handle is <HANDLE>. - Use Google Apps Script with an hourly trigger (or a scheduled GitHub Actions workflow if I say I prefer GitHub). - Read the feed (RSS or Atom), and on the first run only record existing items so old posts aren't shared. - For each new item, log in with com.atproto.server.createSession using an app password stored outside the code (Script Properties or a GitHub secret), then create an app.bsky.feed.post record with the title and link. - Make the link clickable with a link facet; facet offsets are UTF-8 byte positions, end exclusive. Keep the post under 300 characters. - Add a link card (app.bsky.embed.external) with the post's title, description and image if the page has Open Graph tags. - Tell me where to paste or commit it and what to click.
You still need to
- Create the Bluesky app password yourself and store it where the agent tells you.
- Sign in to Google and click Allow when the script first runs.
Things that trip people up
- Links in Bluesky posts are only clickable when the post includes a link facet with byte positions. The script handles this; hand-written code often forgets.
- If your account lives on your own server instead of bsky.social, change the address in the bsky function to your server's.
- Posts are limited to 300 characters, so very long titles may fail to post.