From 2de6e634d6d01d49ba5e9b6055c907ab050f7e72 Mon Sep 17 00:00:00 2001 From: mntn <85877297+mntn-xyz@users.noreply.github.com> Date: Sun, 12 Sep 2021 07:26:33 -0400 Subject: [PATCH] Add RSS support This implements RSS timeline generation in gmnhg. RSS is generated both for the whole site, and for the content directories as an rss.xml file inside these directories. RSS requires the absolute URI to the article. For this to work, a geminiBaseURL setting is required to be set in the Hugo configuration file (config.toml/json/yaml). RSS template can be ovewritten on the site-wide / directory-wise basis; see godoc on how to do this. As there's no discovery method of an RSS timeline in Gemini, the users are expected to put a link to rss.xml on their site where necessary. --- README.md | 8 ++++ cmd/gmnhg/main.go | 100 +++++++++++++++++++++++++++++++++++++++-- cmd/gmnhg/templates.go | 38 ++++++++++++++-- go.mod | 1 + go.sum | 2 + render.go | 1 + 6 files changed, 144 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 95a7fe9..00ca339 100644 --- a/README.md +++ b/README.md @@ -55,6 +55,14 @@ Usage of md2gmn: md2gmn is mainly made to facilitate testing the Gemtext renderer but can be used as a standalone program as well. +## Site configuration + +For RSS feeds to use correct URLs, you should define geminiBaseURL in +Hugo's configuration file (config.toml, config.yaml, or config.json). + +Other attributes from this file, such as site title, will also be used +during RSS feed generation if they are defined. + ## License This program is redistributed under the terms and conditions of the GNU diff --git a/cmd/gmnhg/main.go b/cmd/gmnhg/main.go index 472bfa5..78a056a 100644 --- a/cmd/gmnhg/main.go +++ b/cmd/gmnhg/main.go @@ -78,6 +78,26 @@ // (https://github.com/Masterminds/sprig); see the sprig documentation // for more details. // +// RSS will be generated as rss.xml for the root directory and all +// branch directories. Site title and other RSS metadata will be +// loaded from the Hugo configuration file (config.toml, config.yaml, +// or config.json). +// +// A new setting, geminiBaseURL, should be added to the Hugo +// configuration file to ensure that RSS paths are correct. This is +// more or less the same as Hugo's baseURL, but is separate in case +// your Gemini site is deployed to a different server. +// +// RSS templates can be overriden by defining a template in one of +// several places: +// +// * Site-wide: gmnhg/_default/rss.gotmpl +// +// * Site root: gmnhg/rss.gotmpl +// +// * Directories: gmnhg/rss/dirname.gotmpl for a directory "/dirname" +// or gmnhg/rss/dirname/subdir.gotmpl for "/dirname/subdir" +// // One might want to ignore _index.gmi.md files with the following Hugo // config option in config.toml: // @@ -91,6 +111,7 @@ package main import ( "bytes" + "encoding/json" "errors" "flag" "fmt" @@ -103,6 +124,9 @@ import ( "strings" "text/template" + "github.com/BurntSushi/toml" + "gopkg.in/yaml.v2" + gemini "github.com/tdemin/gmnhg" "github.com/tdemin/gmnhg/internal/gmnhg" ) @@ -111,6 +135,7 @@ const ( defaultPageTemplate = "single" indexMdFilename = "_index.gmi.md" indexFilename = "index.gmi" + rssFilename = "rss.xml" ) const ( @@ -128,6 +153,13 @@ var ( var hugoConfigFiles = []string{"config.toml", "config.yaml", "config.json"} +type SiteConfig struct { + GeminiBaseURL string `yaml:"geminiBaseURL"` + Title string `yaml:"title"` + Copyright string `yaml:"copyright"` + LanguageCode string `yaml:"languageCode"` +} + func copyFile(dst, src string) error { input, err := os.Open(src) if err != nil { @@ -200,9 +232,28 @@ func main() { } configFound := false + var siteConf SiteConfig for _, filename := range hugoConfigFiles { if fileInfo, err := os.Stat(filename); !(os.IsNotExist(err) || fileInfo.IsDir()) { configFound = true + buf, err := ioutil.ReadFile(filename) + if err != nil { + panic(err) + } + switch ext := filepath.Ext(filename); ext { + case ".toml": + if err := toml.Unmarshal(buf, &siteConf); err != nil { + panic(err) + } + case ".yaml": + if err := yaml.Unmarshal(buf, &siteConf); err != nil { + panic(err) + } + case ".json": + if err := json.Unmarshal(buf, &siteConf); err != nil { + panic(err) + } + } break } } @@ -294,7 +345,7 @@ func main() { dirs := strings.Split(matches[1], "/") // only include leaf resources pages in leaf index if !isLeafIndex && hasSubPath(leafIndexPaths, path) { - topLevelPosts[matches[1]] = append(topLevelPosts[matches[1]], p) + topLevelPosts["/"+matches[1]] = append(topLevelPosts["/"+matches[1]], p) } else { // include normal pages in all subdirectory indices for i, dir := range dirs { @@ -303,8 +354,9 @@ func main() { } } for _, dir := range dirs { - topLevelPosts[dir] = append(topLevelPosts[dir], p) + topLevelPosts["/"+dir] = append(topLevelPosts["/"+dir], p) } + topLevelPosts["/"] = append(topLevelPosts["/"], p) } } return nil @@ -353,7 +405,11 @@ func main() { } // render indexes for top-level dirs for dirname, posts := range topLevelPosts { - tmpl, hasTmpl := templates["top/"+dirname] + // skip the main index + if dirname == "/" { + continue + } + tmpl, hasTmpl := templates["top"+dirname] if !hasTmpl { continue } @@ -403,6 +459,44 @@ func main() { panic(err) } + // render RSS/Atom feeds + if tmpl, hasTmpl := templates["_default/rss"]; hasTmpl { + defaultRssTemplate = tmpl + } + for dirname, posts := range topLevelPosts { + // do not render RSS for leaf paths + if hasSubPath(leafIndexPaths, path.Join(contentBase, dirname)+"/") { + continue + } + tmpl, hasTmpl := templates["rss"+dirname] + if !hasTmpl { + if rootTmpl, hasTmpl := templates["rss"]; dirname == "/" && hasTmpl { + tmpl = rootTmpl + } else { + tmpl = defaultRssTemplate + } + } + sc := map[string]interface{}{ + "GeminiBaseURL": siteConf.GeminiBaseURL, + "Title": siteConf.Title, + "Copyright": siteConf.Copyright, + "LanguageCode": siteConf.LanguageCode, + } + cnt := map[string]interface{}{ + "Posts": posts, + "Dirname": dirname, + "Link": path.Join(dirname, rssFilename), + "Site": sc, + } + buf := bytes.Buffer{} + if err := tmpl.Execute(&buf, cnt); err != nil { + panic(err) + } + if err := writeFile(path.Join(outputDir, dirname, rssFilename), buf.Bytes()); err != nil { + panic(err) + } + } + // copy page resources to output dir if err := filepath.Walk(contentBase, func(p string, info os.FileInfo, err error) error { if err != nil { diff --git a/cmd/gmnhg/templates.go b/cmd/gmnhg/templates.go index c8256a0..046d9d6 100644 --- a/cmd/gmnhg/templates.go +++ b/cmd/gmnhg/templates.go @@ -44,8 +44,40 @@ var defaultSingleTemplate = mustParseTmpl("single", `# {{ .Metadata.PostTitle }} var defaultIndexTemplate = mustParseTmpl("index", `# Site index {{ with .Content }}{{ printf "%s" . -}}{{ end }} -{{ range $dir, $posts := .PostData }}Index of {{ $dir }}: +{{- range $dir, $posts := .PostData }}{{ if and (ne $dir "/") (eq (dir $dir) "/") }} +Index of {{ trimPrefix "/" $dir }}: -{{ range $p := $posts | sortPosts }}=> {{ $p.Link }} {{ $p.Metadata.PostDate.Format "2006-01-02 15:04" }} - {{ $p.Metadata.PostTitle }} -{{ end }}{{ end }} +{{ range $p := $posts | sortPosts }}=> {{ $p.Link }} {{ $p.Metadata.PostDate.Format "2006-01-02 15:04" }} - {{ if $p.Metadata.PostTitle }}{{ $p.Metadata.PostTitle }}{{else}}{{ $p.Link }}{{end}} +{{ end }}{{ end }}{{ end }} +`) + +var defaultRssTemplate = mustParseTmpl("rss", `{{- $Site := .Site -}} +{{- $Dirname := trimPrefix "/" .Dirname -}} +{{- $DirLink := list (trimSuffix "/" $Site.GeminiBaseURL) $Dirname | join "/" | html -}} +{{- $RssLink := list (trimSuffix "/" $Site.GeminiBaseURL) (trimPrefix "/" .Link) | join "/" | html -}} + + + + {{ if $Site.Title }}{{ html $Site.Title }}{{ else }}Site feed{{ with $Dirname }} for {{ html . }}{{end}}{{end}} + {{ $DirLink }} + Recent content{{ with $Dirname }} in {{ html . }}{{end}}{{ with $Site.Title }} on {{ html . }}{{end}} + gmnhg{{ with $Site.LanguageCode }} + {{ html .}}{{end}}{{ with $Site.Author.email }} + {{ html . }}{{ with $Site.Author.name }} ({{ html . }}){{end}} + {{ html . }}{{ with $Site.Author.name }} ({{ html . }}){{end}}{{end}}{{ with $Site.Copyright }} + {{ html . }}{{end}} + {{ now.Format "Mon, 02 Jan 2006 15:04:05 -0700" }} + {{ printf "" $RssLink }} + {{ range $i, $p := .Posts | sortPosts }}{{ if lt $i 25 }} + {{- $AbsURL := list (trimSuffix "/" $Site.GeminiBaseURL) (trimPrefix "/" $p.Link) | join "/" | html }} + + {{ if $p.Metadata.PostTitle }}{{ html $p.Metadata.PostTitle }}{{ else }}{{ trimPrefix "/" $p.Link | html }}{{end}} + {{ $AbsURL }} + {{ $p.Metadata.PostDate.Format "Mon, 02 Jan 2006 15:04:05 -0700" }} + {{ $AbsURL }} + {{ html $p.Metadata.PostSummary }} + + {{end}}{{end}} + + `) diff --git a/go.mod b/go.mod index 20b7ca2..4fd1b29 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/tdemin/gmnhg go 1.16 require ( + github.com/BurntSushi/toml v0.4.1 github.com/Masterminds/sprig/v3 v3.2.2 github.com/gomarkdown/markdown v0.0.0-20210514010506-3b9f47219fe7 github.com/mattn/go-runewidth v0.0.13 // indirect diff --git a/go.sum b/go.sum index f67b1a2..40c8c5b 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +github.com/BurntSushi/toml v0.4.1 h1:GaI7EiDXDRfa8VshkTj7Fym7ha+y8/XxIgD2okUIjLw= +github.com/BurntSushi/toml v0.4.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= github.com/Masterminds/semver/v3 v3.1.1 h1:hLg3sBzpNErnxhQtUy/mmLR2I9foDujNK030IGemrRc= diff --git a/render.go b/render.go index f60680d..615867a 100644 --- a/render.go +++ b/render.go @@ -36,6 +36,7 @@ type HugoMetadata struct { PostIsDraft bool `yaml:"draft"` PostLayout string `yaml:"layout"` PostDate time.Time `yaml:"date"` + PostSummary string `yaml:"summary"` IsHeadless bool `yaml:"headless"` }