feat: Google News sitemap

- sitemap-news.xml lists the articles (content kind article) first
  published in the last two days, newest first, at most 1000, with the
  publication name (news_publication, else app.name), the language of the
  locale, the publication date and the title
- news_sources narrows it to type:<key> and category:<key>:<id> values;
  the Images and News tab lists the values to use in a partial
- the index lists the file while news_enabled is on; otherwise it is 404
This commit is contained in:
Jakub Zych
2026-11-04 12:22:40 +01:00
parent 5bf56fde57
commit a10ce2d9de
9 changed files with 604 additions and 5 deletions

View File

@@ -12,6 +12,7 @@ import (
"git.golem15.com/golem15/summercms/modules/cabana"
"git.golem15.com/golem15/summercms/modules/pact"
"git.golem15.com/golem15/summercms/modules/phrasebook"
"git.golem15.com/golem15/summercms/modules/signpost"
"gorm.io/gorm"
)
@@ -129,9 +130,76 @@ type reasonRow struct {
// and the index note; files.empty before the first build and
// files.building while a rebuild asked for from the admin runs.
func (c settingsAdminController) PartialData(ctx context.Context, name string, _ any) (any, error) {
if name != "sitemap_files" {
return nil, nil
switch name {
case "sitemap_files":
return c.filesData(ctx)
case "news_sources":
return c.newsSourcesData(ctx)
}
return nil, nil
}
// sourceRow is one row of the news_sources partial: the value to enter and
// what it lists.
type sourceRow struct {
Value string
Label string
}
// newsSourcesView is the view model of the news_sources partial.
type newsSourcesView struct {
Rows []sourceRow
}
// newsSourcesData lists the values the news_sources taglist takes
// (classes.NewsSources): each article type, then the categories its
// listed articles name, labelled "<type>: <title>".
func (c settingsAdminController) newsSourcesData(ctx context.Context) (any, error) {
app := appOf(c.app)
db, err := database(ctx, c.app)
if err != nil {
return nil, err
}
settings, err := classes.LoadSettings(db)
if err != nil {
return nil, err
}
var reg *signpost.Registry
if app != nil {
reg, _ = app.Lookup[*signpost.Registry]()
}
sources, err := classes.NewsSources(db, reg, models.Text(settings.DefaultLocale))
if err != nil {
return nil, err
}
tr := translator(c.app)
label := func(key string) string {
if reg != nil {
if typ, ok := reg.ContentType(key); ok && typ.Label != "" {
return Phrase(ctx, tr, typ.Label, nil)
}
}
return key
}
view := newsSourcesView{}
for _, src := range sources {
row := sourceRow{Value: src.Value}
if src.ID == "" {
row.Label = Phrase(ctx, tr, "golem15.sitemap::lang.news.source_type", map[string]string{"type": label(src.Type)})
} else {
title := src.Title
if title == "" {
title = src.ID
}
row.Label = label(src.Type) + ": " + title
}
view.Rows = append(view.Rows, row)
}
return view, nil
}
// filesData is the view of the S1 files partial.
func (c settingsAdminController) filesData(ctx context.Context) (any, error) {
app := appOf(c.app)
db, err := database(ctx, c.app)
if err != nil {