aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSamuel Johnson <[email protected]>2025-12-11 02:51:42 -0500
committerSamuel Johnson <[email protected]>2025-12-11 02:51:42 -0500
commitd4ad397b85994d4d6fdfbf75ce1bc65fdb2f9b33 (patch)
tree7dfe7586ed1b7c72432d98a467a82f785c2e557f
parent6785f856b81b6c0de8c8828761779732b30191af (diff)
Add RSS feed constructor
-rw-r--r--cmd/web/handlers/export.go109
-rw-r--r--cmd/web/handlers/routes.go7
-rw-r--r--cmd/web/main.go1
-rw-r--r--go.mod4
-rw-r--r--internal/context/environment.go1
-rw-r--r--static/app.css20
-rw-r--r--ui/html/pages/index.tmpl.html3
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,
diff --git a/go.mod b/go.mod
index e227af7..7b651ce 100644
--- a/go.mod
+++ b/go.mod
@@ -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>