Files
sm-sitemap-plugin/models/url.go
Jakub Zych 5bf56fde57 feat: image sitemap files (tracer)
- migration 202610280004: image_file_no, kind, published_at and parent_refs
  on the slot table; the image, News, IndexNow and HTML sitemap settings
- the rebuild stores each page's featured and body images (absolute on the
  site, deduplicated, at most 1000) and numbers the rows with images into
  sitemap-images-<n>.xml with the per-type files' stable rules
- the index lists the image files while images_enabled is on; the files
  route serves them with image:image and image:loc only
2026-11-04 12:19:05 +01:00

93 lines
3.7 KiB
Go

package models
import (
"strings"
"time"
"unicode"
"unicode/utf8"
"git.golem15.com/golem15/summercms/modules/lagoon"
)
// ExtraTypeKey is the type_key of the settings' additional addresses in
// golem15_sitemap_urls; their record_id is the hex SHA-256 of the address.
const ExtraTypeKey = "extra"
// ExtraGroup is the file group of the additional addresses.
const ExtraGroup = "pages"
// The excluded reasons the sitemap sets itself. A Filter's reasons
// (contract.ReasonNoindex and the others) are stored as it gives them.
const (
// ReasonDeleted is an address whose record or locale is gone.
ReasonDeleted = "deleted"
// ReasonUnpublished is an address whose record is not published.
ReasonUnpublished = "unpublished"
)
// Alternate is one hreflang alternate of an address: Hreflang the locale
// code or x-default, Href the alternate's path or absolute URL.
type Alternate struct {
Hreflang string `json:"hreflang"`
Href string `json:"href"`
}
// Ref points at a record: Type is its content type key and ID the record's
// ID within it (pact.ContentRef as stored in parent_refs).
type Ref struct {
Type string `json:"type"`
ID string `json:"id"`
}
// URL is one row of the slot table golem15_sitemap_urls (D-30): an address
// of a content type's record in one locale, or an additional address. Grp
// and FileNo place it in the file sitemap-<grp>-<file_no>.xml; FileNo is
// nil until the address is numbered. ExcludedReason is "" for a listed
// address; a row left out keeps its FileNo, so its file keeps a hole.
// Images are the page's image addresses (D-31), ImageFileNo the number of
// its sitemap-images file (nil while it has none). Kind is the content
// type's kind (pact.ContentKindArticle and the others), PublishedAt the
// record's first publication and ParentRefs its parents, for the News
// sitemap and the HTML sitemap.
type URL struct {
ID uint64 `gorm:"column:id;primaryKey"`
Grp string `gorm:"column:grp"`
TypeKey string `gorm:"column:type_key"`
RecordID string `gorm:"column:record_id"`
Locale string `gorm:"column:locale"`
URL string `gorm:"column:url"`
Title string `gorm:"column:title"`
FileNo *int `gorm:"column:file_no"`
FirstSeen time.Time `gorm:"column:first_seen"`
LastMod *time.Time `gorm:"column:lastmod"`
Alternates lagoon.Jsonable[[]Alternate] `gorm:"column:alternates"`
Images lagoon.Jsonable[[]string] `gorm:"column:images"`
ImageFileNo *int `gorm:"column:image_file_no"`
Kind string `gorm:"column:kind"`
PublishedAt *time.Time `gorm:"column:published_at"`
ParentRefs lagoon.Jsonable[[]Ref] `gorm:"column:parent_refs"`
ExcludedReason string `gorm:"column:excluded_reason"`
UpdatedAt time.Time `gorm:"column:updated_at"`
}
func (URL) TableName() string { return "golem15_sitemap_urls" }
func init() {
Register(URL{})
}
// AllowedAddress reports whether u may be a stored address: a path that
// starts with a single / (no whitespace, control characters, quotes, < or
// >), or an absolute http(s) URL that AllowedFrontendURL accepts.
func AllowedAddress(u string) bool {
if u == "" || !utf8.ValidString(u) {
return false
}
if strings.HasPrefix(u, "/") && !strings.HasPrefix(u, "//") {
return !strings.ContainsFunc(u, func(r rune) bool {
return unicode.IsControl(r) || unicode.IsSpace(r) || r == '"' || r == '<' || r == '>' || r == '\\'
})
}
return AllowedFrontendURL(u)
}