- GET /_seo/api/v1/site (public, noindex): site_url, default_locale, locales, unprefixed_locale, links.new_tab, html_sitemap (from the sitemap plugin's Listing), routes of every content type, root_files (verification files and the IndexNow key file) and llms - GET /llms.txt: with llms_enabled (robots.txt screen, AI bots tab, off by default) and in production, the indexable pages from the sitemaps per group under the site name and llms_intro (else the default description); otherwise 404; seo_update_robots takes both fields - robots.txt names the sitemap plugin's index unless golem15.seo.robots_sitemaps is set; the config default is now a comment
140 lines
4.1 KiB
Go
140 lines
4.1 KiB
Go
package classes
|
|
|
|
import (
|
|
"context"
|
|
"strings"
|
|
|
|
"git.golem15.com/golem15/sm-seo-plugin/models"
|
|
"git.golem15.com/golem15/sm-sitemap-plugin/contract"
|
|
)
|
|
|
|
// SiteAnswer is the answer of GET /_seo/api/v1/site (P33-17): the facts
|
|
// about the whole site the frontend layer needs, all public. SiteURL,
|
|
// DefaultLocale, Locales and UnprefixedLocale are the resolver's; Links
|
|
// the settings' new-tab switch; HTMLSitemap the sitemap plugin's HTML
|
|
// sitemap settings; Routes each content type's layered URL pattern for
|
|
// the layer's drift check; RootFiles the file names the layer serves at
|
|
// the site root (verification files, the IndexNow key file); LLMS whether
|
|
// /llms.txt is served.
|
|
type SiteAnswer struct {
|
|
SiteURL string `json:"site_url"`
|
|
DefaultLocale string `json:"default_locale"`
|
|
Locales []string `json:"locales"`
|
|
UnprefixedLocale string `json:"unprefixed_locale"`
|
|
Links SiteLinks `json:"links"`
|
|
HTMLSitemap SiteHTMLSitemap `json:"html_sitemap"`
|
|
Routes []SiteRoute `json:"routes"`
|
|
RootFiles []string `json:"root_files"`
|
|
LLMS bool `json:"llms"`
|
|
}
|
|
|
|
// SiteLinks is the site answer's links block.
|
|
type SiteLinks struct {
|
|
NewTab bool `json:"new_tab"`
|
|
}
|
|
|
|
// SiteHTMLSitemap is the site answer's HTML sitemap block: whether the
|
|
// sitemap plugin serves it, its path per locale and its page size.
|
|
type SiteHTMLSitemap struct {
|
|
Enabled bool `json:"enabled"`
|
|
Paths map[string]string `json:"paths"`
|
|
PageSize int `json:"page_size"`
|
|
}
|
|
|
|
// SiteRoute is one content type's URL pattern.
|
|
type SiteRoute struct {
|
|
Type string `json:"type"`
|
|
Pattern string `json:"pattern"`
|
|
}
|
|
|
|
// listing returns the sitemap plugin's published contract.Listing, nil
|
|
// when none is published.
|
|
func (s *Service) listing() contract.Listing {
|
|
if s == nil || s.app == nil {
|
|
return nil
|
|
}
|
|
l, ok := s.app.Lookup[contract.Listing]()
|
|
if !ok {
|
|
return nil
|
|
}
|
|
return l
|
|
}
|
|
|
|
// Site builds the site answer. It names only public facts: no token, no
|
|
// unpublished data (T-33-19).
|
|
func (s *Service) Site(ctx context.Context) (SiteAnswer, error) {
|
|
out := SiteAnswer{
|
|
SiteURL: s.SiteURL(),
|
|
UnprefixedLocale: s.UnprefixedLocale(),
|
|
HTMLSitemap: SiteHTMLSitemap{Paths: map[string]string{}},
|
|
Routes: []SiteRoute{},
|
|
RootFiles: []string{},
|
|
}
|
|
db, err := s.DB(ctx)
|
|
if err != nil {
|
|
return out, err
|
|
}
|
|
settings, err := LoadSettings(db)
|
|
if err != nil {
|
|
return out, err
|
|
}
|
|
robots, err := LoadRobots(db)
|
|
if err != nil {
|
|
return out, err
|
|
}
|
|
out.DefaultLocale = s.DefaultLocale(ctx, db)
|
|
out.Locales = s.Locales(ctx, db)
|
|
out.Links.NewTab = settings.LinksNewTab
|
|
out.LLMS = robots.LlmsEnabled
|
|
if reg := s.Registry(); reg != nil {
|
|
for _, typ := range reg.ContentTypes() {
|
|
if typ.Key == PageTypeKey {
|
|
continue
|
|
}
|
|
out.Routes = append(out.Routes, SiteRoute{Type: typ.Key, Pattern: reg.Pattern(typ.Key, "")})
|
|
}
|
|
}
|
|
seen := map[string]bool{}
|
|
addFile := func(name string) {
|
|
name = strings.TrimSpace(name)
|
|
if name != "" && verificationName.MatchString(name) && !seen[name] {
|
|
seen[name] = true
|
|
out.RootFiles = append(out.RootFiles, name)
|
|
}
|
|
}
|
|
for _, f := range settings.Files() {
|
|
addFile(f.Filename)
|
|
}
|
|
if l := s.listing(); l != nil {
|
|
st, err := l.Settings(ctx)
|
|
if err != nil {
|
|
return out, err
|
|
}
|
|
out.HTMLSitemap.Enabled = st.HTMLEnabled
|
|
for locale, path := range st.HTMLPaths {
|
|
if strings.TrimSpace(path) != "" {
|
|
out.HTMLSitemap.Paths[locale] = path
|
|
}
|
|
}
|
|
out.HTMLSitemap.PageSize = st.HTMLPageSize
|
|
addFile(st.KeyFile)
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
// SiteName is the site's name in locale: the settings' site name, else
|
|
// the application name.
|
|
func (s *Service) SiteName(ctx context.Context, settings models.Settings, locale string) string {
|
|
db, err := s.DB(ctx)
|
|
name := settings.SiteName
|
|
if err == nil {
|
|
name = s.translatedSetting(ctx, db, &settings, "site_name", settings.SiteName, locale)
|
|
}
|
|
if strings.TrimSpace(name) == "" {
|
|
if cfg := s.Config(); cfg != nil {
|
|
name = cfg.String("app.name")
|
|
}
|
|
}
|
|
return strings.TrimSpace(name)
|
|
}
|