feat: HTML sitemap groups API
- GET /_sitemap/api/v1/html?locale= answers the overview: the listed
addresses of the XML sitemaps by group, in the order pages, offers,
services, categories (as a tree), posts (newest first), tags, authors,
other content types, each with its title in the locale, total, the
first html_page_size links and more
- GET /_sitemap/api/v1/html/{group}?locale=&page= answers one page of a
group; past the last page, an unknown group or the HTML sitemap off
answer 404; every answer is noindex; per-IP bucket sitemap-html
- the HTML sitemap tab: html_enabled, the address per locale (html_paths)
and html_page_size 50 to 500, with errors.html_path and errors.page_size
- contract.Listing.Settings answers HTMLEnabled, HTMLPaths, HTMLPageSize
and KeyFile
This commit is contained in:
@@ -20,12 +20,19 @@ import (
|
||||
"gorm.io/gorm/clause"
|
||||
)
|
||||
|
||||
// The settings form's tab keys: the Content types tab follows Files.
|
||||
// The settings form's tab keys: the Content types tab follows Files; the
|
||||
// HTML sitemap tab's address field is added after html_enabled.
|
||||
const (
|
||||
TabFiles = "golem15.sitemap::lang.settings.tab_files"
|
||||
TabTypes = "golem15.sitemap::lang.settings.tab_types"
|
||||
TabHTML = "golem15.sitemap::lang.settings.tab_html"
|
||||
)
|
||||
|
||||
// htmlPathField is the HTML sitemap address field: translatable, stored
|
||||
// as a locale map in html_paths by the plugin's form extension (the
|
||||
// settings row has no translation writer of its own).
|
||||
const htmlPathField = "html_path"
|
||||
|
||||
// The phrase keys of the Content types refusals (UI-SPEC C26).
|
||||
const (
|
||||
errSlug = "golem15.sitemap::lang.errors.slug"
|
||||
@@ -40,17 +47,13 @@ func typeFieldKey(key string) string {
|
||||
return typeFieldUnsafe.ReplaceAllString(strings.ToLower(key), "_")
|
||||
}
|
||||
|
||||
// AdminActionExtensions adds the Content types tab to the plugin's own
|
||||
// settings singleton: one block per signpost content type, in registration
|
||||
// order. Nothing is added when no content type is registered.
|
||||
// AdminActionExtensions extends the plugin's own settings singleton: the
|
||||
// Content types tab (one block per signpost content type, in registration
|
||||
// order; none without content types) and the HTML sitemap address.
|
||||
func (p *Plugin) AdminActionExtensions() []pact.AdminActionExtension {
|
||||
reg := p.registry()
|
||||
if reg == nil {
|
||||
return nil
|
||||
}
|
||||
types := reg.ContentTypes()
|
||||
if len(types) == 0 {
|
||||
return nil
|
||||
var types []pact.MenuItemType
|
||||
if reg := p.registry(); reg != nil {
|
||||
types = reg.ContentTypes()
|
||||
}
|
||||
form := p.typeSettingsTab(types)
|
||||
if form == nil {
|
||||
@@ -67,9 +70,10 @@ func (p *Plugin) registry() *signpost.Registry {
|
||||
return reg
|
||||
}
|
||||
|
||||
// typeSettingsDocument is the fields document of the Content types tab
|
||||
// (UI-SPEC C26): per type, the include switch (labelled with the type's
|
||||
// label), the file name and the HTML sitemap heading.
|
||||
// typeSettingsDocument is the fields document of the extension (UI-SPEC
|
||||
// C26): per type, the include switch (labelled with the type's label), the
|
||||
// file name and the HTML sitemap heading on the Content types tab, then
|
||||
// the HTML sitemap address after html_enabled.
|
||||
func typeSettingsDocument(types []pact.MenuItemType) ([]byte, error) {
|
||||
fields := yaml.MapSlice{}
|
||||
for _, typ := range types {
|
||||
@@ -101,15 +105,24 @@ func typeSettingsDocument(types []pact.MenuItemType) ([]byte, error) {
|
||||
}},
|
||||
)
|
||||
}
|
||||
return yaml.Marshal(yaml.MapSlice{
|
||||
{Key: "tabs", Value: []yaml.MapSlice{{{Key: "name", Value: TabTypes}, {Key: "after", Value: TabFiles}}}},
|
||||
{Key: "fields", Value: fields},
|
||||
})
|
||||
fields = append(fields, yaml.MapItem{Key: htmlPathField, Value: yaml.MapSlice{
|
||||
{Key: "label", Value: "golem15.sitemap::lang.settings.html_path"},
|
||||
{Key: "comment", Value: "golem15.sitemap::lang.settings.html_path_comment"},
|
||||
{Key: "type", Value: "mltext"},
|
||||
{Key: "span", Value: "left"},
|
||||
{Key: "tab", Value: TabHTML},
|
||||
{Key: "after", Value: "html_enabled"},
|
||||
}})
|
||||
doc := yaml.MapSlice{}
|
||||
if len(types) > 0 {
|
||||
doc = append(doc, yaml.MapItem{Key: "tabs", Value: []yaml.MapSlice{{{Key: "name", Value: TabTypes}, {Key: "after", Value: TabFiles}}}})
|
||||
}
|
||||
return yaml.Marshal(append(doc, yaml.MapItem{Key: "fields", Value: fields}))
|
||||
}
|
||||
|
||||
// typeSettingsTab is the plugin's form extension on its own settings
|
||||
// controller: the Content types tab, loaded from and saved to
|
||||
// golem15_sitemap_types.
|
||||
// golem15_sitemap_types, and the HTML sitemap address (html_paths).
|
||||
func (p *Plugin) typeSettingsTab(types []pact.MenuItemType) *pact.AdminFormExtension {
|
||||
doc, err := typeSettingsDocument(types)
|
||||
if err != nil {
|
||||
@@ -138,10 +151,18 @@ func (p *Plugin) typeSettingsTab(types []pact.MenuItemType) *pact.AdminFormExten
|
||||
}
|
||||
out[base+"title"] = titles
|
||||
}
|
||||
settings, err := classes.LoadSettings(in.DB.WithContext(ctx))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out[htmlPathField] = settings.EffectiveHTMLPaths()
|
||||
return out, nil
|
||||
},
|
||||
Save: func(ctx context.Context, in pact.AdminFormSave) (pact.AdminFormSaveResult, error) {
|
||||
return pact.AdminFormSaveResult{}, saveTypeSettings(ctx, in, types)
|
||||
if err := saveTypeSettings(ctx, in, types); err != nil {
|
||||
return pact.AdminFormSaveResult{}, err
|
||||
}
|
||||
return pact.AdminFormSaveResult{}, saveHTMLPaths(in)
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -212,6 +233,32 @@ func saveTypeSettings(ctx context.Context, in pact.AdminFormSave, types []pact.M
|
||||
return nil
|
||||
}
|
||||
|
||||
// saveHTMLPaths stores a submitted HTML sitemap address: the locales sent
|
||||
// replace theirs in html_paths (a blank one removes it), the others keep
|
||||
// theirs. An address that models.ValidHTMLPath refuses is refused with
|
||||
// errors.html_path.
|
||||
func saveHTMLPaths(in pact.AdminFormSave) error {
|
||||
raw, sent := in.Values[htmlPathField]
|
||||
if !sent {
|
||||
return nil
|
||||
}
|
||||
paths := map[string]string{}
|
||||
switch v := raw.(type) {
|
||||
case map[string]string:
|
||||
paths = v
|
||||
case map[string]any:
|
||||
for k, s := range v {
|
||||
text, _ := s.(string)
|
||||
paths[k] = text
|
||||
}
|
||||
}
|
||||
merged, problem := classes.MergeHTMLPaths(in.DB, paths)
|
||||
if problem {
|
||||
return pact.AdminFormErrors{htmlPathField: {models.HTMLPathProblem}}
|
||||
}
|
||||
return classes.StoreHTMLPaths(in.DB, merged)
|
||||
}
|
||||
|
||||
// titlesOf reads a submitted translatable value (a map of locale to text);
|
||||
// blank texts are dropped.
|
||||
func titlesOf(v any) map[string]string {
|
||||
|
||||
Reference in New Issue
Block a user