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:
parent
492deddf06
commit
2de6e634d6
6 changed files with 144 additions and 6 deletions
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
1
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
|
||||
|
|
2
go.sum
2
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=
|
||||
|
|
|
@ -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"`
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue