+2
-1
CLAUDE.md
+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
+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
+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
+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
+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
+1
internal/config/config.go
+37
-5
internal/converter/markdown.go
+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
+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