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.
This commit is contained in:
mntn 2021-09-12 07:26:33 -04:00 committed by GitHub
parent 492deddf06
commit 2de6e634d6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 144 additions and 6 deletions

View file

@ -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

View file

@ -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 {

View file

@ -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 -}}
<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
<channel>
<title>{{ if $Site.Title }}{{ html $Site.Title }}{{ else }}Site feed{{ with $Dirname }} for {{ html . }}{{end}}{{end}}</title>
<link>{{ $DirLink }}</link>
<description>Recent content{{ with $Dirname }} in {{ html . }}{{end}}{{ with $Site.Title }} on {{ html . }}{{end}}</description>
<generator>gmnhg</generator>{{ with $Site.LanguageCode }}
<language>{{ html .}}</language>{{end}}{{ with $Site.Author.email }}
<managingEditor>{{ html . }}{{ with $Site.Author.name }} ({{ html . }}){{end}}</managingEditor>
<webMaster>{{ html . }}{{ with $Site.Author.name }} ({{ html . }}){{end}}</webMaster>{{end}}{{ with $Site.Copyright }}
<copyright>{{ html . }}</copyright>{{end}}
<lastBuildDate>{{ now.Format "Mon, 02 Jan 2006 15:04:05 -0700" }}</lastBuildDate>
{{ printf "<atom:link href=%q rel=\"self\" type=\"application/rss+xml\" />" $RssLink }}
{{ range $i, $p := .Posts | sortPosts }}{{ if lt $i 25 }}
{{- $AbsURL := list (trimSuffix "/" $Site.GeminiBaseURL) (trimPrefix "/" $p.Link) | join "/" | html }}
<item>
<title>{{ if $p.Metadata.PostTitle }}{{ html $p.Metadata.PostTitle }}{{ else }}{{ trimPrefix "/" $p.Link | html }}{{end}}</title>
<link>{{ $AbsURL }}</link>
<pubDate>{{ $p.Metadata.PostDate.Format "Mon, 02 Jan 2006 15:04:05 -0700" }}</pubDate>
<guid>{{ $AbsURL }}</guid>
<description>{{ html $p.Metadata.PostSummary }}</description>
</item>
{{end}}{{end}}
</channel>
</rss>
`)

1
go.mod
View file

@ -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

2
go.sum
View file

@ -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=

View file

@ -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"`
}