this repo has no description

Merge pull request #5 from mariuskimmina/fix-bluesky-links

Fix bluesky links

authored by Marius Kimmina and committed by GitHub 26e4b588 40e20a13

Changed files
+171 -14
cmd
leaflet-hugo-sync
internal
+2 -1
CLAUDE.md
··· 86 86 87 87 **Configuration** (`internal/config/config.go`): 88 88 - `Source`: Specifies handle, collection, optional publication_name 89 - - `Output`: Defines posts_dir, images_dir, image_path_prefix 89 + - `Output`: Defines posts_dir, images_dir, image_path_prefix, bsky_embed_style 90 90 - `Template`: Go template strings for frontmatter and content 91 91 92 92 ### Important Implementation Details ··· 113 113 posts_dir: "content/posts/leaflet" 114 114 images_dir: "static/images/leaflet" 115 115 image_path_prefix: "/images/leaflet" 116 + bsky_embed_style: "link" # "link" (default) or "shortcode" for Hugo embeds 116 117 117 118 template: 118 119 frontmatter: |
+10
README.md
··· 16 16 posts_dir: "content/posts/leaflet" 17 17 images_dir: "static/images/leaflet" 18 18 image_path_prefix: "/images/leaflet" 19 + bsky_embed_style: "link" # Optional: "link" (default) or "shortcode" 19 20 20 21 template: 21 22 frontmatter: | ··· 25 26 original_url: "{{ .OriginalURL }}" 26 27 --- 27 28 ``` 29 + 30 + ## BlueSky Post Embeds 31 + 32 + When your Leaflet posts reference BlueSky posts, they can be rendered in two ways: 33 + 34 + - **`link`** (default): Simple markdown links that work everywhere 35 + - **`shortcode`**: Rich Hugo shortcodes for custom styling 36 + 37 + For shortcode setup instructions, see [SHORTCODE_SETUP.md](SHORTCODE_SETUP.md). 28 38 29 39 ## How it works 30 40
+28
SHORTCODE_SETUP.md
··· 1 + # BlueSky Post Embed Setup 2 + 3 + The sync tool can generate BlueSky post embeds in two ways: 4 + - **Simple links** (default) - Plain markdown links that work everywhere 5 + - **Hugo shortcodes** (optional) - Rich, styled embeds with custom styling 6 + 7 + ## Quick Setup for Hugo Shortcodes 8 + 9 + By default, BlueSky posts are rendered as simple markdown links. To enable rich Hugo shortcode embeds: 10 + 11 + 1. **Enable shortcode mode in your config file** (`.leaflet-sync.yaml`): 12 + ```yaml 13 + output: 14 + posts_dir: "content/posts/leaflet" 15 + images_dir: "static/images/leaflet" 16 + image_path_prefix: "/images/leaflet" 17 + bsky_embed_style: "shortcode" # Add this line 18 + ``` 19 + 20 + 3. **Copy the shortcode to your Hugo site:** 21 + ```bash 22 + cp bsky.html /path/to/your/hugo/site/layouts/shortcodes/bsky.html 23 + ``` 24 + 25 + 4. **Run the sync:** 26 + ```bash 27 + leaflet-hugo-sync -config .leaflet-sync.yaml 28 + ```
+23
bsky.html
··· 1 + {{/* 2 + BlueSky Post Embed Shortcode (Enhanced with oEmbed) 3 + 4 + Usage: {{< bsky-oembed did="did:plc:abc123" postid="3mbrxzvw36c22" >}} 5 + 6 + This shortcode uses BlueSky's iframe embed for rich post display. 7 + Copy this file to your Hugo site's layouts/shortcodes/ directory as bsky.html 8 + */}} 9 + 10 + {{ $did := .Get "did" }} 11 + {{ $postid := .Get "postid" }} 12 + {{ $url := printf "https://bsky.app/profile/%s/post/%s" $did $postid }} 13 + 14 + <div class="bsky-embed-container" style="margin: 1.5em 0;"> 15 + <blockquote class="bluesky-embed" data-bluesky-uri="at://{{ $did }}/app.bsky.feed.post/{{ $postid }}" data-bluesky-cid=""> 16 + <p lang="en"> 17 + <a href="{{ $url }}" target="_blank" rel="noopener noreferrer"> 18 + View post on Bluesky 19 + </a> 20 + </p> 21 + </blockquote> 22 + <script async src="https://embed.bsky.app/static/embed.js" charset="utf-8"></script> 23 + </div>
+1 -1
cmd/leaflet-hugo-sync/main.go
··· 100 100 101 101 downloader := media.NewDownloader(cfg.Output.ImagesDir, cfg.Output.ImagePathPrefix, pdsClient.XRPC.Host) 102 102 gen := generator.NewGenerator(cfg) 103 - conv := converter.NewConverter() 103 + conv := converter.NewConverter(cfg.Output.BskyEmbedStyle) 104 104 105 105 for _, rec := range records { 106 106 // Try to unmarshal as LeafletDocument
+1
internal/config/config.go
··· 22 22 PostsDir string `yaml:"posts_dir"` 23 23 ImagesDir string `yaml:"images_dir"` 24 24 ImagePathPrefix string `yaml:"image_path_prefix"` 25 + BskyEmbedStyle string `yaml:"bsky_embed_style"` // "link" (default) or "shortcode" 25 26 } 26 27 27 28 type Template struct {
+37 -5
internal/converter/markdown.go
··· 9 9 ) 10 10 11 11 type Converter struct { 12 - // No state needed; conversion is stateless 12 + bskyEmbedStyle string // "link" (default) or "shortcode" 13 13 } 14 14 15 15 type ConversionResult struct { ··· 22 22 Alt string 23 23 } 24 24 25 - func NewConverter() *Converter { 26 - return &Converter{} 25 + func NewConverter(bskyEmbedStyle string) *Converter { 26 + // Default to "link" if not specified or invalid 27 + if bskyEmbedStyle != "shortcode" { 28 + bskyEmbedStyle = "link" 29 + } 30 + return &Converter{ 31 + bskyEmbedStyle: bskyEmbedStyle, 32 + } 27 33 } 28 34 29 35 func (c *Converter) ConvertLeaflet(doc *atproto.LeafletDocument) (*ConversionResult, error) { ··· 81 87 continue 82 88 } 83 89 // Render as a blockquote link to the Bluesky post 84 - postURL := fmt.Sprintf("https://bsky.app/profile/%s/post/%s", "did:...", lastPathPart(postBlock.PostRef.Uri)) 85 - sb.WriteString(fmt.Sprintf("> [View on Bluesky](%s)\n\n", postURL)) 90 + // Parse AT-URI: at://did:plc:abc123/app.bsky.feed.post/postID 91 + did, postID := parseATUri(postBlock.PostRef.Uri) 92 + 93 + if c.bskyEmbedStyle == "shortcode" { 94 + // Render as Hugo shortcode for rich embed 95 + sb.WriteString(fmt.Sprintf("{{< bsky did=\"%s\" postid=\"%s\" >}}\n\n", did, postID)) 96 + } else { 97 + // Default: render as simple markdown link 98 + postURL := fmt.Sprintf("https://bsky.app/profile/%s/post/%s", did, postID) 99 + sb.WriteString(fmt.Sprintf("[View on Bluesky](%s)\n\n", postURL)) 100 + } 86 101 } 87 102 } 88 103 } ··· 154 169 parts := strings.Split(uri, "/") 155 170 return parts[len(parts)-1] 156 171 } 172 + 173 + // parseATUri extracts DID and record key from an AT-URI 174 + // Example: at://did:plc:abc123/app.bsky.feed.post/3mbrxzvw36c22 175 + // Returns: (did:plc:abc123, 3mbrxzvw36c22) 176 + func parseATUri(uri string) (did string, recordKey string) { 177 + // Remove "at://" prefix 178 + uri = strings.TrimPrefix(uri, "at://") 179 + 180 + // Split into parts 181 + parts := strings.Split(uri, "/") 182 + if len(parts) >= 3 { 183 + did = parts[0] // did:plc:abc123 184 + recordKey = parts[len(parts)-1] // 3mbrxzvw36c22 185 + } 186 + 187 + return did, recordKey 188 + }
+69 -7
internal/converter/markdown_test.go
··· 25 25 }, 26 26 } 27 27 28 - conv := NewConverter() 28 + conv := NewConverter("") 29 29 result, err := conv.ConvertLeaflet(doc) 30 30 if err != nil { 31 31 t.Fatalf("ConvertLeaflet failed: %v", err) ··· 54 54 }, 55 55 } 56 56 57 - conv := NewConverter() 57 + conv := NewConverter("") 58 58 result, err := conv.ConvertLeaflet(doc) 59 59 if err != nil { 60 60 t.Fatalf("ConvertLeaflet failed: %v", err) ··· 89 89 }, 90 90 } 91 91 92 - conv := NewConverter() 92 + conv := NewConverter("") 93 93 result, err := conv.ConvertLeaflet(doc) 94 94 if err != nil { 95 95 t.Fatalf("ConvertLeaflet failed: %v", err) ··· 125 125 }, 126 126 } 127 127 128 - conv := NewConverter() 128 + conv := NewConverter("") 129 129 result := conv.renderText(block) 130 130 131 131 expected := "Check out [example.com](https://example.com) for more info" ··· 152 152 }, 153 153 } 154 154 155 - conv := NewConverter() 155 + conv := NewConverter("") 156 156 result := conv.renderText(block) 157 157 158 158 expected := "Use the `fmt.Println` function" ··· 180 180 }, 181 181 } 182 182 183 - conv := NewConverter() 183 + conv := NewConverter("") 184 184 result := conv.renderText(block) 185 185 186 186 expected := "Thanks [@alice](https://bsky.app/profile/did:plc:alice123) for the help" ··· 212 212 }, 213 213 } 214 214 215 - conv := NewConverter() 215 + conv := NewConverter("") 216 216 result, err := conv.ConvertLeaflet(doc) 217 217 if err != nil { 218 218 t.Fatalf("ConvertLeaflet failed: %v", err) ··· 220 220 221 221 if !strings.Contains(result.Markdown, "- First item") { 222 222 t.Errorf("expected list item, got %q", result.Markdown) 223 + } 224 + } 225 + 226 + func TestConvertLeaflet_BskyPost_Link(t *testing.T) { 227 + doc := &atproto.LeafletDocument{ 228 + Pages: []atproto.Page{ 229 + { 230 + Blocks: []atproto.BlockWrapper{ 231 + { 232 + Block: mustMarshal(atproto.BskyPostBlock{ 233 + Type: "pub.leaflet.blocks.bskyPost", 234 + PostRef: atproto.PostRef{ 235 + Uri: "at://did:plc:abc123/app.bsky.feed.post/3mbrxzvw36c22", 236 + Cid: "test-cid", 237 + }, 238 + }), 239 + }, 240 + }, 241 + }, 242 + }, 243 + } 244 + 245 + conv := NewConverter("link") // Default link mode 246 + result, err := conv.ConvertLeaflet(doc) 247 + if err != nil { 248 + t.Fatalf("ConvertLeaflet failed: %v", err) 249 + } 250 + 251 + expected := "[View on Bluesky](https://bsky.app/profile/did:plc:abc123/post/3mbrxzvw36c22)" 252 + if !strings.Contains(result.Markdown, expected) { 253 + t.Errorf("expected link format, got %q", result.Markdown) 254 + } 255 + } 256 + 257 + func TestConvertLeaflet_BskyPost_Shortcode(t *testing.T) { 258 + doc := &atproto.LeafletDocument{ 259 + Pages: []atproto.Page{ 260 + { 261 + Blocks: []atproto.BlockWrapper{ 262 + { 263 + Block: mustMarshal(atproto.BskyPostBlock{ 264 + Type: "pub.leaflet.blocks.bskyPost", 265 + PostRef: atproto.PostRef{ 266 + Uri: "at://did:plc:abc123/app.bsky.feed.post/3mbrxzvw36c22", 267 + Cid: "test-cid", 268 + }, 269 + }), 270 + }, 271 + }, 272 + }, 273 + }, 274 + } 275 + 276 + conv := NewConverter("shortcode") 277 + result, err := conv.ConvertLeaflet(doc) 278 + if err != nil { 279 + t.Fatalf("ConvertLeaflet failed: %v", err) 280 + } 281 + 282 + expected := `{{< bsky did="did:plc:abc123" postid="3mbrxzvw36c22" >}}` 283 + if !strings.Contains(result.Markdown, expected) { 284 + t.Errorf("expected shortcode format, got %q", result.Markdown) 223 285 } 224 286 } 225 287