fix: HTML sitemap headings follow each locale until an editor sets one
Settings > Sitemaps no longer prefills a content type's heading with its label in the administrator's language, so a save that did not touch the heading stores none and each locale's HTML sitemap keeps the label translated into it. The field's comment says so.
This commit is contained in:
@@ -211,7 +211,7 @@ The plugin also publishes `classes.Listing` as `contract.Rebuilder`. `RequestReb
|
||||
The Settings page has one Sitemaps tile (icon `network`, category CMS, permission `golem15.sitemap.access_definitions`). It opens the singleton controller `golem15.sitemap.settings`:
|
||||
|
||||
- **Files:** Site address, Addresses per file and the files table: the index and every file with its address count and newest lastmod, the addresses left out by reason, and a note naming the public index address. Before the first build it says there are no files yet; after a save and until the rebuild ran, that it is rebuilding.
|
||||
- **Content types:** per signpost content type, in registration order, a switch labelled with the type's label, the file name (`[a-z0-9-]`, unique, `images` and `news` reserved) and the HTML sitemap heading per locale. Stored in `golem15_sitemap_types`.
|
||||
- **Content types:** per signpost content type, in registration order, a switch labelled with the type's label, the file name (`[a-z0-9-]`, unique, `images` and `news` reserved) and the HTML sitemap heading per locale. The heading loads empty until an editor sets one, so a save that does not touch it stores none and each language's HTML sitemap shows the type's label translated into that language. Stored in `golem15_sitemap_types`.
|
||||
- **Additional addresses:** a repeater of paths (or absolute URLs) with an optional lastmod as `YYYY-MM-DD`. They go into the `pages` group.
|
||||
- **Images and News:** the image sitemap switch, the News sitemap switch, and while it is on the publication name, the News sources (a tag list of `type:` and `category:` values) and the table of the values the site has.
|
||||
- **IndexNow:** the switch, the status partial and, while it is on, "Generate a new key on save".
|
||||
|
||||
@@ -80,6 +80,7 @@ func typeSettingsDocument(types []pact.MenuItemType) ([]byte, error) {
|
||||
}},
|
||||
yaml.MapItem{Key: base + "title", Value: yaml.MapSlice{
|
||||
{Key: "label", Value: "golem15.sitemap::lang.settings.type_title"},
|
||||
{Key: "comment", Value: "golem15.sitemap::lang.settings.type_title_comment"},
|
||||
{Key: "type", Value: "mltext"},
|
||||
{Key: "span", Value: "full"},
|
||||
{Key: "tab", Value: TabTypes},
|
||||
@@ -118,17 +119,18 @@ func (p *Plugin) typeSettingsTab(types []pact.MenuItemType) *pact.AdminFormExten
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
def := p.defaultLocale(ctx, in.DB)
|
||||
tr := p.translator()
|
||||
out := map[string]any{}
|
||||
for _, typ := range types {
|
||||
cfg := classes.EffectiveType(typ, rows)
|
||||
base := classes.TypeFieldBase(typ.Key)
|
||||
out[base+"include"] = cfg.Include
|
||||
out[base+"slug"] = cfg.Slug
|
||||
// Only stored headings (L2 WR-05): a type without one loads
|
||||
// empty, so a save that did not touch it stores none and each
|
||||
// locale's HTML sitemap keeps the label translated into it.
|
||||
titles := cfg.Titles
|
||||
if len(titles) == 0 {
|
||||
titles = map[string]string{def: controllers.Phrase(ctx, tr, typ.Label, nil)}
|
||||
if titles == nil {
|
||||
titles = map[string]string{}
|
||||
}
|
||||
out[base+"title"] = titles
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@ settings:
|
||||
type_slug: File name
|
||||
type_slug_comment: Files are named sitemap-<name>-1.xml, sitemap-<name>-2.xml and so on.
|
||||
type_title: Heading in the HTML sitemap
|
||||
type_title_comment: "Leave empty to show the content type's name in each language's HTML sitemap."
|
||||
extra_urls: Additional addresses
|
||||
extra_urls_add: Add address
|
||||
extra_urls_comment: Addresses that no content type lists, e.g. the site's static pages. They go into the sitemap-pages files.
|
||||
|
||||
@@ -33,6 +33,7 @@ settings:
|
||||
type_slug: Nazwa pliku
|
||||
type_slug_comment: Pliki nazywają się sitemap-<nazwa>-1.xml, sitemap-<nazwa>-2.xml i tak dalej.
|
||||
type_title: Nagłówek grupy w mapie HTML
|
||||
type_title_comment: "Puste pole: mapa HTML w każdym języku pokazuje nazwę typu treści w tym języku."
|
||||
extra_urls: Dodatkowe adresy
|
||||
extra_urls_add: Dodaj adres
|
||||
extra_urls_comment: Adresy, których nie podaje żaden typ treści, np. statyczne strony witryny. Trafiają do plików sitemap-pages.
|
||||
|
||||
128
type_titles_test.go
Normal file
128
type_titles_test.go
Normal file
@@ -0,0 +1,128 @@
|
||||
package sitemap_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
"testing"
|
||||
"testing/fstest"
|
||||
"time"
|
||||
|
||||
"git.golem15.com/golem15/summercms/modules/backpack"
|
||||
"git.golem15.com/golem15/summercms/modules/pact"
|
||||
"git.golem15.com/golem15/summercms/modules/party"
|
||||
)
|
||||
|
||||
// labelsPluginID is a fake content plugin whose type label is a phrase
|
||||
// with an English and a Polish translation, as a real plugin's is.
|
||||
const labelsPluginID = "acme.labels"
|
||||
|
||||
const noteType = "acme-note"
|
||||
|
||||
type labelsPlugin struct{}
|
||||
|
||||
func (labelsPlugin) ID() string { return labelsPluginID }
|
||||
func (labelsPlugin) Requires() []string { return nil }
|
||||
func (labelsPlugin) Register(*backpack.App) error { return nil }
|
||||
func (labelsPlugin) Boot(*backpack.App) error { return nil }
|
||||
|
||||
func (labelsPlugin) LangFS() fs.FS {
|
||||
return fstest.MapFS{
|
||||
"lang/en/lang.yaml": {Data: []byte("types:\n notes: Notes\n")},
|
||||
"lang/pl/lang.yaml": {Data: []byte("types:\n notes: Notatki\n")},
|
||||
}
|
||||
}
|
||||
|
||||
func (labelsPlugin) MenuItemTypes() []pact.MenuItemType {
|
||||
return []pact.MenuItemType{{
|
||||
Key: noteType, Label: "acme.labels::lang.types.notes", DefaultPattern: "/notes/:slug", Placeholders: []string{"slug"},
|
||||
Resolve: func(context.Context, pact.MenuItemResolve) (*pact.ResolvedMenuItem, error) { return nil, nil },
|
||||
Content: &pact.ContentDescriptor{
|
||||
Model: "acme_notes",
|
||||
Records: func(_ context.Context, in pact.ContentList) (pact.ContentPage, error) {
|
||||
return contentStore.page(noteType, in), nil
|
||||
},
|
||||
Describe: func(context.Context, pact.ContentDescribe) (*pact.ContentRecord, error) { return nil, nil },
|
||||
},
|
||||
}}
|
||||
}
|
||||
|
||||
var (
|
||||
_ pact.HasMenuItemTypes = labelsPlugin{}
|
||||
_ pact.HasLang = labelsPlugin{}
|
||||
)
|
||||
|
||||
func init() {
|
||||
party.Register(labelsPlugin{})
|
||||
}
|
||||
|
||||
// TestTypeTitlesNotPinned pins L2 WR-05 (D-34): an administrator with an
|
||||
// English admin loads Settings › Sitemaps and saves it back unchanged; no
|
||||
// heading is stored for the content type, so the Polish HTML sitemap keeps
|
||||
// the Polish translation of the type's label and the English one the
|
||||
// English.
|
||||
func TestTypeTitlesNotPinned(t *testing.T) {
|
||||
contentStore.reset(t)
|
||||
e := bootPlugins(t, []string{labelsPluginID, "golem15.sitemap"})
|
||||
e.exec(t, `UPDATE golem15_sitemap_settings SET default_locale = 'pl', html_enabled = TRUE WHERE id = 1`)
|
||||
at := time.Date(2026, 10, 1, 8, 0, 0, 0, time.UTC)
|
||||
contentStore.put(noteType, fakeRecord{ID: 1, Published: true, LastMod: at, Locales: []pact.ContentLocale{
|
||||
{Locale: "pl", URL: "/notes/pierwsza", Title: "Pierwsza"},
|
||||
{Locale: "en", URL: "/en/notes/first", Title: "First"},
|
||||
}})
|
||||
token := e.admin(t, permSitemap)
|
||||
const base = "/golem15/sitemap/settings"
|
||||
rec := e.call(t, http.MethodGet, base+"/1", token, nil)
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("GET settings: %d %s", rec.Code, rec.Body.String())
|
||||
}
|
||||
var loaded struct {
|
||||
Data map[string]any `json:"data"`
|
||||
}
|
||||
if err := json.Unmarshal(rec.Body.Bytes(), &loaded); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
values := loaded.Data
|
||||
if values == nil {
|
||||
if err := json.Unmarshal(rec.Body.Bytes(), &values); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
title, ok := values["type_acme_note_title"]
|
||||
if !ok {
|
||||
t.Fatalf("the form loads no heading field: %s", rec.Body.String())
|
||||
}
|
||||
m, _ := title.(map[string]any)
|
||||
if len(m) != 0 {
|
||||
t.Errorf("the form loads a heading %v for a type without one", title)
|
||||
}
|
||||
// Without a translation writer the form sends the default locale's
|
||||
// text of the loaded value back.
|
||||
sent, _ := m["pl"].(string)
|
||||
if rec := e.call(t, http.MethodPut, base+"/1", token, map[string]any{
|
||||
"type_acme_note_include": values["type_acme_note_include"],
|
||||
"type_acme_note_slug": values["type_acme_note_slug"],
|
||||
"type_acme_note_title": sent,
|
||||
}); rec.Code != http.StatusOK {
|
||||
t.Fatalf("PUT settings: %d %s", rec.Code, rec.Body.String())
|
||||
}
|
||||
var stored []string
|
||||
e.gdb.Raw(`SELECT titles::text FROM golem15_sitemap_types WHERE type_key = ?`, noteType).Scan(&stored)
|
||||
for _, s := range stored {
|
||||
if s != "{}" && s != "null" {
|
||||
t.Fatalf("a save that did not touch the heading stored %s", s)
|
||||
}
|
||||
}
|
||||
e.rebuild(t)
|
||||
for locale, want := range map[string]string{"pl": "Notatki", "en": "Notes"} {
|
||||
ov := e.htmlJSON(t, "/_sitemap/api/v1/html?locale="+locale, http.StatusOK)
|
||||
groups, _ := ov["groups"].([]any)
|
||||
if len(groups) != 1 {
|
||||
t.Fatalf("%s groups = %v", locale, ov)
|
||||
}
|
||||
if got := groups[0].(map[string]any)["title"]; got != want {
|
||||
t.Errorf("%s group title = %v, want %s", locale, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user