commit 89a6f4e50261569fdc5c2fb110fcd5a41b783a14
parent c67b180bc5d8eaddc92be658dfcb21ba71cf7e12
Author: David Eisinger <[email protected]>
Date: Mon, 6 Oct 2025 14:50:15 -0400
RSS
Diffstat:
| M | README.md | | | 7 | +++++-- |
| M | cmd/marky.go | | | 72 | ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-- |
2 files changed, 75 insertions(+), 4 deletions(-)
diff --git a/README.md b/README.md
@@ -67,14 +67,15 @@ curl -i -X POST http://localhost:8080/bookmarks \
### GET /bookmarks
-List bookmarks in reverse chronological order, or search with full-text. When `q` is provided, results include an `excerpt` with highlighted matches.
+List bookmarks in reverse chronological order, or search with full-text. When `q` is provided, results include an `excerpt` with highlighted matches. The endpoint can return JSON (default) or RSS.
- Query params:
- `limit` (optional, default 50, max 200)
- `offset` (optional, default 0)
- `q` (optional) full-text query across title, url, description, and archived content
+ - `format=rss` (optional) to return an RSS 2.0 feed
- Responses:
- - `200 OK` with JSON array of bookmarks
+ - `200 OK` with JSON array of bookmarks or RSS feed
- `400 Bad Request` for invalid `limit` or `offset`
- `500 Internal Server Error` on unexpected errors
- curl examples:
@@ -82,6 +83,8 @@ List bookmarks in reverse chronological order, or search with full-text. When `q
curl -s http://localhost:8080/bookmarks | jq
curl -s 'http://localhost:8080/bookmarks?limit=20&offset=20' | jq
curl -s 'http://localhost:8080/bookmarks?q=example' | jq
+curl -s 'http://localhost:8080/bookmarks?format=rss' | xmllint --format -
+curl -s 'http://localhost:8080/bookmarks.xml?q=example' | xmllint --format -
```
Notes on search excerpts:
diff --git a/cmd/marky.go b/cmd/marky.go
@@ -3,6 +3,8 @@ package main
import (
"context"
"encoding/json"
+ "encoding/xml"
+ "fmt"
"log"
"net/http"
"net/url"
@@ -65,7 +67,7 @@ func main() {
} else {
mux.Handle("/", h)
}
- mux.HandleFunc("/bookmarks", func(w http.ResponseWriter, r *http.Request) {
+ bookmarksHandler := func(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodPost:
if r.Header.Get("Content-Type") != "" {
@@ -209,6 +211,16 @@ func main() {
resp = append(resp, toResp(&b))
}
}
+ // Choose format: JSON (default) or RSS
+ wantsRSS := r.URL.Path == "/bookmarks.xml" || strings.EqualFold(r.URL.Query().Get("format"), "rss") || strings.Contains(r.Header.Get("Accept"), "application/rss+xml")
+ if wantsRSS {
+ w.Header().Set("Content-Type", "application/rss+xml; charset=utf-8")
+ if err := writeRSS(w, r, resp); err != nil {
+ log.Printf("write rss error: %v", err)
+ http.Error(w, "internal server error", http.StatusInternalServerError)
+ }
+ return
+ }
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(resp)
@@ -216,7 +228,9 @@ func main() {
w.Header().Set("Allow", http.MethodGet+", "+http.MethodPost)
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
}
- })
+ }
+ mux.HandleFunc("/bookmarks", bookmarksHandler)
+ mux.HandleFunc("/bookmarks.xml", bookmarksHandler)
// Archive endpoints: GET /bookmarks/{id}/archive
mux.HandleFunc("/bookmarks/", func(w http.ResponseWriter, r *http.Request) {
@@ -386,3 +400,57 @@ func logging(next http.Handler) http.Handler {
log.Printf("%s %s %s", r.Method, r.URL.Path, time.Since(start))
})
}
+
+// RSS feed structures and writer
+type rssFeed struct {
+ XMLName xml.Name `xml:"rss"`
+ Version string `xml:"version,attr"`
+ Channel rssChannel `xml:"channel"`
+}
+type rssChannel struct {
+ Title string `xml:"title"`
+ Link string `xml:"link"`
+ Description string `xml:"description"`
+ Items []rssItem `xml:"item"`
+}
+type rssItem struct {
+ Title string `xml:"title"`
+ Link string `xml:"link"`
+ GUID string `xml:"guid"`
+ Description string `xml:"description,omitempty"`
+ PubDate string `xml:"pubDate"`
+}
+
+func writeRSS(w http.ResponseWriter, r *http.Request, items []bookmarkResponse) error {
+ chanTitle := "marky bookmarks"
+ // Derive a base link if possible
+ chanLink := ""
+ if r != nil && r.Host != "" {
+ scheme := "http"
+ if r.TLS != nil {
+ scheme = "https"
+ }
+ chanLink = fmt.Sprintf("%s://%s/bookmarks", scheme, r.Host)
+ }
+ chanDesc := "Bookmarks feed"
+ out := rssFeed{Version: "2.0", Channel: rssChannel{Title: chanTitle, Link: chanLink, Description: chanDesc}}
+ out.Channel.Items = make([]rssItem, 0, len(items))
+ for _, b := range items {
+ desc := ""
+ if b.Excerpt != nil && *b.Excerpt != "" {
+ desc = *b.Excerpt
+ } else if b.Description != nil {
+ desc = *b.Description
+ }
+ out.Channel.Items = append(out.Channel.Items, rssItem{
+ Title: b.Title,
+ Link: b.URL,
+ GUID: fmt.Sprintf("marky:%d", b.ID),
+ Description: desc,
+ PubDate: b.UpdatedAt.Format(time.RFC1123Z),
+ })
+ }
+ enc := xml.NewEncoder(w)
+ enc.Indent("", " ")
+ return enc.Encode(out)
+}