Files
sm-sitemap-plugin/classes/files.go
Jakub Zych 2a05516036 feat: sitemap management API and MCP tools
- GET and PUT /_sitemap/api/v1/settings and POST /_sitemap/api/v1/rebuild
  resolve the administrator from a backend token, a personal API token or
  the MCP dispatch and require the sitemap permission (401/403 otherwise);
  bucket sitemap-api
- PUT changes the fields sent with the settings screen's checks (422
  validation_failed, nothing stored), merges the HTML sitemap addresses,
  writes content types, generates the IndexNow key and queues a rebuild
- GET answers the settings, the content types, the index files with their
  counts, the left-out counts, the IndexNow state and the News source
  values
- MCP tools sitemap_get_settings, sitemap_update_settings and
  sitemap_rebuild bound to those routes, listed with the permission
- the content type settings write and the file listing move to classes,
  shared by the form, S1, the index and the API; S1 and the index now list
  the image and News files
2026-11-04 12:38:35 +01:00

83 lines
2.8 KiB
Go

package classes
import (
"time"
"git.golem15.com/golem15/sm-sitemap-plugin/models"
"git.golem15.com/golem15/summercms/modules/backpack"
"gorm.io/gorm"
)
// SitemapFile is one file of the sitemap index: its name, the number of
// addresses (pages for an image file, articles for the News file) and the
// newest lastmod of its addresses.
type SitemapFile struct {
Name string `json:"name"`
Count int `json:"count"`
LastMod *time.Time `json:"lastmod"`
}
// SitemapFiles lists the files the index links, in its order: the
// per-group files, the image files while images_enabled is on, then
// sitemap-news.xml while news_enabled is on and it lists an article.
func SitemapFiles(db *gorm.DB, app *backpack.App, s models.Settings, now time.Time) ([]SitemapFile, error) {
type fileRow struct {
Grp string `gorm:"column:grp"`
FileNo int `gorm:"column:file_no"`
N int `gorm:"column:n"`
LastMod *time.Time `gorm:"column:lastmod"`
}
var rows []fileRow
if err := db.Raw(`SELECT grp, file_no, COUNT(*) AS n, MAX(lastmod) AS lastmod FROM golem15_sitemap_urls
WHERE excluded_reason = '' AND file_no IS NOT NULL
GROUP BY grp, file_no ORDER BY grp, file_no`).Scan(&rows).Error; err != nil {
return nil, err
}
if s.ImagesEnabled {
var images []fileRow
if err := db.Raw(`SELECT image_file_no AS file_no, COUNT(*) AS n, MAX(lastmod) AS lastmod FROM golem15_sitemap_urls
WHERE excluded_reason = '' AND image_file_no IS NOT NULL AND jsonb_array_length(images) > 0
GROUP BY image_file_no ORDER BY image_file_no`).Scan(&images).Error; err != nil {
return nil, err
}
for i := range images {
images[i].Grp = ImagesGroup
}
rows = append(rows, images...)
}
out := make([]SitemapFile, 0, len(rows)+1)
for _, r := range rows {
out = append(out, SitemapFile{Name: FileName(r.Grp, r.FileNo), Count: r.N, LastMod: r.LastMod})
}
if s.NewsEnabled {
news, err := NewsEntries(db, app, s, now)
if err != nil {
return nil, err
}
if len(news) > 0 {
newest := news[0].Published
out = append(out, SitemapFile{Name: NewsFile, Count: len(news), LastMod: &newest})
}
}
return out, nil
}
// LeftOut counts the addresses left out of the sitemaps by reason (a
// Filter's or the type switch's; deleted and unpublished rows are holes,
// not left out).
func LeftOut(db *gorm.DB) (map[string]int, error) {
var rows []struct {
Reason string `gorm:"column:excluded_reason"`
N int `gorm:"column:n"`
}
if err := db.Raw(`SELECT excluded_reason, COUNT(*) AS n FROM golem15_sitemap_urls
WHERE excluded_reason NOT IN ('', ?, ?) GROUP BY excluded_reason`, models.ReasonDeleted, models.ReasonUnpublished).Scan(&rows).Error; err != nil {
return nil, err
}
out := make(map[string]int, len(rows))
for _, r := range rows {
out[r.Reason] = r.N
}
return out, nil
}