feat: URL rules and the settings singleton
This commit is contained in:
211
models/settings.go
Normal file
211
models/settings.go
Normal file
@@ -0,0 +1,211 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.golem15.com/golem15/summercms/modules/lagoon"
|
||||
)
|
||||
|
||||
// The query parameter kinds of the D-07 list.
|
||||
const (
|
||||
ParamContent = "content"
|
||||
ParamFilter = "filter"
|
||||
ParamTracking = "tracking"
|
||||
)
|
||||
|
||||
// The address options of D-20b.
|
||||
const (
|
||||
HostKeep = "keep"
|
||||
HostWWW = "www"
|
||||
HostBare = "bare"
|
||||
SlashKeep = "keep"
|
||||
SlashAdd = "add"
|
||||
SlashDrop = "remove"
|
||||
)
|
||||
|
||||
// Default templates (D-03).
|
||||
const (
|
||||
DefaultTitleTemplate = "%title% | %site%"
|
||||
DefaultHomeTemplate = "%site%"
|
||||
)
|
||||
|
||||
// The phrase keys of the settings refusals.
|
||||
const (
|
||||
errVerifyFilenameInvalid = "golem15.seo::lang.settings.verify_filename_invalid"
|
||||
errVerifyFilenameTaken = "golem15.seo::lang.settings.verify_filename_taken"
|
||||
errTemplateUnknown = "golem15.seo::lang.settings.template_unknown"
|
||||
)
|
||||
|
||||
// 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.
|
||||
type QueryParam struct {
|
||||
Name string `json:"name"`
|
||||
Kind string `json:"kind"`
|
||||
Path string `json:"path"`
|
||||
}
|
||||
|
||||
// VerifyMeta is one extra verification meta tag (D-05).
|
||||
type VerifyMeta struct {
|
||||
Name string `json:"name"`
|
||||
Content string `json:"content"`
|
||||
}
|
||||
|
||||
// VerifyFile is one verification file served from the site root (D-05).
|
||||
type VerifyFile struct {
|
||||
Filename string `json:"filename"`
|
||||
Body string `json:"body"`
|
||||
}
|
||||
|
||||
// DefaultQueryParams is the seeded D-07 list.
|
||||
func DefaultQueryParams() []QueryParam {
|
||||
return []QueryParam{
|
||||
{Name: "page", Kind: ParamContent},
|
||||
{Name: "utm_*", Kind: ParamTracking},
|
||||
{Name: "fbclid", Kind: ParamTracking},
|
||||
{Name: "gclid", Kind: ParamTracking},
|
||||
{Name: "msclkid", Kind: ParamTracking},
|
||||
{Name: "sort", Kind: ParamFilter},
|
||||
}
|
||||
}
|
||||
|
||||
// Settings is the SEO settings singleton (row id 1) on golem15_seo_settings
|
||||
// (D-03, D-05, D-07, D-20b, D-21, D-23). site_name and default_description
|
||||
// are translatable; the other locales live with the translate plugin.
|
||||
type Settings struct {
|
||||
ID uint `gorm:"column:id;primaryKey"`
|
||||
SiteName string `gorm:"column:site_name"`
|
||||
TwitterSite string `gorm:"column:twitter_site"`
|
||||
TitleTemplate string `gorm:"column:title_template"`
|
||||
HomeTemplate string `gorm:"column:home_template"`
|
||||
DefaultDescription string `gorm:"column:default_description"`
|
||||
DefaultOGImage string `gorm:"column:default_og_image"`
|
||||
QueryParams lagoon.Jsonable[[]QueryParam] `gorm:"column:query_params"`
|
||||
NoindexPaths string `gorm:"column:noindex_paths"`
|
||||
SearchPaths string `gorm:"column:search_paths"`
|
||||
URLHost string `gorm:"column:url_host"`
|
||||
URLSlash string `gorm:"column:url_slash"`
|
||||
URLLowercase bool `gorm:"column:url_lowercase"`
|
||||
VerifyGoogle string `gorm:"column:verify_google"`
|
||||
VerifyBing string `gorm:"column:verify_bing"`
|
||||
VerifyYandex string `gorm:"column:verify_yandex"`
|
||||
VerifyOther lagoon.Jsonable[[]VerifyMeta] `gorm:"column:verify_other"`
|
||||
VerifyFiles lagoon.Jsonable[[]VerifyFile] `gorm:"column:verify_files"`
|
||||
CSVDelimiter string `gorm:"column:csv_delimiter"`
|
||||
CreatedAt *time.Time `gorm:"column:created_at"`
|
||||
UpdatedAt *time.Time `gorm:"column:updated_at"`
|
||||
}
|
||||
|
||||
func (Settings) TableName() string { return "golem15_seo_settings" }
|
||||
|
||||
// MorphName names the settings row for translations.
|
||||
func (Settings) MorphName() string { return `Golem15\SEO\Models\Settings` }
|
||||
|
||||
// Fillable lists the form's columns.
|
||||
func (Settings) Fillable() []string {
|
||||
return []string{
|
||||
"site_name", "twitter_site", "title_template", "home_template",
|
||||
"default_description", "default_og_image", "query_params",
|
||||
"noindex_paths", "search_paths", "url_host", "url_slash",
|
||||
"url_lowercase", "verify_google", "verify_bing", "verify_yandex",
|
||||
"verify_other", "verify_files",
|
||||
}
|
||||
}
|
||||
|
||||
// Rules is empty: the settings controller checks Problems before a save.
|
||||
func (Settings) Rules() map[string]string { return map[string]string{} }
|
||||
|
||||
// DefaultSettings is what the resolver reads when the row is missing.
|
||||
func DefaultSettings() Settings {
|
||||
return Settings{
|
||||
ID: 1,
|
||||
TitleTemplate: DefaultTitleTemplate,
|
||||
HomeTemplate: DefaultHomeTemplate,
|
||||
QueryParams: lagoon.Jsonable[[]QueryParam]{Data: DefaultQueryParams(), Valid: true},
|
||||
URLHost: HostKeep,
|
||||
URLSlash: SlashKeep,
|
||||
}
|
||||
}
|
||||
|
||||
// Params returns the query-parameter list.
|
||||
func (s Settings) Params() []QueryParam { return s.QueryParams.Get() }
|
||||
|
||||
// OtherMeta returns the extra verification meta tags.
|
||||
func (s Settings) OtherMeta() []VerifyMeta { return s.VerifyOther.Get() }
|
||||
|
||||
// Files returns the verification files.
|
||||
func (s Settings) Files() []VerifyFile { return s.VerifyFiles.Get() }
|
||||
|
||||
// verifyFilename is the allowed form of a verification file name.
|
||||
var verifyFilename = regexp.MustCompile(`^[A-Za-z0-9._-]+\.(html|txt|xml)$`)
|
||||
|
||||
// placeholderPattern finds %name% placeholders in a template.
|
||||
var placeholderPattern = regexp.MustCompile(`%[A-Za-z0-9_]*%`)
|
||||
|
||||
// KnownPlaceholders are the placeholders a template may use.
|
||||
var KnownPlaceholders = []string{"%title%", "%description%", "%site%"}
|
||||
|
||||
// UnknownPlaceholder returns the first placeholder of tpl that is not
|
||||
// known, "" when every one is.
|
||||
func UnknownPlaceholder(tpl string) string {
|
||||
for _, found := range placeholderPattern.FindAllString(tpl, -1) {
|
||||
known := false
|
||||
for _, k := range KnownPlaceholders {
|
||||
if found == k {
|
||||
known = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !known {
|
||||
return found
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// Problem is one field refusal: a phrase key and its :name parameters.
|
||||
// The admin controllers translate it in the request locale.
|
||||
type Problem struct {
|
||||
Key string
|
||||
Params map[string]string
|
||||
}
|
||||
|
||||
// TemplateProblems refuses a template with an unknown placeholder.
|
||||
func TemplateProblems(tpl string) []Problem {
|
||||
if p := UnknownPlaceholder(tpl); p != "" {
|
||||
return []Problem{{Key: errTemplateUnknown, Params: map[string]string{"placeholder": p}}}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Problems lists the settings refusals per field: templates with an unknown
|
||||
// placeholder, and verification file names that are malformed or used
|
||||
// twice (verify_files.<i>.filename).
|
||||
func (s Settings) Problems() map[string][]Problem {
|
||||
out := map[string][]Problem{}
|
||||
for field, tpl := range map[string]string{"title_template": s.TitleTemplate, "home_template": s.HomeTemplate} {
|
||||
if p := TemplateProblems(tpl); p != nil {
|
||||
out[field] = p
|
||||
}
|
||||
}
|
||||
seen := map[string]bool{}
|
||||
for i, f := range s.Files() {
|
||||
name := strings.TrimSpace(f.Filename)
|
||||
key := fmt.Sprintf("verify_files.%d.filename", i)
|
||||
switch {
|
||||
case !verifyFilename.MatchString(name):
|
||||
out[key] = []Problem{{Key: errVerifyFilenameInvalid}}
|
||||
case seen[strings.ToLower(name)]:
|
||||
out[key] = []Problem{{Key: errVerifyFilenameTaken}}
|
||||
}
|
||||
seen[strings.ToLower(name)] = true
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func init() {
|
||||
Register(Settings{})
|
||||
}
|
||||
Reference in New Issue
Block a user