Skip to content

Commit a6cc5c3

Browse files
authored
feat: syndicate blog posts to ATProto via standard.site lexicons (#404)
1 parent 1341b91 commit a6cc5c3

19 files changed

Lines changed: 1905 additions & 59 deletions

File tree

.github/workflows/bluesky.yml

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -85,15 +85,41 @@ jobs:
8585
BLUESKY_APP_PASSWORD: ${{ secrets.BLUESKY_APP_PASSWORD }}
8686
run: ./target/release/server publish-bluesky --dir notes
8787

88-
- name: Commit bsky_url updates
88+
- name: Init standard.site publication
89+
# No-op when `publications.toml` already has `at_uri` + `at_cid`
90+
# cached (see `init_publication` in `standard_site.rs`). First run
91+
# after merge creates the publication record on the PDS and writes
92+
# the URI/CID back; later runs short-circuit without touching the
93+
# network. Failure here is fatal — sync depends on it.
94+
#
95+
# `APP_BASE_URL` + `IMGPROXY_URL` are required so init can fetch the
96+
# publication's branded OG card (rasterized PNG) and upload it as the
97+
# publication cover blob. This depends on the Fly Deploy that
98+
# preceded this workflow having the `/og/publication/{key}.svg`
99+
# route live — the `workflow_run` chain enforces that ordering.
100+
id: init_standard
101+
env:
102+
BLUESKY_IDENTIFIER: ${{ secrets.BLUESKY_IDENTIFIER }}
103+
BLUESKY_APP_PASSWORD: ${{ secrets.BLUESKY_APP_PASSWORD }}
104+
IMGPROXY_URL: ${{ secrets.IMGPROXY_URL }}
105+
run: ./target/release/server publish-standard-site init blog
106+
107+
- name: Sync standard.site
108+
id: sync_standard
109+
env:
110+
BLUESKY_IDENTIFIER: ${{ secrets.BLUESKY_IDENTIFIER }}
111+
BLUESKY_APP_PASSWORD: ${{ secrets.BLUESKY_APP_PASSWORD }}
112+
run: ./target/release/server publish-standard-site sync --key blog
113+
114+
- name: Commit syndication updates
89115
run: |
90116
git config user.name "${{ steps.app-token.outputs.app-slug }}[bot]"
91117
git config user.email "${{ steps.app-token.outputs.app-slug }}[bot]@users.noreply.github.com"
92-
git add notes/
118+
git add notes/ blog/ publications.toml
93119
if git diff --staged --quiet; then
94120
echo "No changes to commit"
95121
else
96-
git commit -m "Add bsky_url to published notes"
122+
git commit -m "Sync syndication state"
97123
# Rebase onto whatever main looks like *now* — covers the case
98124
# where main moved during this workflow run.
99125
git pull --rebase origin main
@@ -103,7 +129,7 @@ jobs:
103129
- name: Surface publish failures
104130
# Marks the run red so failures are visible. Runs last so partial
105131
# successes still got committed/pushed/deployed via the steps above.
106-
if: steps.publish.outcome == 'failure'
132+
if: steps.publish.outcome == 'failure' || steps.sync_standard.outcome == 'failure'
107133
run: |
108-
echo "::error::One or more notes failed to publish. See the 'Publish to Bluesky' step. Next run will retry."
134+
echo "::error::One or more publishes failed. See logs. Next run will retry."
109135
exit 1

.github/workflows/fly_review.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,5 +63,5 @@ jobs:
6363
uses: superfly/fly-pr-review-apps@1.2.1
6464
with:
6565
name: coreyja-com-pr-${{ github.event.number }}
66-
secrets: APP_BASE_URL="https://coreyja-com-pr-${{ github.event.number }}.fly.dev" DATABASE_URL="${{ steps.create-branch.outputs.db_url }}?sslmode=require" TWITCH_CLIENT_ID="FAKE" TWITCH_CLIENT_SECRET="FAKE" TWITCH_BOT_ACCESS_TOKEN="FAKE" TWITCH_BOT_USER_ID="FAKE" TWITCH_CHANNEL_USER_ID="FAKE" GITHUB_APP_ID="123" GITHUB_APP_CLIENT_ID="FAKE" GITHUB_APP_CLIENT_SECRET="FAKE" GITHUB_APP_PRIVATE_KEY="FAKE" GITHUB_PERSONAL_ACCESS_TOKEN="FAKE" OPEN_AI_API_KEY="FAKE" GOOGLE_CLIENT_ID="FAKE" GOOGLE_CLIENT_SECRET="FAKE" ENCRYPTION_SECRET_KEY="FAKE" JOBS_DISABLED="true" CRON_DISABLED="true" DISCORD_TOKEN="FAKE" DISCORD_BOT_DISABLED="true"
66+
secrets: APP_BASE_URL="https://coreyja-com-pr-${{ github.event.number }}.fly.dev" DATABASE_URL="${{ steps.create-branch.outputs.db_url }}?sslmode=require" TWITCH_CLIENT_ID="FAKE" TWITCH_CLIENT_SECRET="FAKE" TWITCH_BOT_ACCESS_TOKEN="FAKE" TWITCH_BOT_USER_ID="FAKE" TWITCH_CHANNEL_USER_ID="FAKE" GITHUB_APP_ID="123" GITHUB_APP_CLIENT_ID="FAKE" GITHUB_APP_CLIENT_SECRET="FAKE" GITHUB_APP_PRIVATE_KEY="FAKE" GITHUB_PERSONAL_ACCESS_TOKEN="FAKE" OPEN_AI_API_KEY="FAKE" GOOGLE_CLIENT_ID="FAKE" GOOGLE_CLIENT_SECRET="FAKE" ENCRYPTION_SECRET_KEY="FAKE" JOBS_DISABLED="true" CRON_DISABLED="true" DISCORD_TOKEN="FAKE" DISCORD_BOT_DISABLED="true" ANTHROPIC_API_KEY="FAKE" LINEAR_CLIENT_ID="FAKE" LINEAR_CLIENT_SECRET="FAKE" LINEAR_WEBHOOK_SECRET="FAKE"
6767
memory: 512

Cargo.lock

Lines changed: 65 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@ tower = "0.4.13"
112112
tower-cookies = "0.11.0"
113113
tower-http = "0.5.0"
114114
typify = "0.0.14"
115+
toml = "0.8"
115116
url = "2.5.0"
116117
urlencoding = "2.1"
117118
vergen = "8.0.0"

blog/look-ma-no-ai/index.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
---
22
title: Look Ma’ no AI
3+
subtitle: An apology, and a recommitment to writing the words myself.
34
author: Corey Alexander
45
date: 2026-05-27
56
---

posts/src/blog.rs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,9 +236,25 @@ pub struct BlogFrontMatter {
236236
pub buttondown_id: Option<String>,
237237
/// Absolute URL of an OG image to use instead of the auto-generated branded card.
238238
pub og_image: Option<String>,
239+
/// Optional short subtitle/tagline shown on the OG card in place of the
240+
/// post date. When unset, the OG card renders the post date.
241+
pub subtitle: Option<String>,
239242
#[serde(default)]
240243
pub tags: Vec<String>,
241244
pub author: Option<String>,
245+
/// AT URI of the `site.standard.document` record on the PDS, set after
246+
/// the first successful sync. Acts as the idempotency key — its presence
247+
/// means the document record has been created, so subsequent syncs use
248+
/// `putRecord` against the same rkey rather than creating a new record.
249+
pub atproto_uri: Option<String>,
250+
/// Publication this post belongs to. Defaults to `"blog"` and corresponds
251+
/// to an entry in `publications.toml`.
252+
#[serde(default = "default_publication")]
253+
pub publication: String,
254+
}
255+
256+
fn default_publication() -> String {
257+
"blog".to_string()
242258
}
243259

244260
impl PostedOn for BlogFrontMatter {
@@ -321,8 +337,11 @@ mod test {
321337
newsletter_send_at: None,
322338
buttondown_id: None,
323339
og_image: None,
340+
subtitle: None,
324341
tags: vec![],
325342
author: None,
343+
atproto_uri: None,
344+
publication: "blog".to_string(),
326345
};
327346
let post = BlogPost {
328347
path,
@@ -357,8 +376,11 @@ mod test {
357376
newsletter_send_at: None,
358377
buttondown_id: None,
359378
og_image: None,
379+
subtitle: None,
360380
tags: vec![],
361381
author: None,
382+
atproto_uri: None,
383+
publication: "blog".to_string(),
362384
},
363385
}
364386
}
@@ -380,4 +402,12 @@ mod test {
380402
let post = test_post("weekly/20230713/index.md", false);
381403
assert_eq!(post.og_slug(), "weekly/20230713");
382404
}
405+
406+
#[test]
407+
fn frontmatter_defaults_apply_when_fields_absent() {
408+
let yaml = "title: T\ndate: 2026-05-01";
409+
let fm: BlogFrontMatter = serde_yaml::from_str(yaml).unwrap();
410+
assert_eq!(fm.publication, "blog");
411+
assert!(fm.atproto_uri.is_none());
412+
}
383413
}

publications.toml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
[[publication]]
2+
key = "blog"
3+
title = "coreyja.com"
4+
description = "Personal blog: Rust, side projects, Battlesnake, AI agents."
5+
url = "https://coreyja.com/posts"
6+
content_dir = "blog"
7+
collection = "site.standard.document"

server/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ arborium = { workspace = true }
5454
html-escape = { workspace = true }
5555
thiserror = { workspace = true }
5656
tokio = { workspace = true }
57+
toml = { workspace = true }
5758
tower = { workspace = true, features = ["util"] }
5859
tower-http = { workspace = true, features = ["trace", "cors"] }
5960
tracing = { workspace = true }

0 commit comments

Comments
 (0)