M .gitignore => .gitignore +1 -1
@@ 3,4 3,4 @@ resources/
test-article.md
.hugo_build.lock
wring.sh
-layouts/partials/webring-out.html
+data/openring.toml
A go.mod => go.mod +18 -0
@@ 0,0 1,18 @@
+module git.code.netlandish.com/~netlandish/linktaco-blog
+
+go 1.23.4
+
+require (
+ github.com/BurntSushi/toml v1.4.0
+ github.com/SlyMarbo/rss v1.0.5
+ github.com/mattn/go-runewidth v0.0.16
+ github.com/microcosm-cc/bluemonday v1.0.27
+)
+
+require (
+ github.com/axgle/mahonia v0.0.0-20180208002826-3358181d7394 // indirect
+ github.com/aymerick/douceur v0.2.0 // indirect
+ github.com/gorilla/css v1.0.1 // indirect
+ github.com/rivo/uniseg v0.2.0 // indirect
+ golang.org/x/net v0.26.0 // indirect
+)
A go.sum => go.sum +18 -0
@@ 0,0 1,18 @@
+github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0=
+github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
+github.com/SlyMarbo/rss v1.0.5 h1:DPcZ4aOXXHJ5yNLXY1q/57frIixMmAvTtLxDE3fsMEI=
+github.com/SlyMarbo/rss v1.0.5/go.mod h1:w6Bhn1BZs91q4OlEnJVZEUNRJmlbFmV7BkAlgCN8ofM=
+github.com/axgle/mahonia v0.0.0-20180208002826-3358181d7394 h1:OYA+5W64v3OgClL+IrOD63t4i/RW7RqrAVl9LTZ9UqQ=
+github.com/axgle/mahonia v0.0.0-20180208002826-3358181d7394/go.mod h1:Q8n74mJTIgjX4RBBcHnJ05h//6/k6foqmgE45jTQtxg=
+github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
+github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
+github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8=
+github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0=
+github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
+github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
+github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk=
+github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA=
+github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
+github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
+golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ=
+golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE=
M layouts/_default/baseof.html => layouts/_default/baseof.html +1 -1
@@ 73,7 73,7 @@
<footer>
{{ block "webring" . }}
- {{ partial "webring-out.html" }}
+ {{ partial "webring-out.html" . }}
{{ end }}
<div class="footer">
<span>Follow Us On:</span>
A layouts/partials/webring-out.html => layouts/partials/webring-out.html +21 -0
@@ 0,0 1,21 @@
+{{ if and $.Site.Data.openring $.Site.Data.openring.Articles }}
+{{ $articles := $.Site.Data.openring.Articles }}
+ <section class="webring">
+ <h2>
+ Articles from blogs we recommend
+ <small class="attribution">Generated by <a href="https://git.sr.ht/~sircmpwn/openring">openring</a></small>
+ </h2>
+ <section class="articles xrow xis-center">
+ {{range $articles}}
+ <div class="card article">
+ <h4><a href="{{ .Link }}" target="_blank" rel="noopener">{{ .Title }}</a></h4>
+ <p>{{ .Summary }}</p>
+ <footer>
+ via <a href="{{ .SourceLink }}" target="_blank" rel="noopener">{{ .SourceTitle }}</a><br />
+ {{ .Date.Format "Jan 2, 2006" }}
+ </footer>
+ </div>
+ {{ end }}
+ </section>
+ </section>
+{{ end }}
A openring.go => openring.go +98 -0
@@ 0,0 1,98 @@
+package main
+
+import (
+ "flag"
+ "html"
+ "log"
+ "net/url"
+ "os"
+ "sort"
+ "time"
+
+ "github.com/BurntSushi/toml"
+ "github.com/SlyMarbo/rss"
+ "github.com/mattn/go-runewidth"
+ "github.com/microcosm-cc/bluemonday"
+)
+
+type Article struct {
+ Date time.Time
+ Link string
+ SourceLink string
+ SourceTitle string
+ Summary string
+ Title string
+}
+
+func main() {
+ var narticles, summaryLen int
+ var sources []*url.URL
+ flag.IntVar(&narticles, "n", 3, "number of output articles")
+ flag.IntVar(&summaryLen, "l", 256, "maximum summary length")
+ flag.Func("s", "source URL", func(s string) error {
+ u, err := url.Parse(s)
+ if err != nil {
+ return err
+ }
+ sources = append(sources, u)
+ return nil
+ })
+ flag.Parse()
+
+ if len(flag.Args()) > 0 {
+ log.Fatalf("Usage: %s [-s https://source.rss...] > out.toml", os.Args[0])
+ }
+
+ log.Println("Fetching feeds...")
+ var feeds []*rss.Feed
+ for _, source := range sources {
+ feed, err := rss.Fetch(source.String())
+ if err != nil {
+ log.Printf("Error fetching %s: %s", source.String(), err.Error())
+ continue
+ }
+ feeds = append(feeds, feed)
+ log.Printf("Fetched %s", feed.Title)
+ }
+ if len(feeds) == 0 {
+ log.Fatal("Expected at least one feed to successfully fetch")
+ }
+
+ policy := bluemonday.StrictPolicy()
+
+ var articles []Article
+ for _, feed := range feeds {
+ if len(feed.Items) == 0 {
+ log.Printf("Warning: feed %s has no items", feed.Title)
+ continue
+ }
+ item := feed.Items[0]
+ rawSummary := item.Summary
+ if len(rawSummary) == 0 {
+ rawSummary = html.UnescapeString(item.Content)
+ }
+ summary := runewidth.Truncate(
+ policy.Sanitize(rawSummary), summaryLen, "…")
+ articles = append(articles, Article{
+ Date: item.Date,
+ SourceLink: feed.Link,
+ SourceTitle: html.UnescapeString(feed.Title),
+ Summary: summary,
+ Title: html.UnescapeString(item.Title),
+ Link: item.Link,
+ })
+ }
+ sort.Slice(articles, func(i, j int) bool {
+ return articles[i].Date.After(articles[j].Date)
+ })
+ if len(articles) < narticles {
+ narticles = len(articles)
+ }
+ articles = articles[:narticles]
+
+ if err := toml.NewEncoder(os.Stdout).Encode(struct {
+ Articles []Article
+ }{articles}); err != nil {
+ log.Fatal(err)
+ }
+}