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:
@@ -92,7 +92,7 @@ func (Settings) TableName() string { return "golem15_sitemap_settings" }
|
||||
|
||||
// Fillable lists the columns the settings form edits.
|
||||
func (Settings) Fillable() []string {
|
||||
return []string{"site_url", "file_size", "extra_urls", "images_enabled", "news_enabled", "news_publication", "news_sources", "indexnow_enabled"}
|
||||
return []string{"site_url", "file_size", "extra_urls", "images_enabled", "news_enabled", "news_publication", "news_sources", "indexnow_enabled", "html_enabled", "html_page_size"}
|
||||
}
|
||||
|
||||
// Rules is empty: the settings controller runs Validate before a save.
|
||||
@@ -105,6 +105,8 @@ const (
|
||||
errLastmod = "golem15.sitemap::lang.errors.lastmod"
|
||||
errNewsName = "golem15.sitemap::lang.errors.news_publication"
|
||||
errNewsSrc = "golem15.sitemap::lang.errors.news_source"
|
||||
errHTMLPath = "golem15.sitemap::lang.errors.html_path"
|
||||
errPageSize = "golem15.sitemap::lang.errors.page_size"
|
||||
)
|
||||
|
||||
// MaxNewsPublication is the longest publication name.
|
||||
@@ -128,7 +130,9 @@ func (s Settings) Validate() error {
|
||||
// Problems lists the settings' refusals by field (UI-SPEC C26): a site
|
||||
// address that AllowedFrontendURL refuses, a file size outside 1..MaxFileSize and an additional
|
||||
// address that is not a path or an http(s) URL (extra_urls.<i>.url) or
|
||||
// whose lastmod is not a date (extra_urls.<i>.lastmod).
|
||||
// whose lastmod is not a date (extra_urls.<i>.lastmod), a publication name
|
||||
// over 255 characters, a malformed News source, an HTML sitemap address
|
||||
// that ValidHTMLPath refuses (html_path) and a page size outside 50..500.
|
||||
func (s Settings) Problems() map[string][]string {
|
||||
out := map[string][]string{}
|
||||
if !AllowedFrontendURL(Text(s.SiteURL)) {
|
||||
@@ -154,9 +158,42 @@ func (s Settings) Problems() map[string][]string {
|
||||
break
|
||||
}
|
||||
}
|
||||
for _, p := range s.HTMLPaths.Get() {
|
||||
if p = strings.TrimSpace(p); p != "" && !ValidHTMLPath(p) {
|
||||
out["html_path"] = []string{errHTMLPath}
|
||||
break
|
||||
}
|
||||
}
|
||||
if s.HTMLPageSize < MinHTMLPageSize || s.HTMLPageSize > MaxHTMLPageSize {
|
||||
out["html_page_size"] = []string{errPageSize}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// HTMLPathProblem is the refusal of an HTML sitemap address (errors.html_path).
|
||||
const HTMLPathProblem = errHTMLPath
|
||||
|
||||
// ValidHTMLPath reports whether p may be the HTML sitemap's address: a
|
||||
// path that starts with a single /, at most 255 characters of letters,
|
||||
// digits and - _ . ~ / (no query, fragment, "//" or ".." segment), and not
|
||||
// the site root.
|
||||
func ValidHTMLPath(p string) bool {
|
||||
if len(p) < 2 || len(p) > 255 || p[0] != '/' || strings.Contains(p, "//") {
|
||||
return false
|
||||
}
|
||||
for _, seg := range strings.Split(p[1:], "/") {
|
||||
if seg == "." || seg == ".." {
|
||||
return false
|
||||
}
|
||||
}
|
||||
for _, r := range p {
|
||||
if !(r >= 'a' && r <= 'z' || r >= 'A' && r <= 'Z' || r >= '0' && r <= '9' || strings.ContainsRune("-_.~/", r)) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// The prefixes of the news_sources values (P33-15): type:<type key> and
|
||||
// category:<type key>:<id>.
|
||||
const (
|
||||
|
||||
@@ -127,3 +127,21 @@ fields:
|
||||
action: hide
|
||||
field: indexnow_enabled
|
||||
condition: unchecked
|
||||
|
||||
# The address per locale (html_path, an mltext field) is added after
|
||||
# this switch by the plugin's form extension, stored in html_paths.
|
||||
html_enabled:
|
||||
label: golem15.sitemap::lang.settings.html_enabled
|
||||
comment: golem15.sitemap::lang.settings.html_enabled_comment
|
||||
type: switch
|
||||
default: true
|
||||
span: full
|
||||
tab: golem15.sitemap::lang.settings.tab_html
|
||||
|
||||
html_page_size:
|
||||
label: golem15.sitemap::lang.settings.html_page_size
|
||||
comment: golem15.sitemap::lang.settings.html_page_size_comment
|
||||
type: number
|
||||
default: 150
|
||||
span: right
|
||||
tab: golem15.sitemap::lang.settings.tab_html
|
||||
|
||||
@@ -75,6 +75,9 @@ func TestSettingsValidate(t *testing.T) {
|
||||
if s.FileSize == 0 {
|
||||
s.FileSize = 1000
|
||||
}
|
||||
if s.HTMLPageSize == 0 {
|
||||
s.HTMLPageSize = models.DefaultHTMLPageSize
|
||||
}
|
||||
return s
|
||||
}
|
||||
extra := func(urls ...string) lagoon.Jsonable[[]models.ExtraURL] {
|
||||
@@ -88,8 +91,8 @@ func TestSettingsValidate(t *testing.T) {
|
||||
"null site_url": ok(models.Settings{}),
|
||||
"empty site_url": ok(models.Settings{SiteURL: text("")}),
|
||||
"site_url": ok(models.Settings{SiteURL: text("https://www.ex.pl/")}),
|
||||
"file size 1": {FileSize: 1},
|
||||
"file size 50 000": {FileSize: 50000},
|
||||
"file size 1": ok(models.Settings{FileSize: 1}),
|
||||
"file size 50 000": ok(models.Settings{FileSize: 50000}),
|
||||
"extra addresses": ok(models.Settings{ExtraURLs: extra("/", "/o-nas?x=1&y=2", "https://www.ex.pl/kontakt")}),
|
||||
} {
|
||||
if err := s.Validate(); err != nil {
|
||||
@@ -103,8 +106,8 @@ func TestSettingsValidate(t *testing.T) {
|
||||
"javascript site_url": {ok(models.Settings{SiteURL: text("javascript:alert(document.domain)//")}), map[string]any{"site_url": []string{"golem15.sitemap::lang.errors.frontend_url"}}},
|
||||
"attribute site_url": {ok(models.Settings{SiteURL: text(`https://x"/><y a="`)}), map[string]any{"site_url": []string{"golem15.sitemap::lang.errors.frontend_url"}}},
|
||||
"path site_url": {ok(models.Settings{SiteURL: text("/x")}), map[string]any{"site_url": []string{"golem15.sitemap::lang.errors.frontend_url"}}},
|
||||
"file size 0": {models.Settings{}, map[string]any{"file_size": []string{"golem15.sitemap::lang.errors.file_size"}}},
|
||||
"file size 50 001": {models.Settings{FileSize: 50001}, map[string]any{"file_size": []string{"golem15.sitemap::lang.errors.file_size"}}},
|
||||
"file size 0": {models.Settings{HTMLPageSize: 150}, map[string]any{"file_size": []string{"golem15.sitemap::lang.errors.file_size"}}},
|
||||
"file size 50 001": {models.Settings{FileSize: 50001, HTMLPageSize: 150}, map[string]any{"file_size": []string{"golem15.sitemap::lang.errors.file_size"}}},
|
||||
"javascript address": {ok(models.Settings{ExtraURLs: extra("/", "javascript:alert(1)")}), map[string]any{"extra_urls.1.url": []string{"golem15.sitemap::lang.errors.url_scheme"}}},
|
||||
"protocol-relative": {ok(models.Settings{ExtraURLs: extra("//evil.test/x")}), map[string]any{"extra_urls.0.url": []string{"golem15.sitemap::lang.errors.url_scheme"}}},
|
||||
"address with a space": {ok(models.Settings{ExtraURLs: extra("/a b")}), map[string]any{"extra_urls.0.url": []string{"golem15.sitemap::lang.errors.url_scheme"}}},
|
||||
|
||||
Reference in New Issue
Block a user