123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389 |
- package seo
- import (
- "context"
- "net/http"
- "net/url"
- "path"
- "reflect"
- "regexp"
- "strings"
- "github.com/qor5/admin/l10n"
- h "github.com/theplant/htmlgo"
- "gorm.io/gorm"
- )
- var (
- GlobalSEO = "Global SEO"
- GlobalDB *gorm.DB
- DBContextKey contextKey = "DB"
- )
- type (
- contextKey string
- contextVariablesFunc func(interface{}, *Setting, *http.Request) string
- )
- // Create a SeoCollection instance
- func NewCollection() *Collection {
- collection := &Collection{
- settingModel: &QorSEOSetting{},
- dbContextKey: DBContextKey,
- globalName: GlobalSEO,
- inherited: true,
- }
- collection.RegisterSEO(GlobalSEO).RegisterSettingVaribles(struct{ SiteName string }{}).
- RegisterContextVariables(
- "og:url", func(_ interface{}, _ *Setting, req *http.Request) string {
- return req.URL.String()
- },
- )
- return collection
- }
- // Collection will hold registered seo configures and global setting definition and other configures
- // @snippet_begin(SeoCollectionDefinition)
- type Collection struct {
- registeredSEO []*SEO
- globalName string //default name is GlobalSEO
- inherited bool //default is true. the order is model seo setting, system seo setting, global seo setting
- dbContextKey interface{} // get db from context
- settingModel interface{} // db model
- afterSave func(ctx context.Context, settingName string, locale string) error // hook called after saving
- }
- // @snippet_end
- // SEO represents a seo object for a page
- // @snippet_begin(SeoDefinition)
- type SEO struct {
- name string
- modelTyp reflect.Type
- contextVariables map[string]contextVariablesFunc // fetch context variables from request
- settingVariables interface{} // fetch setting variables from db
- }
- // @snippet_end
- // RegisterModel register a model to seo
- func (seo *SEO) SetModel(model interface{}) *SEO {
- seo.modelTyp = reflect.Indirect(reflect.ValueOf(model)).Type()
- return seo
- }
- // SetName set seo name
- func (seo *SEO) SetName(name string) *SEO {
- seo.name = name
- return seo
- }
- // RegisterContextVariables register context variables. the registered variables will be rendered to the page
- func (seo *SEO) RegisterContextVariables(key string, f contextVariablesFunc) *SEO {
- if seo.contextVariables == nil {
- seo.contextVariables = map[string]contextVariablesFunc{}
- }
- seo.contextVariables[key] = f
- return seo
- }
- // RegisterSettingVaribles register a setting variable
- func (seo *SEO) RegisterSettingVaribles(setting interface{}) *SEO {
- seo.settingVariables = setting
- return seo
- }
- func (collection *Collection) SetGlobalName(name string) *Collection {
- collection.globalName = name
- if globalSeo := collection.GetSEOByName(GlobalSEO); globalSeo != nil {
- globalSeo.SetName(name)
- }
- return collection
- }
- func (collection *Collection) NewSettingModelInstance() interface{} {
- return reflect.New(reflect.Indirect(reflect.ValueOf(collection.settingModel)).Type()).Interface()
- }
- func (collection *Collection) NewSettingModelSlice() interface{} {
- sliceType := reflect.SliceOf(reflect.PtrTo(reflect.Indirect(reflect.ValueOf(collection.settingModel)).Type()))
- slice := reflect.New(sliceType)
- slice.Elem().Set(reflect.MakeSlice(sliceType, 0, 0))
- return slice.Interface()
- }
- // RegisterVariblesSetting register variables setting
- func (collection *Collection) SetInherited(b bool) *Collection {
- collection.inherited = b
- return collection
- }
- // RegisterVariblesSetting register variables setting
- func (collection *Collection) SetSettingModel(s interface{}) *Collection {
- collection.settingModel = s
- return collection
- }
- // RegisterDBContextKey register a key to get db from context
- func (collection *Collection) SetDBContextKey(key interface{}) *Collection {
- collection.dbContextKey = key
- return collection
- }
- // RegisterSEOByNames register mutiple seo by names
- func (collection *Collection) RegisterSEOByNames(names ...string) *Collection {
- for index := range names {
- collection.registeredSEO = append(collection.registeredSEO, &SEO{name: names[index]})
- }
- return collection
- }
- // RegisterSEO register a seo
- func (collection *Collection) RegisterSEO(obj interface{}) (seo *SEO) {
- if name, ok := obj.(string); ok {
- seo = &SEO{name: name}
- } else {
- typ := reflect.Indirect(reflect.ValueOf(obj)).Type()
- seo = &SEO{name: typ.Name(), modelTyp: typ}
- }
- collection.registeredSEO = append(collection.registeredSEO, seo)
- return
- }
- // RegisterSEO remove a seo
- func (collection *Collection) RemoveSEO(obj interface{}) *Collection {
- var name string
- if n, ok := obj.(string); ok {
- name = n
- } else {
- name = reflect.Indirect(reflect.ValueOf(obj)).Type().Name()
- }
- for index, s := range collection.registeredSEO {
- if s.name == name {
- collection.registeredSEO = append(collection.registeredSEO[:index], collection.registeredSEO[index+1:]...)
- break
- }
- }
- return collection
- }
- // GetSEO get a Seo
- func (collection *Collection) GetSEO(obj interface{}) *SEO {
- if name, ok := obj.(string); ok {
- return collection.GetSEOByName(name)
- } else {
- return collection.GetSEOByModel(obj)
- }
- }
- // GetSEO get a Seo by name
- func (collection *Collection) GetSEOByName(name string) *SEO {
- for _, s := range collection.registeredSEO {
- if s.name == name {
- return s
- }
- }
- return nil
- }
- // GetSEOByModel get a seo by model
- func (collection *Collection) GetSEOByModel(model interface{}) *SEO {
- for _, s := range collection.registeredSEO {
- if reflect.Indirect(reflect.ValueOf(model)).Type() == s.modelTyp {
- return s
- }
- }
- return nil
- }
- // AfterSave set the hook called after saving
- func (collection *Collection) AfterSave(v func(ctx context.Context, settingName string, locale string) error) *Collection {
- collection.afterSave = v
- return collection
- }
- // RenderGlobal render global seo
- func (collection Collection) RenderGlobal(req *http.Request) h.HTMLComponent {
- return collection.Render(collection.globalName, req)
- }
- // Render render seo tags
- func (collection Collection) Render(obj interface{}, req *http.Request) h.HTMLComponent {
- var (
- db = collection.getDBFromContext(req.Context())
- sortedSEOs []*SEO
- sortedSeoNames []string
- sortedDBSettings []QorSEOSettingInterface
- sortedSettings []Setting
- setting Setting
- locale string
- )
- // sort all SEOs
- globalSeo := collection.GetSEO(collection.globalName)
- if globalSeo == nil {
- return h.RawHTML("")
- }
- sortedSEOs = append(sortedSEOs, globalSeo)
- if name, ok := obj.(string); !ok || name != collection.globalName {
- if seo := collection.GetSEO(obj); seo != nil {
- sortedSeoNames = append(sortedSeoNames, seo.name)
- sortedSEOs = append(sortedSEOs, seo)
- }
- }
- sortedSeoNames = append(sortedSeoNames, globalSeo.name)
- if v, ok := obj.(l10n.L10nInterface); ok {
- locale = v.GetLocale()
- }
- // sort all QorSEOSettingInterface
- var settingModelSlice = collection.NewSettingModelSlice()
- if db.Find(settingModelSlice, "name in (?) AND locale_code = ?", sortedSeoNames, locale).Error != nil {
- return h.RawHTML("")
- }
- reflectVlaue := reflect.Indirect(reflect.ValueOf(settingModelSlice))
- for _, name := range sortedSeoNames {
- for i := 0; i < reflectVlaue.Len(); i++ {
- if modelSetting, ok := reflectVlaue.Index(i).Interface().(QorSEOSettingInterface); ok && modelSetting.GetName() == name {
- sortedDBSettings = append(sortedDBSettings, modelSetting)
- }
- }
- }
- // sort all settings
- if _, ok := obj.(string); !ok {
- if value := reflect.Indirect(reflect.ValueOf(obj)); value.IsValid() && value.Kind() == reflect.Struct {
- for i := 0; i < value.NumField(); i++ {
- if value.Field(i).Type() == reflect.TypeOf(Setting{}) {
- if setting := value.Field(i).Interface().(Setting); setting.EnabledCustomize {
- sortedSettings = append(sortedSettings, setting)
- }
- break
- }
- }
- }
- }
- for _, s := range sortedDBSettings {
- sortedSettings = append(sortedSettings, s.GetSEOSetting())
- }
- // get the final setting from sortedSettings
- for i, s := range sortedSettings {
- if !collection.inherited && i >= 1 {
- break
- }
- if s.Title != "" && setting.Title == "" {
- setting.Title = s.Title
- }
- if s.Description != "" && setting.Description == "" {
- setting.Description = s.Description
- }
- if s.Keywords != "" && setting.Keywords == "" {
- setting.Keywords = s.Keywords
- }
- if s.OpenGraphURL != "" && setting.OpenGraphURL == "" {
- setting.OpenGraphURL = s.OpenGraphURL
- }
- if s.OpenGraphType != "" && setting.OpenGraphType == "" {
- setting.OpenGraphType = s.OpenGraphType
- }
- if s.OpenGraphImageURL != "" && setting.OpenGraphImageURL == "" {
- setting.OpenGraphImageURL = s.OpenGraphImageURL
- }
- if s.OpenGraphImageFromMediaLibrary.URL("og") != "" && setting.OpenGraphImageURL == "" {
- setting.OpenGraphImageURL = s.OpenGraphImageFromMediaLibrary.URL("og")
- }
- if len(s.OpenGraphMetadata) > 0 && len(setting.OpenGraphMetadata) == 0 {
- setting.OpenGraphMetadata = s.OpenGraphMetadata
- }
- }
- if setting.OpenGraphURL != "" && !isAbsoluteURL(setting.OpenGraphURL) {
- var u url.URL
- u.Host = req.Host
- if req.URL.Scheme != "" {
- u.Scheme = req.URL.Scheme
- } else {
- u.Scheme = "http"
- }
- setting.OpenGraphURL = path.Join(u.String(), setting.OpenGraphURL)
- }
- // fetch all variables and tags from context
- var (
- variables = map[string]string{}
- tags = map[string]string{}
- )
- for _, s := range sortedDBSettings {
- for key, val := range s.GetVariables() {
- variables[key] = val
- }
- }
- for i, seo := range sortedSEOs {
- for key, f := range seo.contextVariables {
- value := f(obj, &setting, req)
- if strings.Contains(key, ":") && collection.inherited {
- tags[key] = value
- } else if strings.Contains(key, ":") && !collection.inherited && i == 0 {
- tags[key] = value
- } else {
- variables[key] = f(obj, &setting, req)
- }
- }
- }
- setting = replaceVariables(setting, variables)
- return setting.HTMLComponent(tags)
- }
- // GetDB get db from context
- func (collection Collection) getDBFromContext(ctx context.Context) *gorm.DB {
- if contextdb := ctx.Value(collection.dbContextKey); contextdb != nil {
- return contextdb.(*gorm.DB)
- }
- return GlobalDB
- }
- var regex = regexp.MustCompile("{{([a-zA-Z0-9]*)}}")
- func replaceVariables(setting Setting, values map[string]string) Setting {
- replace := func(str string) string {
- matches := regex.FindAllStringSubmatch(str, -1)
- for _, match := range matches {
- str = strings.Replace(str, match[0], values[match[1]], 1)
- }
- return str
- }
- setting.Title = replace(setting.Title)
- setting.Description = replace(setting.Description)
- setting.Keywords = replace(setting.Keywords)
- return setting
- }
- func isAbsoluteURL(str string) bool {
- if u, err := url.Parse(str); err == nil && u.IsAbs() {
- return true
- }
- return false
- }
- func ContextWithDB(ctx context.Context, db *gorm.DB) context.Context {
- return context.WithValue(ctx, DBContextKey, db)
- }
|