feat: sitemap settings singleton with content types and additional addresses

- one settings tile opening the golem15.sitemap.settings singleton
- Files tab (site address, file size, S1 files partial), Additional
  addresses repeater, Content types tab built from signpost at boot
- golem15_sitemap_types; a type switched off leaves the files (reason type)
- a save validates (errors.*) and dispatches a rebuild after commit
This commit is contained in:
Jakub Zych
2026-11-04 11:06:29 +01:00
parent 3359f1a196
commit 3ba70bb69f
20 changed files with 1304 additions and 337 deletions

View File

@@ -9,6 +9,7 @@ import (
"strings"
"time"
"git.golem15.com/golem15/sm-sitemap-plugin/contract"
"git.golem15.com/golem15/sm-sitemap-plugin/models"
"git.golem15.com/golem15/summercms/modules/backpack"
"git.golem15.com/golem15/summercms/modules/lagoon"
@@ -124,8 +125,12 @@ func Rebuild(ctx context.Context, app *backpack.App) (RebuildResult, error) {
if err != nil {
return res, err
}
types, err := LoadTypeSettings(db)
if err != nil {
return res, err
}
reg, _ := app.Lookup[*signpost.Registry]()
found, err := collect(ctx, reg, settings)
found, err := collect(ctx, reg, settings, types)
if err != nil {
return res, err
}
@@ -145,8 +150,10 @@ func Rebuild(ctx context.Context, app *backpack.App) (RebuildResult, error) {
}
// collect walks the content types page by page and adds the additional
// addresses.
func collect(ctx context.Context, reg *signpost.Registry, settings models.Settings) (collected, error) {
// addresses. The records of a type switched off in the settings are kept
// in the table with the reason contract.ReasonType, so the settings screen
// counts them.
func collect(ctx context.Context, reg *signpost.Registry, settings models.Settings, types map[string]models.TypeSetting) (collected, error) {
out := collected{unpublished: map[slotKey]bool{}}
seen := map[slotKey]bool{}
add := func(w wanted) {
@@ -159,7 +166,11 @@ func collect(ctx context.Context, reg *signpost.Registry, settings models.Settin
unprefixed := models.Text(settings.DefaultLocale)
if reg != nil {
for _, typ := range reg.ContentTypes() {
slug := DefaultSlug(typ.Key)
cfg := EffectiveType(typ, types)
reason := ""
if !cfg.Include {
reason = contract.ReasonType
}
after := ""
for {
page, err := reg.Records(ctx, typ.Key, after, recordsPage, unprefixed)
@@ -177,10 +188,11 @@ func collect(ctx context.Context, reg *signpost.Registry, settings models.Settin
}
add(wanted{
key: slotKey{typ.Key, rec.ID, l.Locale},
grp: slug,
grp: cfg.Slug,
url: l.URL,
title: l.Title,
firstMod: rec.LastMod,
reason: reason,
})
}
}
@@ -196,7 +208,7 @@ func collect(ctx context.Context, reg *signpost.Registry, settings models.Settin
if !models.AllowedAddress(u) {
continue
}
mod := ParseDate(e.LastMod)
mod := models.ParseDate(e.LastMod)
add(wanted{
key: slotKey{models.ExtraTypeKey, ExtraRecordID(u), ""},
grp: models.ExtraGroup,
@@ -209,23 +221,6 @@ func collect(ctx context.Context, reg *signpost.Registry, settings models.Settin
return out, nil
}
// ParseDate reads a date as the admin's date picker or an import sends it
// (RFC 3339, "2006-01-02 15:04:05" or "2006-01-02", UTC); nil when empty or
// unreadable.
func ParseDate(s string) *time.Time {
s = strings.TrimSpace(s)
if s == "" {
return nil
}
for _, layout := range []string{time.RFC3339Nano, "2006-01-02 15:04:05", "2006-01-02T15:04:05", "2006-01-02"} {
if t, err := time.Parse(layout, s); err == nil {
t = t.UTC()
return &t
}
}
return nil
}
// apply writes the found addresses into the slot table and numbers the
// files, inside the rebuild's transaction.
func apply(tx *gorm.DB, found collected, settings models.Settings, site string) (RebuildResult, error) {