// 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) }