Add option to filter for parameters on index

This commit is contained in:
Jan-Lukas Else 2023-09-07 17:01:53 +02:00
parent 272353ad7a
commit 9da5f0085a
8 changed files with 109 additions and 56 deletions

View File

@ -20,9 +20,9 @@ import (
func (a *goBlog) checkAllExternalLinks() error { func (a *goBlog) checkAllExternalLinks() error {
posts, err := a.getPosts(&postsRequestConfig{ posts, err := a.getPosts(&postsRequestConfig{
status: []postStatus{statusPublished}, status: []postStatus{statusPublished},
visibility: []postVisibility{visibilityPublic, visibilityUnlisted}, visibility: []postVisibility{visibilityPublic, visibilityUnlisted},
withoutParameters: true, fetchWithoutParams: true,
}) })
if err != nil { if err != nil {
return err return err

View File

@ -3,7 +3,6 @@ package main
import ( import (
"io" "io"
"net/http" "net/http"
"strings"
"time" "time"
"github.com/araddon/dateparse" "github.com/araddon/dateparse"
@ -24,14 +23,14 @@ const (
minJsonFeed feedType = "min.json" minJsonFeed feedType = "min.json"
) )
func (a *goBlog) generateFeed(blog string, f feedType, w http.ResponseWriter, r *http.Request, posts []*post, title, description string) { func (a *goBlog) generateFeed(blog string, f feedType, w http.ResponseWriter, r *http.Request, posts []*post, title, description, path, query string) {
now := time.Now() now := time.Now()
title = a.renderMdTitle(defaultIfEmpty(title, a.cfg.Blogs[blog].Title)) title = a.renderMdTitle(defaultIfEmpty(title, a.cfg.Blogs[blog].Title))
description = defaultIfEmpty(description, a.cfg.Blogs[blog].Description) description = defaultIfEmpty(description, a.cfg.Blogs[blog].Description)
feed := &feeds.Feed{ feed := &feeds.Feed{
Title: title, Title: title,
Description: description, Description: description,
Link: &feeds.Link{Href: a.getFullAddress(strings.TrimSuffix(r.URL.Path, "."+string(f)))}, Link: &feeds.Link{Href: a.getFullAddress(path) + query},
Created: now, Created: now,
Author: &feeds.Author{ Author: &feeds.Author{
Name: a.cfg.User.Name, Name: a.cfg.User.Name,

View File

@ -17,9 +17,9 @@ func (a *goBlog) serveGeoMap(w http.ResponseWriter, r *http.Request) {
canonical := a.getFullAddress(mapPath) canonical := a.getFullAddress(mapPath)
allPostsWithLocationRequestConfig := &postsRequestConfig{ allPostsWithLocationRequestConfig := &postsRequestConfig{
blog: blog, blog: blog,
parameters: []string{a.cfg.Micropub.LocationParam, gpxParameter}, anyParams: []string{a.cfg.Micropub.LocationParam, gpxParameter},
withOnlyParameters: []string{a.cfg.Micropub.LocationParam, gpxParameter}, fetchParams: []string{a.cfg.Micropub.LocationParam, gpxParameter},
} }
allPostsWithLocationRequestConfig.status, allPostsWithLocationRequestConfig.visibility = a.getDefaultPostStates(r) allPostsWithLocationRequestConfig.status, allPostsWithLocationRequestConfig.visibility = a.getDefaultPostStates(r)
@ -58,8 +58,8 @@ func (a *goBlog) serveGeoMapTracks(w http.ResponseWriter, r *http.Request) {
allPostsWithTracksRequestConfig := &postsRequestConfig{ allPostsWithTracksRequestConfig := &postsRequestConfig{
blog: blog, blog: blog,
parameters: []string{gpxParameter}, anyParams: []string{gpxParameter},
withOnlyParameters: []string{gpxParameter}, fetchParams: []string{gpxParameter},
excludeParameter: showRouteParam, excludeParameter: showRouteParam,
excludeParameterValue: "false", // Don't show hidden route tracks excludeParameterValue: "false", // Don't show hidden route tracks
} }
@ -102,9 +102,9 @@ func (a *goBlog) serveGeoMapLocations(w http.ResponseWriter, r *http.Request) {
blog, _ := a.getBlog(r) blog, _ := a.getBlog(r)
allPostsWithLocationRequestConfig := &postsRequestConfig{ allPostsWithLocationRequestConfig := &postsRequestConfig{
blog: blog, blog: blog,
parameters: []string{a.cfg.Micropub.LocationParam}, anyParams: []string{a.cfg.Micropub.LocationParam},
withOnlyParameters: []string{a.cfg.Micropub.LocationParam}, fetchParams: []string{a.cfg.Micropub.LocationParam},
} }
allPostsWithLocationRequestConfig.status, allPostsWithLocationRequestConfig.visibility = a.getDefaultPostStates(r) allPostsWithLocationRequestConfig.status, allPostsWithLocationRequestConfig.visibility = a.getDefaultPostStates(r)

View File

@ -5,6 +5,7 @@ import (
"errors" "errors"
"fmt" "fmt"
"net/http" "net/http"
"net/url"
"path" "path"
"reflect" "reflect"
"strings" "strings"
@ -314,12 +315,31 @@ func (a *goBlog) serveIndex(w http.ResponseWriter, r *http.Request) {
if len(visibility) == 0 { if len(visibility) == 0 {
visibility = defaultVisibility visibility = defaultVisibility
} }
// Parameter filter
params, paramValues := []string{}, []string{}
paramUrlValues := url.Values{}
for param, values := range r.URL.Query() {
if strings.HasPrefix(param, "p:") {
paramKey := strings.TrimPrefix(param, "p:")
for _, value := range values {
params, paramValues = append(params, paramKey), append(paramValues, value)
paramUrlValues.Add(param, value)
}
}
}
paramUrlQuery := ""
if len(paramUrlValues) > 0 {
paramUrlQuery += "?" + paramUrlValues.Encode()
}
// Create paginator
p := paginator.New(&postPaginationAdapter{config: &postsRequestConfig{ p := paginator.New(&postPaginationAdapter{config: &postsRequestConfig{
blog: blog, blog: blog,
sections: sections, sections: sections,
taxonomy: ic.tax, taxonomy: ic.tax,
taxonomyValue: ic.taxValue, taxonomyValue: ic.taxValue,
parameter: ic.parameter, parameter: ic.parameter,
allParams: params,
allParamValues: paramValues,
search: ic.search, search: ic.search,
publishedYear: ic.year, publishedYear: ic.year,
publishedMonth: ic.month, publishedMonth: ic.month,
@ -356,11 +376,10 @@ func (a *goBlog) serveIndex(w http.ResponseWriter, r *http.Request) {
} }
// Check if feed // Check if feed
if ft := feedType(chi.URLParam(r, "feed")); ft != noFeed { if ft := feedType(chi.URLParam(r, "feed")); ft != noFeed {
a.generateFeed(blog, ft, w, r, posts, title, description) a.generateFeed(blog, ft, w, r, posts, title, description, ic.path, paramUrlQuery)
return return
} }
// Navigation // Navigation
path := ic.path
var hasPrev, hasNext bool var hasPrev, hasNext bool
var prevPage, nextPage int var prevPage, nextPage int
var prevPath, nextPath string var prevPath, nextPath string
@ -371,9 +390,9 @@ func (a *goBlog) serveIndex(w http.ResponseWriter, r *http.Request) {
prevPage, _ = p.Page() prevPage, _ = p.Page()
} }
if prevPage < 2 { if prevPage < 2 {
prevPath = path prevPath = ic.path
} else { } else {
prevPath = fmt.Sprintf("%s/page/%d", strings.TrimSuffix(path, "/"), prevPage) prevPath = fmt.Sprintf("%s/page/%d", strings.TrimSuffix(ic.path, "/"), prevPage)
} }
hasNext, _ = p.HasNext() hasNext, _ = p.HasNext()
if hasNext { if hasNext {
@ -381,23 +400,24 @@ func (a *goBlog) serveIndex(w http.ResponseWriter, r *http.Request) {
} else { } else {
nextPage, _ = p.Page() nextPage, _ = p.Page()
} }
nextPath = fmt.Sprintf("%s/page/%d", strings.TrimSuffix(path, "/"), nextPage) nextPath = fmt.Sprintf("%s/page/%d", strings.TrimSuffix(ic.path, "/"), nextPage)
summaryTemplate := ic.summaryTemplate summaryTemplate := ic.summaryTemplate
if summaryTemplate == "" { if summaryTemplate == "" {
summaryTemplate = defaultSummary summaryTemplate = defaultSummary
} }
a.render(w, r, a.renderIndex, &renderData{ a.render(w, r, a.renderIndex, &renderData{
Canonical: a.getFullAddress(path), Canonical: a.getFullAddress(ic.path) + paramUrlQuery,
Data: &indexRenderData{ Data: &indexRenderData{
title: title, title: title,
description: description, description: description,
posts: posts, posts: posts,
hasPrev: hasPrev, hasPrev: hasPrev,
hasNext: hasNext, hasNext: hasNext,
first: path, first: ic.path,
prev: prevPath, prev: prevPath,
next: nextPath, next: nextPath,
summaryTemplate: summaryTemplate, summaryTemplate: summaryTemplate,
paramUrlQuery: paramUrlQuery,
}, },
}) })
} }

View File

@ -373,21 +373,23 @@ type postsRequestConfig struct {
visibility []postVisibility visibility []postVisibility
taxonomy *configTaxonomy taxonomy *configTaxonomy
taxonomyValue string taxonomyValue string
parameters []string // Ignores parameterValue anyParams []string // filter for posts that have any of these parameters (with non-empty values)
parameter string // Ignores parameters allParams []string // filter for posts that have all these parameters (with non-empty values)
parameterValue string allParamValues []string // ... with exactly these values
excludeParameter string // exclude posts that have a certain parameter (with non-empty value) parameter string // filter for posts that have this parameter (with non-empty value)
excludeParameterValue string // ... with exactly this value parameterValue string // ... with exactly this value
excludeParameter string // exclude posts that have this parameter (with non-empty value)
excludeParameterValue string // ... with exactly this value
publishedYear, publishedMonth, publishedDay int publishedYear, publishedMonth, publishedDay int
publishedBefore time.Time publishedBefore time.Time
randomOrder bool randomOrder bool
priorityOrder bool priorityOrder bool
withoutParameters bool fetchWithoutParams bool // fetch posts without parameters
withOnlyParameters []string fetchParams []string // only fetch these parameters
withoutRenderedTitle bool withoutRenderedTitle bool // fetch posts without rendered title
} }
func buildPostsQuery(c *postsRequestConfig, selection string) (query string, args []any) { func buildPostsQuery(c *postsRequestConfig, selection string) (query string, args []any, err error) {
queryBuilder := builderpool.Get() queryBuilder := builderpool.Get()
defer builderpool.Put(queryBuilder) defer builderpool.Put(queryBuilder)
// Selection // Selection
@ -437,21 +439,40 @@ func buildPostsQuery(c *postsRequestConfig, selection string) (query string, arg
queryBuilder.WriteString(" and blog = @blog") queryBuilder.WriteString(" and blog = @blog")
args = append(args, sql.Named("blog", c.blog)) args = append(args, sql.Named("blog", c.blog))
} }
if c.parameter != "" { allParams := append(c.allParams, c.parameter)
if c.parameterValue != "" { allParamValues := append(c.allParamValues, c.parameterValue)
queryBuilder.WriteString(" and path in (select path from post_parameters where parameter = @param and value = @paramval)") if len(allParams) > 0 {
args = append(args, sql.Named("param", c.parameter), sql.Named("paramval", c.parameterValue)) if len(allParamValues) > 0 && len(allParamValues) != len(allParams) {
} else { return "", nil, errors.New("number of parameters != number of parameter values")
queryBuilder.WriteString(" and path in (select path from post_parameters where parameter = @param and length(coalesce(value, '')) > 0)")
args = append(args, sql.Named("param", c.parameter))
} }
} else if len(c.parameters) > 0 { for i, param := range allParams {
if param == "" {
continue
}
named := "allparam" + strconv.Itoa(i)
paramValue := allParamValues[i]
queryBuilder.WriteString(" and path in (select path from post_parameters where parameter = @")
queryBuilder.WriteString(named)
queryBuilder.WriteString(" and ")
if paramValue != "" {
namedVal := "allparamval" + strconv.Itoa(i)
queryBuilder.WriteString("value = @")
queryBuilder.WriteString(namedVal)
args = append(args, sql.Named(namedVal, paramValue))
} else {
queryBuilder.WriteString("length(coalesce(value, '')) > 0")
}
queryBuilder.WriteString(")")
args = append(args, sql.Named(named, param))
}
}
if len(c.anyParams) > 0 {
queryBuilder.WriteString(" and path in (select path from post_parameters where parameter in (") queryBuilder.WriteString(" and path in (select path from post_parameters where parameter in (")
for i, param := range c.parameters { for i, param := range c.anyParams {
if i > 0 { if i > 0 {
queryBuilder.WriteString(", ") queryBuilder.WriteString(", ")
} }
named := "param" + strconv.Itoa(i) named := "anyparam" + strconv.Itoa(i)
queryBuilder.WriteString("@") queryBuilder.WriteString("@")
queryBuilder.WriteString(named) queryBuilder.WriteString(named)
args = append(args, param) args = append(args, param)
@ -514,7 +535,7 @@ func buildPostsQuery(c *postsRequestConfig, selection string) (query string, arg
queryBuilder.WriteString(" limit @limit offset @offset") queryBuilder.WriteString(" limit @limit offset @offset")
args = append(args, sql.Named("limit", c.limit), sql.Named("offset", c.offset)) args = append(args, sql.Named("limit", c.limit), sql.Named("offset", c.offset))
} }
return queryBuilder.String(), args return queryBuilder.String(), args, nil
} }
func (d *database) loadPostParameters(posts []*post, parameters ...string) (err error) { func (d *database) loadPostParameters(posts []*post, parameters ...string) (err error) {
@ -582,7 +603,10 @@ func (d *database) loadPostParameters(posts []*post, parameters ...string) (err
func (a *goBlog) getPosts(config *postsRequestConfig) (posts []*post, err error) { func (a *goBlog) getPosts(config *postsRequestConfig) (posts []*post, err error) {
// Query posts // Query posts
query, queryParams := buildPostsQuery(config, "path, coalesce(content, ''), coalesce(published, ''), coalesce(updated, ''), blog, coalesce(section, ''), status, visibility, priority") query, queryParams, err := buildPostsQuery(config, "path, coalesce(content, ''), coalesce(published, ''), coalesce(updated, ''), blog, coalesce(section, ''), status, visibility, priority")
if err != nil {
return nil, err
}
rows, err := a.db.Query(query, queryParams...) rows, err := a.db.Query(query, queryParams...)
if err != nil { if err != nil {
return nil, err return nil, err
@ -608,8 +632,8 @@ func (a *goBlog) getPosts(config *postsRequestConfig) (posts []*post, err error)
} }
posts = append(posts, p) posts = append(posts, p)
} }
if !config.withoutParameters { if !config.fetchWithoutParams {
err = a.db.loadPostParameters(posts, config.withOnlyParameters...) err = a.db.loadPostParameters(posts, config.fetchParams...)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -636,7 +660,10 @@ func (a *goBlog) getPost(path string) (*post, error) {
} }
func (d *database) countPosts(config *postsRequestConfig) (count int, err error) { func (d *database) countPosts(config *postsRequestConfig) (count int, err error) {
query, params := buildPostsQuery(config, "path") query, params, err := buildPostsQuery(config, "path")
if err != nil {
return
}
row, err := d.QueryRow("select count(distinct path) from ("+query+")", params...) row, err := d.QueryRow("select count(distinct path) from ("+query+")", params...)
if err != nil { if err != nil {
return return
@ -647,7 +674,7 @@ func (d *database) countPosts(config *postsRequestConfig) (count int, err error)
func (a *goBlog) getRandomPostPath(blog string) (path string, err error) { func (a *goBlog) getRandomPostPath(blog string) (path string, err error) {
sections := lo.Keys(a.cfg.Blogs[blog].Sections) sections := lo.Keys(a.cfg.Blogs[blog].Sections)
query, params := buildPostsQuery(&postsRequestConfig{ query, params, err := buildPostsQuery(&postsRequestConfig{
randomOrder: true, randomOrder: true,
limit: 1, limit: 1,
blog: blog, blog: blog,
@ -655,6 +682,9 @@ func (a *goBlog) getRandomPostPath(blog string) (path string, err error) {
visibility: []postVisibility{visibilityPublic}, visibility: []postVisibility{visibilityPublic},
status: []postStatus{statusPublished}, status: []postStatus{statusPublished},
}, "path") }, "path")
if err != nil {
return
}
row, err := a.db.QueryRow(query, params...) row, err := a.db.QueryRow(query, params...)
if err != nil { if err != nil {
return return

View File

@ -79,7 +79,7 @@ func Test_postsDb(t *testing.T) {
is.Equal(0, count) is.Equal(0, count)
// Check by multiple parameters // Check by multiple parameters
count, err = app.db.countPosts(&postsRequestConfig{parameters: []string{"tags", "title"}}) count, err = app.db.countPosts(&postsRequestConfig{anyParams: []string{"tags", "title"}})
must.NoError(err) must.NoError(err)
is.Equal(1, count) is.Equal(1, count)

View File

@ -162,10 +162,10 @@ func (a *goBlog) serveSitemapBlogPosts(w http.ResponseWriter, r *http.Request) {
// Request posts // Request posts
blog, _ := a.getBlog(r) blog, _ := a.getBlog(r)
posts, _ := a.getPosts(&postsRequestConfig{ posts, _ := a.getPosts(&postsRequestConfig{
status: []postStatus{statusPublished}, status: []postStatus{statusPublished},
visibility: []postVisibility{visibilityPublic}, visibility: []postVisibility{visibilityPublic},
blog: blog, blog: blog,
withoutParameters: true, fetchWithoutParams: true,
}) })
// Add posts to sitemap // Add posts to sitemap
for _, p := range posts { for _, p := range posts {
@ -220,12 +220,15 @@ select distinct '/x/x/' || day from alldates;
` `
func (a *goBlog) sitemapDatePaths(blog string, sections []string) (paths []string, err error) { func (a *goBlog) sitemapDatePaths(blog string, sections []string) (paths []string, err error) {
query, args := buildPostsQuery(&postsRequestConfig{ query, args, err := buildPostsQuery(&postsRequestConfig{
blog: blog, blog: blog,
sections: sections, sections: sections,
status: []postStatus{statusPublished}, status: []postStatus{statusPublished},
visibility: []postVisibility{visibilityPublic}, visibility: []postVisibility{visibilityPublic},
}, "published") }, "published")
if err != nil {
return
}
rows, err := a.db.Query(fmt.Sprintf(sitemapDatePathsSql, query), args...) rows, err := a.db.Query(fmt.Sprintf(sitemapDatePathsSql, query), args...)
if err != nil { if err != nil {
return nil, err return nil, err

9
ui.go
View File

@ -340,6 +340,7 @@ type indexRenderData struct {
posts []*post posts []*post
hasPrev, hasNext bool hasPrev, hasNext bool
first, prev, next string first, prev, next string
paramUrlQuery string
summaryTemplate summaryTyp summaryTemplate summaryTyp
} }
@ -359,9 +360,9 @@ func (a *goBlog) renderIndex(hb *htmlbuilder.HtmlBuilder, rd *renderData) {
if renderedIndexTitle != "" { if renderedIndexTitle != "" {
feedTitle = " (" + renderedIndexTitle + ")" feedTitle = " (" + renderedIndexTitle + ")"
} }
hb.WriteElementOpen("link", "rel", "alternate", "type", "application/rss+xml", "title", "RSS"+feedTitle, "href", a.getFullAddress(id.first+".rss")) hb.WriteElementOpen("link", "rel", "alternate", "type", "application/rss+xml", "title", "RSS"+feedTitle, "href", a.getFullAddress(id.first+".rss")+id.paramUrlQuery)
hb.WriteElementOpen("link", "rel", "alternate", "type", "application/atom+xml", "title", "ATOM"+feedTitle, "href", a.getFullAddress(id.first+".atom")) hb.WriteElementOpen("link", "rel", "alternate", "type", "application/atom+xml", "title", "ATOM"+feedTitle, "href", a.getFullAddress(id.first+".atom")+id.paramUrlQuery)
hb.WriteElementOpen("link", "rel", "alternate", "type", "application/feed+json", "title", "JSON Feed"+feedTitle, "href", a.getFullAddress(id.first+".json")) hb.WriteElementOpen("link", "rel", "alternate", "type", "application/feed+json", "title", "JSON Feed"+feedTitle, "href", a.getFullAddress(id.first+".json")+id.paramUrlQuery)
}, },
func(hb *htmlbuilder.HtmlBuilder) { func(hb *htmlbuilder.HtmlBuilder) {
hb.WriteElementOpen("main", "class", "h-feed") hb.WriteElementOpen("main", "class", "h-feed")
@ -393,7 +394,7 @@ func (a *goBlog) renderIndex(hb *htmlbuilder.HtmlBuilder, rd *renderData) {
hb.WriteElementClose("p") hb.WriteElementClose("p")
} }
// Navigation // Navigation
a.renderPagination(hb, rd.Blog, id.hasPrev, id.hasNext, id.prev, id.next) a.renderPagination(hb, rd.Blog, id.hasPrev, id.hasNext, id.prev+id.paramUrlQuery, id.next+id.paramUrlQuery)
// Author // Author
a.renderAuthor(hb) a.renderAuthor(hb)
hb.WriteElementClose("main") hb.WriteElementClose("main")