diff --git a/plugins/scholar/scholar.go b/plugins/scholar/scholar.go
index f88a5a1..e23f823 100644
--- a/plugins/scholar/scholar.go
+++ b/plugins/scholar/scholar.go
@@ -7,8 +7,12 @@ import (
"errors"
"fmt"
"goblog/blog"
+ "html"
"log"
+ "net/url"
"sort"
+ "strconv"
+ "strings"
"sync"
"time"
@@ -115,12 +119,13 @@ func (p *ScholarPlugin) RenderPage(ctx *gplugin.HookContext, pageType string) (s
return "", nil
}
+ data := gin.H{"has_plugin_content": true}
+
settings := ctx.Settings
scholarID := settings["scholar_id"]
if scholarID == "" {
- return "page_research.html", gin.H{
- "errors": "Google Scholar ID not configured. Set it in the Scholar plugin settings.",
- }
+ data["plugin_content"] = `
Google Scholar ID not configured. Set it in the Scholar plugin settings.
`
+ return "page_content.html", data
}
limitStr := settings["article_limit"]
@@ -132,18 +137,71 @@ func (p *ScholarPlugin) RenderPage(ctx *gplugin.HookContext, pageType string) (s
p.ensureScholar(settings)
articles, err := p.sch.QueryProfileWithMemoryCache(scholarID, limit)
- data := gin.H{}
- if err == nil {
- sortArticlesByDateDesc(articles)
- p.sch.SaveCache(settings["profile_cache"], settings["article_cache"])
- data["articles"] = articles
- } else {
+ if err != nil {
log.Printf("Scholar query failed: %v", err)
- data["articles"] = make([]*scholarlib.Article, 0)
- data["errors"] = err.Error()
+ data["plugin_content"] = `` + html.EscapeString(err.Error()) + `
`
+ return "page_content.html", data
+ }
+
+ sortArticlesByDateDesc(articles)
+ p.sch.SaveCache(settings["profile_cache"], settings["article_cache"])
+
+ data["plugin_content"] = renderArticlesHTML(articles)
+ return "page_content.html", data
+}
+
+// safeHref returns the URL only if it uses http or https scheme, otherwise empty.
+func safeHref(rawURL string) string {
+ u, err := url.Parse(rawURL)
+ if err != nil {
+ return ""
+ }
+ if u.Scheme != "http" && u.Scheme != "https" {
+ return ""
+ }
+ return html.EscapeString(rawURL)
+}
+
+// renderArticlesHTML generates the HTML for the articles list.
+func renderArticlesHTML(articles []*scholarlib.Article) string {
+ if len(articles) == 0 {
+ return `No publications found.
`
}
- return "page_research.html", data
+ var b strings.Builder
+ for _, a := range articles {
+ b.WriteString(``)
+
+ // Title — link only if URL is safe
+ href := safeHref(a.ScholarURL)
+ if href != "" {
+ b.WriteString(`
`)
+ } else {
+ b.WriteString(`
` + html.EscapeString(a.Title) + `
`)
+ }
+
+ if a.Authors != "" {
+ b.WriteString(`
` + html.EscapeString(a.Authors) + `
`)
+ }
+
+ // Meta line: year · journal · citations
+ var meta []string
+ if a.Year > 0 {
+ meta = append(meta, strconv.Itoa(a.Year))
+ }
+ if a.Journal != "" {
+ meta = append(meta, html.EscapeString(a.Journal))
+ }
+ if a.NumCitations > 0 {
+ meta = append(meta, strconv.Itoa(a.NumCitations)+" citations")
+ }
+ if len(meta) > 0 {
+ b.WriteString(`
` + strings.Join(meta, " · ") + `
`)
+ }
+
+ b.WriteString(`
`)
+ }
+ return b.String()
}
func (p *ScholarPlugin) ScheduledJobs() []gplugin.ScheduledJob {
diff --git a/plugins/scholar/scholar_test.go b/plugins/scholar/scholar_test.go
index f3ca677..1b816bd 100644
--- a/plugins/scholar/scholar_test.go
+++ b/plugins/scholar/scholar_test.go
@@ -1,8 +1,10 @@
package scholar
import (
- scholarlib "github.com/compscidr/scholar"
+ "strings"
"testing"
+
+ scholarlib "github.com/compscidr/scholar"
)
func TestSortArticlesByDateDesc(t *testing.T) {
@@ -23,3 +25,84 @@ func TestSortArticlesByDateDesc(t *testing.T) {
}
}
}
+
+func TestRenderArticlesHTML_Empty(t *testing.T) {
+ result := renderArticlesHTML(nil)
+ if !strings.Contains(result, "No publications found") {
+ t.Errorf("expected 'No publications found' for empty list, got %q", result)
+ }
+}
+
+func TestRenderArticlesHTML_WithArticles(t *testing.T) {
+ articles := []*scholarlib.Article{
+ {
+ Title: "Test Paper",
+ Authors: "Alice, Bob",
+ ScholarURL: "https://scholar.google.com/test",
+ Year: 2024,
+ Journal: "Test Journal",
+ NumCitations: 42,
+ },
+ }
+ result := renderArticlesHTML(articles)
+
+ if !strings.Contains(result, "Test Paper") {
+ t.Error("expected title in output")
+ }
+ if !strings.Contains(result, "Alice, Bob") {
+ t.Error("expected authors in output")
+ }
+ if !strings.Contains(result, "2024") {
+ t.Error("expected year in output")
+ }
+ if !strings.Contains(result, "Test Journal") {
+ t.Error("expected journal in output")
+ }
+ if !strings.Contains(result, "42 citations") {
+ t.Error("expected citation count in output")
+ }
+ if !strings.Contains(result, `href="https://scholar.google.com/test"`) {
+ t.Error("expected scholar URL in href")
+ }
+}
+
+func TestRenderArticlesHTML_XSSEscaping(t *testing.T) {
+ articles := []*scholarlib.Article{
+ {
+ Title: ``,
+ Authors: `Bob "the hacker"`,
+ ScholarURL: "https://scholar.google.com/safe",
+ },
+ }
+ result := renderArticlesHTML(articles)
+
+ if strings.Contains(result, "
+
+
+ {{ end }}
-
-
-
-
{{ template "footer.html" .}}
diff --git a/themes/default/templates/page_research.html b/themes/default/templates/page_research.html
deleted file mode 100644
index f998526..0000000
--- a/themes/default/templates/page_research.html
+++ /dev/null
@@ -1,30 +0,0 @@
-{{ template "header.html" .}}
-
- {{ if .page.HasHero }}
-
-
-
- {{ end }}
-
-
-
{{ .page.Title }}
- {{ if .is_admin }}
-
Edit this page
- {{ end }}
- {{ if .errors }}
-
- {{ .errors }}
-
- {{ end }}
- {{range .articles}}
-
- {{end}}
-
-
-{{ template "footer.html" .}}
diff --git a/themes/default/templates/research.html b/themes/default/templates/research.html
deleted file mode 100644
index 0292b55..0000000
--- a/themes/default/templates/research.html
+++ /dev/null
@@ -1,25 +0,0 @@
-{{ template "header.html" .}}
-
-
-
-
-
-
-
Research and Publications
- {{ if .errors }}
-
- {{ .errors }}
-
- {{ end }}
- {{range .articles}}
-
- {{end}}
-
-
-{{ template "footer.html" .}}
\ No newline at end of file
diff --git a/themes/forest/templates/page_content.html b/themes/forest/templates/page_content.html
index 453e7f5..67e10b8 100644
--- a/themes/forest/templates/page_content.html
+++ b/themes/forest/templates/page_content.html
@@ -5,15 +5,18 @@ {{ .page.Title }}
{{ if .is_admin }}
Edit this page
{{ end }}
+ {{ if .has_plugin_content }}
+ {{ .plugin_content | rawHTML }}
+ {{ else }}
+
+
+
+ {{ end }}
-
-
-
-
{{ template "footer.html" .}}
diff --git a/themes/forest/templates/page_research.html b/themes/forest/templates/page_research.html
deleted file mode 100644
index ca191f1..0000000
--- a/themes/forest/templates/page_research.html
+++ /dev/null
@@ -1,24 +0,0 @@
-{{ template "header.html" .}}
-
-
-
{{ .page.Title }}
- {{ if .is_admin }}
-
Edit this page
- {{ end }}
- {{ if .errors }}
-
- {{ .errors }}
-
- {{ end }}
- {{range .articles}}
-
- {{end}}
-
-
-{{ template "footer.html" .}}
diff --git a/themes/minimal/templates/page_content.html b/themes/minimal/templates/page_content.html
index 33ca2e3..459946d 100644
--- a/themes/minimal/templates/page_content.html
+++ b/themes/minimal/templates/page_content.html
@@ -5,15 +5,18 @@ {{ .page.Title }}
{{ if .is_admin }}
Edit this page
{{ end }}
+ {{ if .has_plugin_content }}
+ {{ .plugin_content | rawHTML }}
+ {{ else }}
+
+
+
+ {{ end }}
-
-
-
-
{{ template "footer.html" .}}
diff --git a/themes/minimal/templates/page_research.html b/themes/minimal/templates/page_research.html
deleted file mode 100644
index 90e874d..0000000
--- a/themes/minimal/templates/page_research.html
+++ /dev/null
@@ -1,24 +0,0 @@
-{{ template "header.html" .}}
-
-
-
{{ .page.Title }}
- {{ if .is_admin }}
-
Edit this page
- {{ end }}
- {{ if .errors }}
-
- {{ .errors }}
-
- {{ end }}
- {{range .articles}}
-
- {{end}}
-
-
-{{ template "footer.html" .}}