An API you can curl, or open in a browser, to receive Bluesky data as markdown!
11
fork

Configure Feed

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

Add /skill.md endpoint and agent install section on homepage

- GET /skill.md — installable skill file for coding agents, includes
full endpoint reference, URL parsing table, and usage patterns
- Homepage: "Add to your coding agent" section with tabbed UI for
Claude Code (/bsky slash command), CLAUDE.md, Cursor (.cursorrules),
Windsurf (.windsurfrules), GitHub Copilot, and raw copy
- Each tab shows a one-line curl install command with copy button

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

jack fbb8eb26 79db1a20

+366
+127
app/page.module.css
··· 569 569 line-height: 1.45; 570 570 } 571 571 572 + /* ── Agent install section ────────────────────────── */ 573 + .agentSection { 574 + max-width: 900px; 575 + width: 100%; 576 + margin: 56px auto 0; 577 + padding: 0 24px; 578 + } 579 + 580 + .agentCard { 581 + border: 1px solid var(--border); 582 + border-radius: var(--radius); 583 + overflow: hidden; 584 + background: var(--bg); 585 + } 586 + 587 + .agentTabs { 588 + display: flex; 589 + overflow-x: auto; 590 + border-bottom: 1px solid var(--border); 591 + background: var(--bg-2); 592 + scrollbar-width: none; 593 + -ms-overflow-style: none; 594 + } 595 + .agentTabs::-webkit-scrollbar { display: none; } 596 + 597 + .agentTab { 598 + display: flex; 599 + align-items: center; 600 + gap: 6px; 601 + padding: 11px 16px; 602 + font-size: 0.82rem; 603 + font-weight: 500; 604 + color: var(--text-2); 605 + background: transparent; 606 + border: none; 607 + border-bottom: 2px solid transparent; 608 + cursor: pointer; 609 + white-space: nowrap; 610 + transition: color 0.12s, border-color 0.12s, background 0.12s; 611 + flex-shrink: 0; 612 + } 613 + 614 + .agentTab:hover { 615 + color: var(--text); 616 + background: var(--bg); 617 + } 618 + 619 + .agentTabActive { 620 + color: var(--blue); 621 + border-bottom-color: var(--blue); 622 + background: var(--bg); 623 + } 624 + 625 + .agentBody { 626 + padding: 20px 20px 16px; 627 + } 628 + 629 + .agentDesc { 630 + font-size: 0.85rem; 631 + color: var(--text-2); 632 + margin-bottom: 12px; 633 + line-height: 1.5; 634 + } 635 + 636 + .codeBlock { 637 + display: flex; 638 + align-items: stretch; 639 + border: 1px solid var(--border); 640 + border-radius: 8px; 641 + overflow: hidden; 642 + background: var(--bg-2); 643 + } 644 + 645 + .codeText { 646 + flex: 1; 647 + padding: 12px 14px; 648 + font-family: var(--mono); 649 + font-size: 0.8rem; 650 + color: var(--text); 651 + overflow-x: auto; 652 + white-space: nowrap; 653 + scrollbar-width: thin; 654 + line-height: 1.4; 655 + } 656 + 657 + .codeCopy { 658 + padding: 0 16px; 659 + font-size: 0.78rem; 660 + font-weight: 500; 661 + background: var(--bg); 662 + border: none; 663 + border-left: 1px solid var(--border); 664 + cursor: pointer; 665 + color: var(--text-2); 666 + white-space: nowrap; 667 + transition: background 0.1s, color 0.1s; 668 + flex-shrink: 0; 669 + } 670 + 671 + .codeCopy:hover { 672 + background: var(--blue-pale); 673 + color: var(--blue); 674 + } 675 + 676 + .codeCopyDone { 677 + color: #16a34a !important; 678 + background: #dcfce7 !important; 679 + } 680 + 681 + @media (prefers-color-scheme: dark) { 682 + .codeCopyDone { 683 + color: #4ade80 !important; 684 + background: #052e16 !important; 685 + } 686 + } 687 + 688 + .agentNote { 689 + margin-top: 10px; 690 + font-size: 0.78rem; 691 + color: var(--text-3); 692 + line-height: 1.5; 693 + } 694 + 695 + .agentNote a { 696 + color: var(--blue); 697 + } 698 + 572 699 /* ── Footer ───────────────────────────────────────── */ 573 700 .footer { 574 701 margin-top: auto;
+100
app/page.tsx
··· 67 67 return `${(n / 1000).toFixed(1)}k chars` 68 68 } 69 69 70 + // ── Agent install data ──────────────────────────────────────────────────────── 71 + 72 + const AGENTS = [ 73 + { 74 + id: 'claude-code', 75 + label: 'Claude Code', 76 + icon: '🤖', 77 + command: 'curl -s https://bsky-md.vercel.app/skill.md > ~/.claude/commands/bsky.md', 78 + desc: 'Installs a global /bsky slash command. Use it in any Claude Code session with /bsky.', 79 + note: 'After running, type <code>/bsky</code> in any Claude Code conversation to activate.', 80 + }, 81 + { 82 + id: 'claude-md', 83 + label: 'CLAUDE.md', 84 + icon: '📄', 85 + command: 'curl -s https://bsky-md.vercel.app/skill.md >> CLAUDE.md', 86 + desc: 'Appends the full API reference to your project\'s CLAUDE.md — Claude will use it automatically for every conversation in this project.', 87 + note: 'Run this in your project root. Works for Claude Code, Cursor, and any agent that reads CLAUDE.md.', 88 + }, 89 + { 90 + id: 'cursor', 91 + label: 'Cursor', 92 + icon: '⌨️', 93 + command: 'curl -s https://bsky-md.vercel.app/skill.md >> .cursorrules', 94 + desc: 'Appends the API reference to your Cursor project rules.', 95 + note: 'Cursor reads .cursorrules automatically for every chat in this workspace.', 96 + }, 97 + { 98 + id: 'windsurf', 99 + label: 'Windsurf', 100 + icon: '🏄', 101 + command: 'curl -s https://bsky-md.vercel.app/skill.md >> .windsurfrules', 102 + desc: 'Appends the API reference to your Windsurf workspace rules.', 103 + note: 'Windsurf reads .windsurfrules for every Cascade conversation in this workspace.', 104 + }, 105 + { 106 + id: 'copilot', 107 + label: 'Copilot', 108 + icon: '🐙', 109 + command: 'mkdir -p .github && curl -s https://bsky-md.vercel.app/skill.md >> .github/copilot-instructions.md', 110 + desc: 'Appends the API reference to your GitHub Copilot workspace instructions.', 111 + note: 'GitHub Copilot reads .github/copilot-instructions.md automatically.', 112 + }, 113 + { 114 + id: 'raw', 115 + label: 'Raw file', 116 + icon: '📋', 117 + command: 'curl -s https://bsky-md.vercel.app/skill.md', 118 + desc: 'Print the raw skill file — paste it into any agent context, system prompt, or rules file.', 119 + note: 'Or just open <a href="/skill.md" target="_blank" rel="noopener noreferrer">bsky-md.vercel.app/skill.md</a> in your browser.', 120 + }, 121 + ] 122 + 70 123 // ── Catalogue ───────────────────────────────────────────────────────────────── 71 124 72 125 const ENDPOINTS = [ ··· 102 155 const [error, setError] = useState<string | null>(null) 103 156 const [copiedUrl, setCopiedUrl] = useState(false) 104 157 const [copiedMd, setCopiedMd] = useState(false) 158 + const [activeAgent, setActiveAgent] = useState('claude-code') 159 + const [copiedCmd, setCopiedCmd] = useState(false) 105 160 const abortRef = useRef<AbortController | null>(null) 106 161 const resultRef = useRef<HTMLDivElement | null>(null) 107 162 ··· 180 235 }) 181 236 }, [markdown]) 182 237 238 + const copyCmd = useCallback((cmd: string) => { 239 + navigator.clipboard.writeText(cmd).then(() => { 240 + setCopiedCmd(true) 241 + setTimeout(() => setCopiedCmd(false), 2000) 242 + }) 243 + }, []) 244 + 183 245 const activePath = parsed ? getPath(parsed, parsed.isPost ? viewMode : 'post') : null 246 + const currentAgent = AGENTS.find((a) => a.id === activeAgent) ?? AGENTS[0] 184 247 const charCount = markdown ? markdown.length : 0 185 248 186 249 return ( ··· 330 393 <p className={s.cardDesc}>{ep.desc}</p> 331 394 </a> 332 395 ))} 396 + </div> 397 + </section> 398 + 399 + {/* ── Agent install ── */} 400 + <section className={s.agentSection}> 401 + <p className={s.sectionTitle}>Add to your coding agent</p> 402 + <div className={s.agentCard}> 403 + {/* Tab row */} 404 + <div className={s.agentTabs}> 405 + {AGENTS.map((a) => ( 406 + <button 407 + key={a.id} 408 + className={`${s.agentTab} ${activeAgent === a.id ? s.agentTabActive : ''}`} 409 + onClick={() => { setActiveAgent(a.id); setCopiedCmd(false) }} 410 + > 411 + {a.icon} {a.label} 412 + </button> 413 + ))} 414 + </div> 415 + 416 + {/* Body */} 417 + <div className={s.agentBody}> 418 + <p className={s.agentDesc}>{currentAgent.desc}</p> 419 + <div className={s.codeBlock}> 420 + <code className={s.codeText}>$ {currentAgent.command}</code> 421 + <button 422 + className={`${s.codeCopy} ${copiedCmd ? s.codeCopyDone : ''}`} 423 + onClick={() => copyCmd(currentAgent.command)} 424 + > 425 + {copiedCmd ? '✓ Copied' : 'Copy'} 426 + </button> 427 + </div> 428 + <p 429 + className={s.agentNote} 430 + dangerouslySetInnerHTML={{ __html: currentAgent.note }} 431 + /> 432 + </div> 333 433 </div> 334 434 </section> 335 435
+139
app/skill.md/route.ts
··· 1 + import { type NextRequest } from 'next/server' 2 + import { baseUrl } from '@/lib/respond' 3 + 4 + export async function GET(req: NextRequest) { 5 + const base = baseUrl(req) 6 + 7 + const md = `# bsky-md — Bluesky Markdown API 8 + 9 + Fetch any public Bluesky content as clean Markdown. No auth, no API key required. 10 + Base URL: ${base} 11 + 12 + ## When to use this skill 13 + 14 + Use this API whenever the user asks to: 15 + - Read a Bluesky post, thread, or profile 16 + - Search Bluesky posts for a topic or hashtag 17 + - See what's trending on Bluesky 18 + - Fetch posts from a custom Bluesky feed 19 + - Get a user's followers, following, or liked posts 20 + - Summarise or analyse Bluesky content 21 + 22 + ## How to call it 23 + 24 + All endpoints return \`Content-Type: text/markdown\`. Just fetch the URL. 25 + Open CORS — works from browser, server, or CLI. 26 + 27 + \`\`\`bash 28 + # Example: fetch a profile 29 + curl ${base}/profile/bsky.app 30 + 31 + # Example: fetch recent posts 32 + curl ${base}/profile/bsky.app/posts 33 + 34 + # Example: search 35 + curl "${base}/search?q=atproto" 36 + \`\`\` 37 + 38 + ## Endpoint reference 39 + 40 + ### Profile 41 + \`\`\` 42 + GET ${base}/profile/:handle 43 + \`\`\` 44 + Returns display name, bio, avatar/banner URLs, follower/following/post counts. 45 + 46 + ### Posts feed 47 + \`\`\` 48 + GET ${base}/profile/:handle/posts[?cursor=&limit=] 49 + \`\`\` 50 + Paginated list of original posts (no replies). Follow the "Next page →" link or pass \`cursor\`. 51 + 52 + ### Single post 53 + \`\`\` 54 + GET ${base}/profile/:handle/post/:rkey 55 + \`\`\` 56 + :rkey is the last path segment of any bsky.app post URL. 57 + Returns body, images (with alt text + CDN URLs), video, external link card, quote post. 58 + 59 + ### Full thread 60 + \`\`\` 61 + GET ${base}/profile/:handle/post/:rkey/thread 62 + \`\`\` 63 + Root post + all self-replies from the same author, in chronological order. 64 + 65 + ### Custom feed 66 + \`\`\` 67 + GET ${base}/profile/:handle/feed/:rkey[?cursor=&limit=] 68 + \`\`\` 69 + Public Bluesky feeds. Example feeds: whats-hot, for-you, discover. 70 + Maps to bsky.app/profile/:handle/feed/:rkey. 71 + 72 + ### Likes 73 + \`\`\` 74 + GET ${base}/profile/:handle/likes[?cursor=&limit=] 75 + \`\`\` 76 + 77 + ### Followers / Following 78 + \`\`\` 79 + GET ${base}/profile/:handle/followers[?cursor=&limit=] 80 + GET ${base}/profile/:handle/following[?cursor=&limit=] 81 + \`\`\` 82 + 83 + ### Search 84 + \`\`\` 85 + GET ${base}/search?q=:query[&cursor=&limit=] 86 + \`\`\` 87 + Full-text search across all public Bluesky posts. 88 + Supports quoted phrases: \`q="open social web"\` 89 + 90 + ### Trending topics 91 + \`\`\` 92 + GET ${base}/trending 93 + \`\`\` 94 + Live list of trending topics with links to search each one. 95 + 96 + ## Parsing bsky.app URLs 97 + 98 + When the user pastes a bsky.app URL, convert it: 99 + 100 + | bsky.app URL | API path | 101 + |---|---| 102 + | /profile/:handle | /profile/:handle | 103 + | /profile/:handle/post/:rkey | /profile/:handle/post/:rkey | 104 + | /profile/:handle/feed/:rkey | /profile/:handle/feed/:rkey | 105 + | /hashtag/:tag | /search?q=%23:tag | 106 + | /search?q=... | /search?q=... | 107 + 108 + ## Parameter notes 109 + 110 + - **:handle** — any of: \`user.bsky.social\`, custom domain (\`j4ck.xyz\`), or DID (\`did:plc:...\`) 111 + - **:rkey** — the record key; final segment of a bsky.app post URL 112 + - **limit** — integer 1–100; defaults vary per endpoint 113 + - **cursor** — opaque token from the "Next page →" link in the previous response 114 + 115 + ## Response format 116 + 117 + All responses are plain Markdown text: 118 + - Posts include author, timestamp, body, embeds, and engagement counts 119 + - Images: \`![alt](url)\` with raw CDN URL on the following line 120 + - Quote posts: rendered as Markdown blockquotes 121 + - Rich text: mentions/URLs/hashtags converted to Markdown links 122 + - Pagination: last line of response contains a "Next page →" link if more results exist 123 + 124 + ## Full reference 125 + 126 + ${base}/llms.txt — complete machine-readable guide 127 + ${base} — interactive homepage with URL converter 128 + ` 129 + 130 + return new Response(md, { 131 + status: 200, 132 + headers: { 133 + 'Content-Type': 'text/markdown; charset=utf-8', 134 + 'Access-Control-Allow-Origin': '*', 135 + 'Cache-Control': 'public, s-maxage=3600, stale-while-revalidate=86400', 136 + 'Content-Disposition': 'inline; filename="bsky-md.skill.md"', 137 + }, 138 + }) 139 + }