The code for my personal website, powered by Jekyll. arthr.me
jekyll-site personal-website
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: add GitHub Action to syndicate posts and write back URLs via Bridgy

After a push to main with new/modified posts, the workflow:
1. Detects which posts were changed
2. Waits 120s for Cloudflare Pages to deploy
3. POSTs to brid.gy/publish/webmention for each syndicate_to target
4. Writes the returned URLs into syndication_urls in the post front matter
5. Commits the updated front matter back to the repo

https://claude.ai/code/session_011sLDKMLU8c2A1TRsfWS4no

authored by

Claude and committed by
Arthur Freitas
251f9a44 7c13c1a3

+186
+134
.github/scripts/syndicate.py
··· 1 + #!/usr/bin/env python3 2 + """ 3 + Syndicate new/modified Jekyll posts to social networks via Bridgy. 4 + 5 + For each post listed in CHANGED_POSTS_FILE, checks if any syndicate_to 6 + targets are missing from syndication_urls and publishes them via Bridgy's 7 + webmention endpoint. Writes the returned URLs back into the post's front matter. 8 + """ 9 + 10 + import os 11 + import re 12 + import sys 13 + import time 14 + 15 + import frontmatter 16 + import requests 17 + 18 + SITE_URL = os.environ.get("SITE_URL", "https://arthr.me").rstrip("/") 19 + CHANGED_POSTS_FILE = os.environ.get("CHANGED_POSTS_FILE", "changed_posts.txt") 20 + 21 + BRIDGY_ENDPOINTS = { 22 + "bluesky": "https://brid.gy/publish/bluesky", 23 + "mastodon": "https://brid.gy/publish/mastodon", 24 + "flickr": "https://brid.gy/publish/flickr", 25 + "tumblr": "https://brid.gy/publish/tumblr", 26 + } 27 + 28 + 29 + def get_post_url(post_path, post): 30 + """Build the live URL for a post using Jekyll's permalink format.""" 31 + filename = os.path.basename(post_path) 32 + match = re.match(r"(\d{4})-(\d{2})-(\d{2})-(.+)\.md$", filename) 33 + if not match: 34 + return None 35 + 36 + year, month, day, slug = match.groups() 37 + category = str(post.get("category", "")).lower() 38 + 39 + if category: 40 + return f"{SITE_URL}/{category}/{year}/{month}/{day}/{slug}/" 41 + return f"{SITE_URL}/{year}/{month}/{day}/{slug}/" 42 + 43 + 44 + def publish_to_bridgy(post_url, target): 45 + """ 46 + Send a webmention to Bridgy's publish endpoint for a target network. 47 + Returns the syndicated URL on success, or None on failure. 48 + """ 49 + endpoint = BRIDGY_ENDPOINTS.get(target) 50 + if not endpoint: 51 + print(f" Unknown target: {target}, skipping.") 52 + return None 53 + 54 + try: 55 + resp = requests.post( 56 + "https://brid.gy/publish/webmention", 57 + data={"source": post_url, "target": endpoint}, 58 + timeout=30, 59 + ) 60 + except requests.RequestException as e: 61 + print(f" Request error: {e}") 62 + return None 63 + 64 + if resp.status_code in (200, 201): 65 + url = resp.headers.get("location") 66 + if not url: 67 + try: 68 + url = resp.json().get("url") 69 + except Exception: 70 + pass 71 + return url 72 + 73 + print(f" Bridgy returned {resp.status_code}: {resp.text[:300]}") 74 + return None 75 + 76 + 77 + def main(): 78 + with open(CHANGED_POSTS_FILE) as f: 79 + changed = [line.strip() for line in f if line.strip()] 80 + 81 + if not changed: 82 + print("No changed posts to process.") 83 + return 84 + 85 + any_written = False 86 + 87 + for post_path in changed: 88 + if not os.path.exists(post_path): 89 + print(f"Skipping {post_path} (deleted).") 90 + continue 91 + 92 + post = frontmatter.load(post_path) 93 + syndicate_to = list(post.get("syndicate_to") or []) 94 + syndication_urls = dict(post.get("syndication_urls") or {}) 95 + 96 + pending = [t for t in syndicate_to if t not in syndication_urls] 97 + if not pending: 98 + print(f"{post_path}: already syndicated to all targets, skipping.") 99 + continue 100 + 101 + post_url = get_post_url(post_path, post) 102 + if not post_url: 103 + print(f"{post_path}: could not determine post URL, skipping.") 104 + continue 105 + 106 + print(f"\nSyndicating: {post_url}") 107 + 108 + for target in pending: 109 + print(f" -> {target} ...", end=" ", flush=True) 110 + url = publish_to_bridgy(post_url, target) 111 + if url: 112 + syndication_urls[target] = url 113 + print(f"OK: {url}") 114 + else: 115 + print("FAILED (will retry on next push)") 116 + time.sleep(2) 117 + 118 + if syndication_urls != dict(post.get("syndication_urls") or {}): 119 + post["syndication_urls"] = syndication_urls 120 + with open(post_path, "wb") as f: 121 + frontmatter.dump(post, f) 122 + print(f" Wrote syndication_urls back to {post_path}") 123 + any_written = True 124 + 125 + if any_written: 126 + print("\nDone. Syndication URLs written back to posts.") 127 + sys.exit(0) 128 + else: 129 + print("\nNo syndication URLs to write back.") 130 + sys.exit(0) 131 + 132 + 133 + if __name__ == "__main__": 134 + main()
+52
.github/workflows/syndicate.yml
··· 1 + name: POSSE — Syndicate to social networks 2 + 3 + on: 4 + push: 5 + branches: [main] 6 + paths: ["_posts/**"] 7 + 8 + jobs: 9 + syndicate: 10 + runs-on: ubuntu-latest 11 + permissions: 12 + contents: write 13 + 14 + steps: 15 + - uses: actions/checkout@v4 16 + with: 17 + fetch-depth: 2 18 + 19 + - name: Find new/modified posts 20 + run: | 21 + if git rev-parse HEAD~1 &>/dev/null; then 22 + git diff --name-only HEAD~1 HEAD -- '_posts/*.md' > changed_posts.txt 23 + else 24 + git show --name-only --format="" HEAD -- '_posts/*.md' > changed_posts.txt 25 + fi 26 + echo "Posts changed in this push:" 27 + cat changed_posts.txt 28 + 29 + - name: Wait for Cloudflare Pages deployment 30 + run: | 31 + echo "Waiting for Cloudflare Pages deployment to go live..." 32 + sleep 120 33 + 34 + - name: Set up Python 35 + uses: actions/setup-python@v5 36 + with: 37 + python-version: "3.12" 38 + 39 + - name: Install dependencies 40 + run: pip install requests python-frontmatter 41 + 42 + - name: Syndicate posts via Bridgy 43 + run: python .github/scripts/syndicate.py 44 + env: 45 + SITE_URL: https://arthr.me 46 + CHANGED_POSTS_FILE: changed_posts.txt 47 + 48 + - name: Commit syndication URLs back to posts 49 + uses: stefanzweifel/git-auto-commit-action@v5 50 + with: 51 + commit_message: "chore: add syndication URLs [skip ci]" 52 + file_pattern: "_posts/*.md"