- golem15.sitemap plugin: golem15_sitemap_definitions migration, Definition model (Fillable theme and data) with tolerant PHP-shaped item decoding and PHP toArray item encoding - Url::to and buildUrl port, libxml-format serializer, generator for url and registry items - GET /sitemap.xml serves the definition with theme default (404 without one, empty body without items) - tests/php/gen-goldens.php and the tracer golden; TestSitemapGolden and the Postgres TestSitemapTracer
148 lines
4.4 KiB
Go
148 lines
4.4 KiB
Go
// Package classes generates the sitemap: it walks the definition items,
|
|
// resolves registry items through the framework's menu item type registry
|
|
// and writes the PHP-identical XML.
|
|
package classes
|
|
|
|
import (
|
|
"context"
|
|
"time"
|
|
|
|
"git.golem15.com/golem15/sm-sitemap-plugin/models"
|
|
"git.golem15.com/golem15/summercms/modules/pact"
|
|
)
|
|
|
|
// MaxURLs is the sitemap protocol's URL limit (PHP Definition::MAX_URLS):
|
|
// the 50,000th URL is written, the 50,001st is not.
|
|
const MaxURLs = 50000
|
|
|
|
// lastmodLayout is PHP date('c') in UTC: "+00:00", never "Z".
|
|
const lastmodLayout = "2006-01-02T15:04:05-07:00"
|
|
|
|
// Resolver resolves a registry item; *signpost.Registry implements it.
|
|
type Resolver interface {
|
|
Resolve(ctx context.Context, key, reference, itemPattern string, nesting bool, unprefixedLocale string) (*pact.ResolvedMenuItem, error)
|
|
}
|
|
|
|
// Generator writes the sitemap of a definition. Root is the backend root URL
|
|
// (scheme and host without a trailing slash) that Url::to builds absolute
|
|
// URLs from. Resolver resolves registry items; nil skips them.
|
|
// UnprefixedLocale is handed to every Resolve as the locale without a URL
|
|
// prefix ("" leaves it to the type).
|
|
type Generator struct {
|
|
Root string
|
|
Resolver Resolver
|
|
UnprefixedLocale string
|
|
|
|
entries []URLEntry
|
|
count int
|
|
}
|
|
|
|
// itemInfo is PHP's $itemInfo: the URL (set marks it present), the record's
|
|
// modification time and the hreflang alternates.
|
|
type itemInfo struct {
|
|
url string
|
|
set bool
|
|
mtime *time.Time
|
|
alternates []Alternate
|
|
}
|
|
|
|
// Generate returns the sitemap XML of items, or "" when there are none (PHP
|
|
// generateSitemap). A url item is added with its URL; a registry item is
|
|
// resolved with its URL pattern and nesting, its URL added when it has one
|
|
// and its sub-items added recursively. A type the registry does not know,
|
|
// or a Resolve that finds nothing, adds nothing.
|
|
func (g *Generator) Generate(ctx context.Context, items []models.DefinitionItem) (string, error) {
|
|
if len(items) == 0 {
|
|
return "", nil
|
|
}
|
|
g.entries, g.count = nil, 0
|
|
for i := range items {
|
|
item := &items[i]
|
|
if item.Type == models.ItemTypeURL {
|
|
g.addItem(item, itemInfo{})
|
|
continue
|
|
}
|
|
resolved, err := g.resolve(ctx, item)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if resolved == nil {
|
|
continue
|
|
}
|
|
if resolved.URL != "" {
|
|
g.addItem(item, infoOf(*resolved))
|
|
}
|
|
g.addChildren(item, resolved.Items)
|
|
}
|
|
return Serialize(g.Stylesheet(), g.entries), nil
|
|
}
|
|
|
|
// Stylesheet is the stylesheet href PHP built:
|
|
// Url::buildUrl(Url::current(), ['path' => 'sitemap.xsl']).
|
|
func (g *Generator) Stylesheet() string {
|
|
return replacePath(URLTo(g.Root, "/sitemap.xml"), "sitemap.xsl")
|
|
}
|
|
|
|
func (g *Generator) resolve(ctx context.Context, item *models.DefinitionItem) (*pact.ResolvedMenuItem, error) {
|
|
if g.Resolver == nil || item.Type == "" {
|
|
return nil, nil
|
|
}
|
|
if reg, ok := g.Resolver.(interface {
|
|
Type(string) (pact.MenuItemType, bool)
|
|
}); ok {
|
|
if _, known := reg.Type(item.Type); !known {
|
|
return nil, nil
|
|
}
|
|
}
|
|
return g.Resolver.Resolve(ctx, item.Type, models.Text(item.Reference), models.Text(item.URLPattern), item.Nesting, g.UnprefixedLocale)
|
|
}
|
|
|
|
// addChildren adds every child with a URL, then its own children.
|
|
func (g *Generator) addChildren(item *models.DefinitionItem, children []pact.ResolvedMenuItem) {
|
|
for _, child := range children {
|
|
if child.URL != "" {
|
|
g.addItem(item, infoOf(child))
|
|
}
|
|
g.addChildren(item, child.Items)
|
|
}
|
|
}
|
|
|
|
func infoOf(r pact.ResolvedMenuItem) itemInfo {
|
|
info := itemInfo{url: r.URL, set: true, mtime: r.MTime}
|
|
for _, a := range r.Alternates {
|
|
info.alternates = append(info.alternates, Alternate{Hreflang: a.Locale, Href: a.URL})
|
|
}
|
|
return info
|
|
}
|
|
|
|
// addItem is PHP Definition::addItemToSet.
|
|
func (g *Generator) addItem(item *models.DefinitionItem, info itemInfo) {
|
|
if g.count >= MaxURLs {
|
|
return
|
|
}
|
|
if !info.set {
|
|
info.url = URLTo(g.Root, models.Text(item.URL))
|
|
}
|
|
lastmod := ""
|
|
if info.mtime != nil && info.mtime.Unix() != 0 {
|
|
lastmod = info.mtime.UTC().Format(lastmodLayout)
|
|
}
|
|
if info.url == "" || info.url == "0" {
|
|
return
|
|
}
|
|
loc := URLTo(g.Root, info.url)
|
|
g.count++
|
|
entry := URLEntry{
|
|
Loc: loc,
|
|
Lastmod: lastmod,
|
|
Changefreq: models.Text(item.Changefreq),
|
|
Priority: models.Text(item.Priority),
|
|
}
|
|
if len(info.alternates) > 1 {
|
|
for _, a := range info.alternates {
|
|
entry.Alternates = append(entry.Alternates, Alternate{Hreflang: a.Hreflang, Href: URLTo(g.Root, a.Href)})
|
|
}
|
|
}
|
|
g.entries = append(g.entries, entry)
|
|
}
|