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:
@@ -2,6 +2,7 @@ package models
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -40,8 +41,28 @@ const (
|
||||
errVerifyFilenameTaken = "golem15.seo::lang.settings.verify_filename_taken"
|
||||
errTemplateUnknown = "golem15.seo::lang.settings.template_unknown"
|
||||
errDomainInvalid = "golem15.seo::lang.settings.domain_invalid"
|
||||
errValueInvalid = "golem15.seo::lang.api.value_invalid"
|
||||
errSameAsInvalid = "golem15.seo::lang.settings.same_as_invalid"
|
||||
errURLInvalid = "golem15.seo::lang.settings.url_invalid"
|
||||
errEmailInvalid = "golem15.seo::lang.settings.email_invalid"
|
||||
errLocalPerson = "golem15.seo::lang.settings.local_person"
|
||||
errLocalAddress = "golem15.seo::lang.settings.local_address_required"
|
||||
errTimeInvalid = "golem15.seo::lang.settings.time_invalid"
|
||||
errHoursOrder = "golem15.seo::lang.settings.hours_order"
|
||||
errCountryInvalid = "golem15.seo::lang.settings.country_invalid"
|
||||
errCoordinateInvalid = "golem15.seo::lang.settings.coordinate_invalid"
|
||||
)
|
||||
|
||||
// LocalTypes are the LocalBusiness types the settings offer (UI-SPEC C20).
|
||||
var LocalTypes = []string{
|
||||
"LocalBusiness", "ProfessionalService", "Store", "FoodEstablishment",
|
||||
"HealthAndBeautyBusiness", "MedicalBusiness", "LegalService",
|
||||
"HomeAndConstructionBusiness", "AutomotiveBusiness", "FinancialService",
|
||||
}
|
||||
|
||||
// Days are the day codes of the opening hours, Monday first.
|
||||
var Days = []string{"mon", "tue", "wed", "thu", "fri", "sat", "sun"}
|
||||
|
||||
// QueryParam is one row of the D-07 query-parameter list: a name (a
|
||||
// trailing * matches a prefix), its kind, and an optional path pattern the
|
||||
// row is limited to.
|
||||
@@ -174,7 +195,12 @@ func (Settings) Fillable() []string {
|
||||
"noindex_paths", "search_paths", "url_host", "url_slash",
|
||||
"url_lowercase", "verify_google", "verify_bing", "verify_yandex",
|
||||
"verify_other", "verify_files", "links_new_tab", "nofollow_domains",
|
||||
"sponsored_domains",
|
||||
"sponsored_domains", "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",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -278,6 +304,108 @@ func (s Settings) Problems() map[string][]Problem {
|
||||
out[field] = p
|
||||
}
|
||||
}
|
||||
for field, list := range s.PublisherProblems() {
|
||||
out[field] = append(out[field], list...)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// timePattern is a 24-hour HH:MM time.
|
||||
var timePattern = regexp.MustCompile(`^([01][0-9]|2[0-3]):[0-5][0-9]$`)
|
||||
|
||||
// countryPattern is a two-letter country code.
|
||||
var countryPattern = regexp.MustCompile(`^[A-Za-z]{2}$`)
|
||||
|
||||
// HTTPSURL reports whether s is an absolute https URL with a host.
|
||||
func HTTPSURL(s string) bool {
|
||||
u, err := url.Parse(strings.TrimSpace(s))
|
||||
return err == nil && u.Scheme == "https" && u.Host != "" && !strings.ContainsAny(s, " \t\n<>\"")
|
||||
}
|
||||
|
||||
// oneOfList reports whether v is one of list.
|
||||
func oneOfList(v string, list []string) bool {
|
||||
for _, item := range list {
|
||||
if v == item {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// PublisherProblems lists the refusals of the Publisher and Local
|
||||
// business tabs (UI-SPEC C20, D-45, T-33-25): the publisher type, https
|
||||
// sameAs lines (with the line number), the website, the email, a person
|
||||
// as a local business, the business's address, country, coordinates and
|
||||
// opening hours (local_hours.<i>.opens / .closes).
|
||||
func (s Settings) PublisherProblems() map[string][]Problem {
|
||||
out := map[string][]Problem{}
|
||||
add := func(field string, p Problem) { out[field] = append(out[field], p) }
|
||||
if s.PublisherType != "" && !oneOfList(s.PublisherType, []string{PublisherOrganization, PublisherPerson}) {
|
||||
add("publisher_type", Problem{Key: errValueInvalid})
|
||||
}
|
||||
n := 0
|
||||
for _, line := range strings.Split(strings.ReplaceAll(s.PublisherSameAs, "\r\n", "\n"), "\n") {
|
||||
n++
|
||||
if v := strings.TrimSpace(line); v != "" && !HTTPSURL(v) {
|
||||
add("publisher_same_as", Problem{Key: errSameAsInvalid, Params: map[string]string{"line": strconv.Itoa(n)}})
|
||||
break
|
||||
}
|
||||
}
|
||||
if v := strings.TrimSpace(s.PublisherURL); v != "" && !HTTPSURL(v) {
|
||||
add("publisher_url", Problem{Key: errURLInvalid})
|
||||
}
|
||||
if v := strings.TrimSpace(s.PublisherEmail); v != "" {
|
||||
local, domain, ok := strings.Cut(v, "@")
|
||||
if !ok || local == "" || !strings.Contains(domain, ".") || strings.ContainsAny(v, " <>\"") {
|
||||
add("publisher_email", Problem{Key: errEmailInvalid})
|
||||
}
|
||||
}
|
||||
if !s.LocalEnabled {
|
||||
return out
|
||||
}
|
||||
if s.PublisherType == PublisherPerson {
|
||||
add("local_enabled", Problem{Key: errLocalPerson})
|
||||
}
|
||||
if s.LocalType != "" && !oneOfList(s.LocalType, LocalTypes) {
|
||||
add("local_type", Problem{Key: errValueInvalid})
|
||||
}
|
||||
if strings.TrimSpace(s.LocalStreet) == "" || strings.TrimSpace(s.LocalPostalCode) == "" || strings.TrimSpace(s.LocalCity) == "" {
|
||||
for field, v := range map[string]string{"local_street": s.LocalStreet, "local_postal_code": s.LocalPostalCode, "local_city": s.LocalCity} {
|
||||
if strings.TrimSpace(v) == "" {
|
||||
add(field, Problem{Key: errLocalAddress})
|
||||
}
|
||||
}
|
||||
}
|
||||
if v := strings.TrimSpace(s.LocalCountry); v != "" && !countryPattern.MatchString(v) {
|
||||
add("local_country", Problem{Key: errCountryInvalid})
|
||||
}
|
||||
coordinate := func(field, v string, limit float64) {
|
||||
if v = strings.TrimSpace(v); v == "" {
|
||||
return
|
||||
}
|
||||
f, err := strconv.ParseFloat(strings.ReplaceAll(v, ",", "."), 64)
|
||||
if err != nil || f < -limit || f > limit {
|
||||
add(field, Problem{Key: errCoordinateInvalid})
|
||||
}
|
||||
}
|
||||
coordinate("local_latitude", s.LocalLatitude, 90)
|
||||
coordinate("local_longitude", s.LocalLongitude, 180)
|
||||
for i, h := range s.Hours() {
|
||||
base := fmt.Sprintf("local_hours.%d.", i)
|
||||
if !oneOfList(h.Day, Days) {
|
||||
add(base+"day", Problem{Key: errValueInvalid})
|
||||
}
|
||||
okOpens, okCloses := timePattern.MatchString(h.Opens), timePattern.MatchString(h.Closes)
|
||||
if !okOpens {
|
||||
add(base+"opens", Problem{Key: errTimeInvalid})
|
||||
}
|
||||
if !okCloses {
|
||||
add(base+"closes", Problem{Key: errTimeInvalid})
|
||||
}
|
||||
if okOpens && okCloses && h.Closes <= h.Opens {
|
||||
add(base+"closes", Problem{Key: errHoursOrder})
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
# Form Field Definitions
|
||||
# ===================================
|
||||
#
|
||||
# SEO settings (UI-SPEC C11): General, one tab per content type (added by
|
||||
# the plugin's form extension after General), Indexing, Addresses, Links
|
||||
# (UI-SPEC C20) and Verification.
|
||||
# SEO settings (UI-SPEC C11, C20): General, one tab per content type (added
|
||||
# by the plugin's form extension after General), Indexing, Addresses,
|
||||
# Links, Publisher, Local business and Verification.
|
||||
|
||||
fields:
|
||||
_staging:
|
||||
@@ -27,6 +27,13 @@ fields:
|
||||
span: right
|
||||
tab: golem15.seo::lang.settings.tab_general
|
||||
|
||||
breadcrumb_home:
|
||||
label: golem15.seo::lang.settings.breadcrumb_home
|
||||
comment: golem15.seo::lang.settings.breadcrumb_home_comment
|
||||
type: mltext
|
||||
span: right
|
||||
tab: golem15.seo::lang.settings.tab_general
|
||||
|
||||
title_template:
|
||||
label: golem15.seo::lang.settings.title_template
|
||||
comment: golem15.seo::lang.settings.title_template_comment
|
||||
@@ -163,6 +170,215 @@ fields:
|
||||
span: right
|
||||
tab: golem15.seo::lang.settings.tab_links
|
||||
|
||||
publisher_type:
|
||||
label: golem15.seo::lang.settings.publisher_type
|
||||
comment: golem15.seo::lang.settings.publisher_type_comment
|
||||
type: dropdown
|
||||
span: left
|
||||
default: organization
|
||||
options:
|
||||
organization: golem15.seo::lang.settings.publisher_org
|
||||
person: golem15.seo::lang.settings.publisher_person
|
||||
tab: golem15.seo::lang.settings.tab_publisher
|
||||
|
||||
publisher_name:
|
||||
label: golem15.seo::lang.settings.publisher_name
|
||||
comment: golem15.seo::lang.settings.publisher_name_comment
|
||||
type: mltext
|
||||
span: right
|
||||
tab: golem15.seo::lang.settings.tab_publisher
|
||||
|
||||
publisher_logo:
|
||||
label: golem15.seo::lang.settings.publisher_logo
|
||||
comment: golem15.seo::lang.settings.publisher_logo_comment
|
||||
type: seoimage
|
||||
span: full
|
||||
tab: golem15.seo::lang.settings.tab_publisher
|
||||
|
||||
publisher_url:
|
||||
label: golem15.seo::lang.settings.publisher_url
|
||||
comment: golem15.seo::lang.settings.publisher_url_comment
|
||||
type: text
|
||||
span: left
|
||||
tab: golem15.seo::lang.settings.tab_publisher
|
||||
|
||||
publisher_email:
|
||||
label: golem15.seo::lang.settings.publisher_email
|
||||
type: text
|
||||
span: right
|
||||
tab: golem15.seo::lang.settings.tab_publisher
|
||||
|
||||
publisher_phone:
|
||||
label: golem15.seo::lang.settings.publisher_phone
|
||||
comment: golem15.seo::lang.settings.publisher_phone_comment
|
||||
type: text
|
||||
span: left
|
||||
tab: golem15.seo::lang.settings.tab_publisher
|
||||
|
||||
publisher_same_as:
|
||||
label: golem15.seo::lang.settings.publisher_same_as
|
||||
comment: golem15.seo::lang.settings.publisher_same_as_comment
|
||||
type: textarea
|
||||
size: small
|
||||
span: full
|
||||
tab: golem15.seo::lang.settings.tab_publisher
|
||||
|
||||
_publisher_preview:
|
||||
label: golem15.seo::lang.settings.publisher_preview
|
||||
type: partial
|
||||
path: publisher_preview
|
||||
span: full
|
||||
context: update
|
||||
tab: golem15.seo::lang.settings.tab_publisher
|
||||
|
||||
local_enabled:
|
||||
label: golem15.seo::lang.settings.local_enabled
|
||||
comment: golem15.seo::lang.settings.local_enabled_comment
|
||||
type: switch
|
||||
span: full
|
||||
default: false
|
||||
tab: golem15.seo::lang.settings.tab_local
|
||||
|
||||
local_type:
|
||||
label: golem15.seo::lang.settings.local_type
|
||||
type: dropdown
|
||||
span: left
|
||||
default: LocalBusiness
|
||||
options:
|
||||
LocalBusiness: golem15.seo::lang.settings.local_type_local_business
|
||||
ProfessionalService: golem15.seo::lang.settings.local_type_professional_service
|
||||
Store: golem15.seo::lang.settings.local_type_store
|
||||
FoodEstablishment: golem15.seo::lang.settings.local_type_food_establishment
|
||||
HealthAndBeautyBusiness: golem15.seo::lang.settings.local_type_health_and_beauty_business
|
||||
MedicalBusiness: golem15.seo::lang.settings.local_type_medical_business
|
||||
LegalService: golem15.seo::lang.settings.local_type_legal_service
|
||||
HomeAndConstructionBusiness: golem15.seo::lang.settings.local_type_home_and_construction_business
|
||||
AutomotiveBusiness: golem15.seo::lang.settings.local_type_automotive_business
|
||||
FinancialService: golem15.seo::lang.settings.local_type_financial_service
|
||||
tab: golem15.seo::lang.settings.tab_local
|
||||
trigger:
|
||||
action: hide
|
||||
field: local_enabled
|
||||
condition: unchecked
|
||||
|
||||
local_price_range:
|
||||
label: golem15.seo::lang.settings.local_price_range
|
||||
comment: golem15.seo::lang.settings.local_price_range_comment
|
||||
type: text
|
||||
span: right
|
||||
tab: golem15.seo::lang.settings.tab_local
|
||||
trigger:
|
||||
action: hide
|
||||
field: local_enabled
|
||||
condition: unchecked
|
||||
|
||||
local_street:
|
||||
label: golem15.seo::lang.settings.local_street
|
||||
type: text
|
||||
span: full
|
||||
tab: golem15.seo::lang.settings.tab_local
|
||||
trigger:
|
||||
action: hide
|
||||
field: local_enabled
|
||||
condition: unchecked
|
||||
|
||||
local_postal_code:
|
||||
label: golem15.seo::lang.settings.local_postal_code
|
||||
type: text
|
||||
span: left
|
||||
tab: golem15.seo::lang.settings.tab_local
|
||||
trigger:
|
||||
action: hide
|
||||
field: local_enabled
|
||||
condition: unchecked
|
||||
|
||||
local_city:
|
||||
label: golem15.seo::lang.settings.local_city
|
||||
type: text
|
||||
span: right
|
||||
tab: golem15.seo::lang.settings.tab_local
|
||||
trigger:
|
||||
action: hide
|
||||
field: local_enabled
|
||||
condition: unchecked
|
||||
|
||||
local_region:
|
||||
label: golem15.seo::lang.settings.local_region
|
||||
type: text
|
||||
span: left
|
||||
tab: golem15.seo::lang.settings.tab_local
|
||||
trigger:
|
||||
action: hide
|
||||
field: local_enabled
|
||||
condition: unchecked
|
||||
|
||||
local_country:
|
||||
label: golem15.seo::lang.settings.local_country
|
||||
comment: golem15.seo::lang.settings.local_country_comment
|
||||
type: text
|
||||
span: right
|
||||
tab: golem15.seo::lang.settings.tab_local
|
||||
trigger:
|
||||
action: hide
|
||||
field: local_enabled
|
||||
condition: unchecked
|
||||
|
||||
local_latitude:
|
||||
label: golem15.seo::lang.settings.local_latitude
|
||||
comment: golem15.seo::lang.settings.local_coordinates_comment
|
||||
type: text
|
||||
span: left
|
||||
tab: golem15.seo::lang.settings.tab_local
|
||||
trigger:
|
||||
action: hide
|
||||
field: local_enabled
|
||||
condition: unchecked
|
||||
|
||||
local_longitude:
|
||||
label: golem15.seo::lang.settings.local_longitude
|
||||
comment: golem15.seo::lang.settings.local_coordinates_comment
|
||||
type: text
|
||||
span: right
|
||||
tab: golem15.seo::lang.settings.tab_local
|
||||
trigger:
|
||||
action: hide
|
||||
field: local_enabled
|
||||
condition: unchecked
|
||||
|
||||
local_hours:
|
||||
label: golem15.seo::lang.settings.local_hours
|
||||
comment: golem15.seo::lang.settings.local_hours_comment
|
||||
type: repeater
|
||||
prompt: golem15.seo::lang.settings.local_hours_add
|
||||
titleFrom: day
|
||||
span: full
|
||||
tab: golem15.seo::lang.settings.tab_local
|
||||
form:
|
||||
fields:
|
||||
day:
|
||||
label: golem15.seo::lang.settings.hours_day
|
||||
type: dropdown
|
||||
span: left
|
||||
default: mon
|
||||
options:
|
||||
mon: golem15.seo::lang.settings.day_mon
|
||||
tue: golem15.seo::lang.settings.day_tue
|
||||
wed: golem15.seo::lang.settings.day_wed
|
||||
thu: golem15.seo::lang.settings.day_thu
|
||||
fri: golem15.seo::lang.settings.day_fri
|
||||
sat: golem15.seo::lang.settings.day_sat
|
||||
sun: golem15.seo::lang.settings.day_sun
|
||||
opens:
|
||||
label: golem15.seo::lang.settings.hours_opens
|
||||
comment: golem15.seo::lang.settings.hours_time_comment
|
||||
type: text
|
||||
span: left
|
||||
closes:
|
||||
label: golem15.seo::lang.settings.hours_closes
|
||||
comment: golem15.seo::lang.settings.hours_time_comment
|
||||
type: text
|
||||
span: right
|
||||
|
||||
verify_google:
|
||||
label: golem15.seo::lang.settings.verify_google
|
||||
comment: golem15.seo::lang.settings.verify_code_comment
|
||||
|
||||
Reference in New Issue
Block a user