feat: publisher, local business and home crumb settings

- Publisher and Local business tabs with the C20 refusals (sameAs lines,
  person as local business, address, times, country, coordinates)
- the publisher node from the settings, as an Organization, a Person or
  the chosen local business type with address, geo and opening hours
- P8 publisher preview with Google's required and recommended findings
  (classes/schema Validate)
- the home crumb name on the General tab; every new key in the settings
  API and seo_update_settings
This commit is contained in:
Jakub Zych
2026-11-04 14:14:59 +01:00
parent ea2ce24954
commit fde5024b5a
13 changed files with 938 additions and 52 deletions

View File

@@ -39,7 +39,51 @@ var SettingsColumns = []string{
"noindex_paths", "search_paths", "url_host", "url_slash",
"url_lowercase", "links_new_tab", "nofollow_domains",
"sponsored_domains", "verify_google", "verify_bing", "verify_yandex",
"verify_other", "verify_files", "csv_delimiter",
"verify_other", "verify_files", "csv_delimiter", "breadcrumb_home",
"publisher_type", "publisher_name", "publisher_logo", "publisher_url",
"publisher_email", "publisher_phone", "publisher_same_as",
"local_enabled", "local_type", "local_price_range", "local_street",
"local_postal_code", "local_city", "local_region", "local_country",
"local_latitude", "local_longitude", "local_hours",
}
// settingsText maps the plain text settings columns to their fields.
func settingsText(st *models.Settings) map[string]*string {
return map[string]*string{
"site_name": &st.SiteName,
"twitter_site": &st.TwitterSite,
"title_template": &st.TitleTemplate,
"home_template": &st.HomeTemplate,
"default_description": &st.DefaultDescription,
"default_og_image": &st.DefaultOGImage,
"noindex_paths": &st.NoindexPaths,
"search_paths": &st.SearchPaths,
"url_host": &st.URLHost,
"url_slash": &st.URLSlash,
"verify_google": &st.VerifyGoogle,
"verify_bing": &st.VerifyBing,
"verify_yandex": &st.VerifyYandex,
"csv_delimiter": &st.CSVDelimiter,
"nofollow_domains": &st.NofollowDomains,
"sponsored_domains": &st.SponsoredDomains,
"breadcrumb_home": &st.BreadcrumbHome,
"publisher_type": &st.PublisherType,
"publisher_name": &st.PublisherName,
"publisher_logo": &st.PublisherLogo,
"publisher_url": &st.PublisherURL,
"publisher_email": &st.PublisherEmail,
"publisher_phone": &st.PublisherPhone,
"publisher_same_as": &st.PublisherSameAs,
"local_type": &st.LocalType,
"local_price_range": &st.LocalPriceRange,
"local_street": &st.LocalStreet,
"local_postal_code": &st.LocalPostalCode,
"local_city": &st.LocalCity,
"local_region": &st.LocalRegion,
"local_country": &st.LocalCountry,
"local_latitude": &st.LocalLatitude,
"local_longitude": &st.LocalLongitude,
}
}
// SettingsValues are the settings row's values keyed by column, for
@@ -67,6 +111,25 @@ func SettingsValues(st models.Settings) map[string]any {
"verify_other": plainValue(st.OtherMeta()),
"verify_files": plainValue(st.Files()),
"csv_delimiter": st.CSVDelimiter,
"breadcrumb_home": st.BreadcrumbHome,
"publisher_type": st.PublisherType,
"publisher_name": st.PublisherName,
"publisher_logo": st.PublisherLogo,
"publisher_url": st.PublisherURL,
"publisher_email": st.PublisherEmail,
"publisher_phone": st.PublisherPhone,
"publisher_same_as": st.PublisherSameAs,
"local_enabled": st.LocalEnabled,
"local_type": st.LocalType,
"local_price_range": st.LocalPriceRange,
"local_street": st.LocalStreet,
"local_postal_code": st.LocalPostalCode,
"local_city": st.LocalCity,
"local_region": st.LocalRegion,
"local_country": st.LocalCountry,
"local_latitude": st.LocalLatitude,
"local_longitude": st.LocalLongitude,
"local_hours": plainValue(st.Hours()),
}
}
@@ -93,52 +156,34 @@ func SetSettingsValue(st *models.Settings, column string, v any) error {
}
return json.Unmarshal(raw, dst)
}
if field, ok := settingsText(st)[column]; ok {
var err error
*field, err = text()
return err
}
var err error
switch column {
case "site_name":
st.SiteName, err = text()
case "twitter_site":
st.TwitterSite, err = text()
case "title_template":
st.TitleTemplate, err = text()
case "home_template":
st.HomeTemplate, err = text()
case "default_description":
st.DefaultDescription, err = text()
case "default_og_image":
st.DefaultOGImage, err = text()
case "noindex_paths":
st.NoindexPaths, err = text()
case "search_paths":
st.SearchPaths, err = text()
case "url_host":
st.URLHost, err = text()
case "url_slash":
st.URLSlash, err = text()
case "verify_google":
st.VerifyGoogle, err = text()
case "verify_bing":
st.VerifyBing, err = text()
case "verify_yandex":
st.VerifyYandex, err = text()
case "csv_delimiter":
st.CSVDelimiter, err = text()
case "nofollow_domains":
st.NofollowDomains, err = text()
case "sponsored_domains":
st.SponsoredDomains, err = text()
case "url_lowercase":
b, ok := v.(bool)
if !ok {
return fmt.Errorf("golem15.seo: settings %s is not a bool", column)
}
st.URLLowercase = b
case "links_new_tab":
case "links_new_tab", "local_enabled":
b, ok := v.(bool)
if !ok {
return fmt.Errorf("golem15.seo: settings %s is not a bool", column)
}
st.LinksNewTab = b
if column == "links_new_tab" {
st.LinksNewTab = b
} else {
st.LocalEnabled = b
}
case "local_hours":
var out []models.OpeningHours
if err = list(&out); err == nil {
st.LocalHours = lagoon.Jsonable[[]models.OpeningHours]{Data: out, Valid: true}
}
case "query_params":
var out []models.QueryParam
if err = list(&out); err == nil {
@@ -216,8 +261,14 @@ func (s *Service) SaveSettings(ctx context.Context, tx *gorm.DB, in SettingsWrit
oneOf("url_host", row.URLHost, models.HostKeep, models.HostWWW, models.HostBare)
oneOf("url_slash", row.URLSlash, models.SlashKeep, models.SlashAdd, models.SlashDrop)
oneOf("csv_delimiter", row.CSVDelimiter, "", ",", ";")
if _, sent := in.Values["default_og_image"]; sent && !ValidImageURL(row.DefaultOGImage) {
problems["default_og_image"] = append(problems["default_og_image"], models.Problem{Key: errImageURL})
for _, column := range []string{"default_og_image", "publisher_logo"} {
value := row.DefaultOGImage
if column == "publisher_logo" {
value = row.PublisherLogo
}
if _, sent := in.Values[column]; sent && !ValidImageURL(value) {
problems[column] = append(problems[column], models.Problem{Key: errImageURL})
}
}
for field, list := range row.Problems() {
problems[field] = append(problems[field], list...)