Skip to content

feat: Notes content type + Bluesky POSSE syndication#366

Open
byte-the-bot wants to merge 6 commits intomainfrom
implement/BLOG-6a79edf75fad4d29
Open

feat: Notes content type + Bluesky POSSE syndication#366
byte-the-bot wants to merge 6 commits intomainfrom
implement/BLOG-6a79edf75fad4d29

Conversation

@byte-the-bot
Copy link
Collaborator

Summary

  • Replaces TIL content type with broader "Notes" supporting optional kind field (til, link)
  • Migrates 15 existing TIL files to notes/ with kind: til and adds permanent /til/*/notes/* redirects
  • Adds Bluesky API client and publish-bluesky CLI command for POSSE syndication with idempotency via bsky_url frontmatter field
  • Creates GitHub Actions workflow to auto-syndicate new notes to Bluesky on deploy
  • Notes pages display Bluesky thread comments when bsky_url is present

Test plan

  • cargo test -p posts — 17 tests pass (notes module, migration verification)
  • cargo test -p server bluesky — 31 tests pass (API client, CLI command, facets, formatting)
  • cargo test -p server test::validate — 1 test passes (content validation)
  • cargo clippy -- -D warnings — clean
  • cargo fmt --check — clean
  • Verify /til/ routes redirect to /notes/ in browser
  • Test publish-bluesky with real Bluesky credentials

Implements BLOG-6a79edf75fad4d29

🤖 Generated with Claude Code

Comment on lines +45 to +71
- name: Find unpublished notes
id: find
run: |
unpublished=""
for file in notes/*.md; do
if [ -f "$file" ]; then
if ! grep -q "bsky_url:" "$file"; then
# Check if date is >= 2026-03-01
date_line=$(grep "^date:" "$file" | head -1)
if [ -n "$date_line" ]; then
post_date=$(echo "$date_line" | sed 's/date: //')
if [[ "$post_date" > "2026-02-28" ]] || [[ "$post_date" == "2026-03-01" ]]; then
unpublished="$unpublished $file"
fi
fi
fi
fi
done

unpublished=$(echo "$unpublished" | xargs)
echo "paths=$unpublished" >> $GITHUB_OUTPUT

if [ -n "$unpublished" ]; then
echo "Found unpublished notes: $unpublished"
else
echo "No unpublished notes found"
fi
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we fold all this into a cli command too

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 74b3ad5 — the find-unpublished-notes shell logic has been folded into the CLI. The command now takes --dir and handles scanning for unpublished notes internally. The workflow just calls ./target/release/server publish-bluesky --dir notes.


/// Format the post text for Bluesky, truncating body if needed to stay within 300 chars
fn format_post_text(title: &str, body: &str, url: &str) -> String {
// Format: "{title}\n\n{body}\n\n{url}"
Copy link
Owner

@coreyja coreyja Mar 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I want to leave the url off.

And is title optional? or I think I want a way to hide the title from bluesky optionally if its not

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 74b3ad5 — URL is removed from the post text (the website card embed handles linking instead). Title is included by default but can be hidden by setting bsky_hide_title: true in the note's frontmatter.

if content.contains("bsky_url:") {
println!("Note already has bsky_url, skipping: {}", path.display());
return Ok(());
}
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lets do this check after we parse the frontmatter

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 74b3ad5 — the string-based content.contains("bsky_url:") pre-parse check has been removed. The idempotency check now happens after parsing frontmatter via frontmatter.bsky_url.is_some() in find_unpublished_notes.

byte-the-bot and others added 3 commits March 11, 2026 09:02
Test scaffolds for notes content type + Bluesky syndication:
- posts/src/notes.rs: NoteKind enum, FrontMatter with optional kind/bsky_url, NotePosts loading
- server/src/bluesky.rs: AT URI to web URL conversion, facet byte offsets, serialization
- server/src/commands/bluesky.rs: frontmatter parsing, strip_markdown, format_post_text, update_frontmatter

All tests use #[ignore] since the production types don't exist yet.
…cation

Migrate 15 TIL files to notes/ with optional kind field (til, link).
Add permanent redirects from /til/* to /notes/*. Create Bluesky API
client for POSSE syndication with publish-bluesky CLI command and
GitHub Actions workflow for automated posting on deploy.

Implements BLOG-6a79edf75fad4d29
Leave markdown formatting as-is when posting notes to Bluesky
rather than stripping bold, italic, backticks, headings, and links.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…parsed frontmatter for idempotency

- Add strip_markdown() to remove markdown formatting before posting to Bluesky
- Fix build_link_facets to track search position, avoiding wrong offsets for duplicate URLs
- Use parsed frontmatter.bsky_url.is_some() instead of raw string search for idempotency check
- Add tests for strip_markdown and duplicate URL facet offsets

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Only convert [text](url) -> text in Bluesky posts. Leave all other
markdown formatting (bold, italic, backticks, headings) as-is.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants