A Bluesky Archival Tool
2
fork

Configure Feed

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

fix: sanitize export IDs for valid CSS selectors

Export IDs contain special characters (colons and slashes from DIDs)
that are invalid in CSS selectors. This fix:

1. Added sanitizeID() template function and handler helper
2. Converts IDs like "did:plc:xxx/2025-11-02_18-24-04" to "did-plc-xxx-2025-11-02_18-24-04"
3. Uses sanitized ID for tr id attribute
4. Uses sanitized ID in hx-target="#export-{sanitizedID}"
5. Keeps original ID in data-export-id attribute for backend operations

This allows HTMX to properly target and delete table rows without
CSS selector syntax errors.

Fixes: SyntaxError: '#export-did:plc:...' is not a valid selector

🤖 Generated with [Claude Code](https://claude.com/claude-code)

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

+47 -6
+27 -4
internal/web/handlers/export.go
··· 7 7 "log" 8 8 "net/http" 9 9 "os" 10 + "strings" 10 11 "sync" 11 12 "time" 12 13 ··· 316 317 }) 317 318 } 318 319 320 + // sanitizeID converts an export ID to a valid CSS selector ID 321 + // by replacing special characters with hyphens 322 + func sanitizeID(id string) string { 323 + replacer := strings.NewReplacer( 324 + ":", "-", 325 + "/", "-", 326 + " ", "-", 327 + ) 328 + sanitized := replacer.Replace(id) 329 + // Remove any remaining non-alphanumeric characters except hyphens 330 + var result strings.Builder 331 + for _, ch := range sanitized { 332 + if (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || (ch >= '0' && ch <= '9') || ch == '-' || ch == '_' { 333 + result.WriteRune(ch) 334 + } 335 + } 336 + return result.String() 337 + } 338 + 319 339 // ExportRow returns a single export as an HTML table row fragment for HTMX 320 340 func (h *Handlers) ExportRow(w http.ResponseWriter, r *http.Request) { 321 341 session, ok := auth.GetSessionFromContext(r.Context()) ··· 349 369 350 370 // Return HTML table row 351 371 w.Header().Set("Content-Type", "text/html") 352 - fmt.Fprintf(w, `<tr id="export-%s"> 372 + sanitizedID := sanitizeID(exportRecord.ID) 373 + fmt.Fprintf(w, `<tr id="export-%s" data-export-id="%s"> 353 374 <td>%s</td> 354 375 <td>%s</td> 355 376 <td>%s</td> ··· 371 392 style="margin: 0;" 372 393 hx-delete="/export/delete/%s" 373 394 hx-confirm="Are you sure you want to delete this export? This action cannot be undone." 374 - hx-target="closest tr" 375 - hx-swap="delete" 395 + hx-target="#export-%s" 396 + hx-swap="outerHTML" 376 397 hx-headers='{"X-CSRF-Token": "%s"}'> 377 398 Delete 378 399 </button> ··· 387 408 </div> 388 409 </td> 389 410 </tr>`, 390 - exportRecord.ID, // For tr id="export-%s" 411 + sanitizedID, // For tr id="export-%s" 412 + exportRecord.ID, // For tr data-export-id="%s" (original ID) 391 413 exportRecord.CreatedAt.Format("2006-01-02 15:04"), 392 414 exportRecord.Format, 393 415 exportRecord.DateRangeString(), ··· 397 419 exportRecord.ID, // For download link href 398 420 exportRecord.ID, // For download button data-export-id 399 421 exportRecord.ID, // For delete button hx-delete 422 + sanitizedID, // For delete button hx-target="#export-%s" 400 423 csrfToken, // For delete button X-CSRF-Token 401 424 exportRecord.ID, // For checkbox data-export-id 402 425 )
+18
internal/web/handlers/template.go
··· 74 74 } 75 75 return false 76 76 }, 77 + "sanitizeID": func(id string) string { 78 + // Replace special characters with hyphens to create valid CSS selectors 79 + // Replaces : / and any other non-alphanumeric characters 80 + replacer := strings.NewReplacer( 81 + ":", "-", 82 + "/", "-", 83 + " ", "-", 84 + ) 85 + sanitized := replacer.Replace(id) 86 + // Remove any remaining non-alphanumeric characters except hyphens 87 + var result strings.Builder 88 + for _, ch := range sanitized { 89 + if (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || (ch >= '0' && ch <= '9') || ch == '-' || ch == '_' { 90 + result.WriteRune(ch) 91 + } 92 + } 93 + return result.String() 94 + }, 77 95 } 78 96 } 79 97
+2 -2
internal/web/templates/pages/export.html
··· 140 140 </thead> 141 141 <tbody id="exports-tbody"> 142 142 {{range .Exports}} 143 - <tr id="export-{{.ID}}"> 143 + <tr id="export-{{sanitizeID .ID}}" data-export-id="{{.ID}}"> 144 144 <td>{{.CreatedAt.Format "2006-01-02 15:04"}}</td> 145 145 <td>{{.Format}}</td> 146 146 <td>{{.DateRangeString}}</td> ··· 162 162 style="margin: 0;" 163 163 hx-delete="/export/delete/{{.ID}}" 164 164 hx-confirm="Are you sure you want to delete this export? This action cannot be undone." 165 - hx-target="closest tr" 165 + hx-target="#export-{{sanitizeID .ID}}" 166 166 hx-swap="outerHTML" 167 167 hx-headers='{"X-CSRF-Token": "{{$.CSRFToken}}"}'> 168 168 Delete