diff options
| author | Samuel Johnson <[email protected]> | 2025-12-11 02:51:42 -0500 |
|---|---|---|
| committer | Samuel Johnson <[email protected]> | 2025-12-11 02:51:42 -0500 |
| commit | d4ad397b85994d4d6fdfbf75ce1bc65fdb2f9b33 (patch) | |
| tree | 7dfe7586ed1b7c72432d98a467a82f785c2e557f | |
| parent | 6785f856b81b6c0de8c8828761779732b30191af (diff) | |
Add RSS feed constructor
| -rw-r--r-- | cmd/web/handlers/export.go | 109 | ||||
| -rw-r--r-- | cmd/web/handlers/routes.go | 7 | ||||
| -rw-r--r-- | cmd/web/main.go | 1 | ||||
| -rw-r--r-- | go.mod | 4 | ||||
| -rw-r--r-- | internal/context/environment.go | 1 | ||||
| -rw-r--r-- | static/app.css | 20 | ||||
| -rw-r--r-- | ui/html/pages/index.tmpl.html | 3 |
7 files changed, 139 insertions, 6 deletions
diff --git a/cmd/web/handlers/export.go b/cmd/web/handlers/export.go new file mode 100644 index 0000000..becdae8 --- /dev/null +++ b/cmd/web/handlers/export.go @@ -0,0 +1,109 @@ +package handlers + +import ( + "database/sql" + "encoding/xml" + "log" + "net/http" + "strconv" + "strings" + "time" + + "golang.org/x/net/html" + + "paterissa.net/mblog/internal/models" +) + +type Item struct { + XMLName xml.Name `xml:"item"` + Title string `xml:"title"` + Link string `xml:"link"` + Description string `xml:"description"` + PubDate string `xml:"pubDate"` + Content string `xml:"content:encoded"` +} + +type Channel struct { + XMLName xml.Name `xml:"channel"` + Title string `xml:"title"` + Link string `xml:"link"` + Description string `xml:"description"` + Items []Item `xml:"item"` +} + +type rssExportContext struct { + err *log.Logger + db *sql.DB + + serv string +} + +func (ctx *rssExportContext) feed(w http.ResponseWriter, r *http.Request) { + feed := &Channel{ + Title: "Paterissa", + Link: ctx.serv, + Description: "Blog feed", + } + + rows, err := ctx.db.Query("SELECT p.id, u.name, p.time, p.brief, p.content FROM posts p INNER JOIN logins u ON p.user_id = u.id ORDER BY p.id DESC LIMIT 20;") + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte("Internal Error")) + ctx.err.Printf("Could not load posts from DB: %v\n", err) + return + } + defer rows.Close() + + for rows.Next() { + var p models.Post + + if err = rows.Scan(&p.Id, &p.Name, &p.Time, &p.Brief, &p.Content); err != nil { + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte("Internal Error")) + ctx.err.Printf("Could not load posts from DB: %v\n", err) + return + } + + p.FormattedTime = p.Time.Format(time.ANSIC) + + title := "" + z := html.NewTokenizer(strings.NewReader(string(p.Brief))) + + for { + tt := z.Next() + + if tt == html.ErrorToken { + break + } else if tt == html.StartTagToken { + tag := z.Token() + + if tag.Data == "h1" { + if tt = z.Next(); tt == html.TextToken { + title = z.Token().Data + } + } + } + } + + brief := html.EscapeString(string(p.Brief)) + content := html.EscapeString(string(p.Content)) + + feed.Items = append(feed.Items, Item{ + Title: title, + Link: ctx.serv + "/post?id=" + strconv.Itoa(p.Id), + Description: brief, + PubDate: p.FormattedTime, + Content: content, + }) + } + + out, err := xml.MarshalIndent(feed, "", " ") + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte("Internal Error")) + ctx.err.Printf("Could not create RSS feed: %v\n", err) + return + } + + w.Write([]byte(xml.Header + `<rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/">` + string(out) + `</rss>`)) +} diff --git a/cmd/web/handlers/routes.go b/cmd/web/handlers/routes.go index be48ae3..1c14705 100644 --- a/cmd/web/handlers/routes.go +++ b/cmd/web/handlers/routes.go @@ -27,6 +27,12 @@ func RegisterEndpoints(app types.Application, db *sql.DB) *http.ServeMux { err: app.Err, db: db, } + constructor := rssExportContext{ + err: app.Err, + db: db, + + serv: app.Env.Serv, + } audio := fsContext{ err: app.Err, path: app.AudioDir, @@ -48,6 +54,7 @@ func RegisterEndpoints(app types.Application, db *sql.DB) *http.ServeMux { blogRouter.HandleFunc("/feeds", auth.CheckAndInvalidate(feeds.index)) blogRouter.HandleFunc("/feeds/new", auth.Resolve(feeds.new)) blogRouter.HandleFunc("/feed", feeds.feed) + blogRouter.HandleFunc("/feed.rss", constructor.feed) blogRouter.HandleFunc("/audio", audio.readdir) blogRouter.HandleFunc("/audio/get", audio.get) diff --git a/cmd/web/main.go b/cmd/web/main.go index 00286b8..d4deaff 100644 --- a/cmd/web/main.go +++ b/cmd/web/main.go @@ -43,6 +43,7 @@ func main() { if err != nil { app.Env.AppPort = 5005 } + app.Env.Serv = os.Getenv("serv") connStr := fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s sslmode=disable", app.Env.Db.Host, @@ -6,8 +6,10 @@ require ( github.com/google/uuid v1.6.0 github.com/jackc/pgx/v5 v5.7.6 github.com/joho/godotenv v1.5.1 + github.com/mmcdole/gofeed v1.3.0 github.com/yuin/goldmark v1.7.11 golang.org/x/crypto v0.37.0 + golang.org/x/net v0.21.0 ) require ( @@ -17,11 +19,9 @@ require ( github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect github.com/jackc/puddle/v2 v2.2.2 // indirect github.com/json-iterator/go v1.1.12 // indirect - github.com/mmcdole/gofeed v1.3.0 // indirect github.com/mmcdole/goxpp v1.1.1-0.20240225020742-a0c311522b23 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect - golang.org/x/net v0.21.0 // indirect golang.org/x/sync v0.13.0 // indirect golang.org/x/text v0.24.0 // indirect ) diff --git a/internal/context/environment.go b/internal/context/environment.go index 3e680cf..844d625 100644 --- a/internal/context/environment.go +++ b/internal/context/environment.go @@ -11,6 +11,7 @@ type DbCredentials struct { type Environment struct { Webmaster string AppPort uint64 + Serv string Db DbCredentials } diff --git a/static/app.css b/static/app.css index c340259..2ed671f 100644 --- a/static/app.css +++ b/static/app.css @@ -22,6 +22,10 @@ pre { word-wrap: inherit; } +.rss { + margin-left: 1.5em; +} + .lead { display: flex; flex-direction: row; @@ -71,7 +75,7 @@ a.icon { color: #0fcff0; } -a:not(.icon)::after { +a:not(.icon):not(.rss)::after { content: ""; background: white; height: 1px; @@ -80,17 +84,17 @@ a:not(.icon)::after { transition: .16s all 0.025s; } -a:not(.icon)::after { +a:not(.icon):not(.rss)::after { left: 100%; right: 0; } -a:hover ~ a:not(.icon)::after { +a:hover ~ a:not(.icon):not(.rss)::after { left: 0; right: 100%; } -a:not(.icon):hover::after { +a:not(.icon):not(.rss):hover::after { left: 0; right: 0; } @@ -113,6 +117,10 @@ a:not(.icon):hover::after { width: 50%; } +.rss img { + width: 1.5rem; +} + input { background-color: rgba(255, 255, 255, 0.8); } @@ -351,6 +359,10 @@ iframe { width: 100%; } + .rss img { + width: 1.5rem; + } + .inner_pane { padding-bottom: 4em; } diff --git a/ui/html/pages/index.tmpl.html b/ui/html/pages/index.tmpl.html index ee0a0e0..3f30833 100644 --- a/ui/html/pages/index.tmpl.html +++ b/ui/html/pages/index.tmpl.html @@ -42,6 +42,9 @@ </div> {{end}} <div class="flex_end bottom_elem"> + <a href="/feed.rss" class="rss"> + <img src="/static/get?file=images/feed.svg" alt="RSS"> + </a> <div class="right"> {{if ne .Offset 0}} <a href="/?offset={{sub .Offset 10}}" class="nav_tag">Previous</a> |
