collection.go 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408
  1. package seo
  2. import (
  3. "context"
  4. "net/http"
  5. "net/url"
  6. "path"
  7. "reflect"
  8. "regexp"
  9. "strings"
  10. "github.com/qor5/admin/l10n"
  11. h "github.com/theplant/htmlgo"
  12. "gorm.io/gorm"
  13. )
  14. var (
  15. GlobalSEO = "Global SEO"
  16. GlobalDB *gorm.DB
  17. DBContextKey contextKey = "DB"
  18. )
  19. type (
  20. contextKey string
  21. contextVariablesFunc func(interface{}, *Setting, *http.Request) string
  22. )
  23. // Create a SeoCollection instance
  24. func NewCollection() *Collection {
  25. collection := &Collection{
  26. settingModel: &QorSEOSetting{},
  27. dbContextKey: DBContextKey,
  28. globalName: GlobalSEO,
  29. inherited: true,
  30. }
  31. collection.RegisterSEO(GlobalSEO).RegisterSettingVaribles(struct{ SiteName string }{}).
  32. RegisterContextVariables(
  33. "og:url", func(_ interface{}, _ *Setting, req *http.Request) string {
  34. return req.URL.String()
  35. },
  36. )
  37. return collection
  38. }
  39. // Collection will hold registered seo configures and global setting definition and other configures
  40. // @snippet_begin(SeoCollectionDefinition)
  41. type Collection struct {
  42. registeredSEO []*SEO
  43. globalName string //default name is GlobalSEO
  44. inherited bool //default is true. the order is model seo setting, system seo setting, global seo setting
  45. dbContextKey interface{} // get db from context
  46. settingModel interface{} // db model
  47. afterSave func(ctx context.Context, settingName string, locale string) error // hook called after saving
  48. }
  49. // @snippet_end
  50. // SEO represents a seo object for a page
  51. // @snippet_begin(SeoDefinition)
  52. type SEO struct {
  53. name string
  54. modelTyp reflect.Type
  55. contextVariables map[string]contextVariablesFunc // fetch context variables from request
  56. settingVariables interface{} // fetch setting variables from db
  57. }
  58. // @snippet_end
  59. // RegisterModel register a model to seo
  60. func (seo *SEO) SetModel(model interface{}) *SEO {
  61. seo.modelTyp = reflect.Indirect(reflect.ValueOf(model)).Type()
  62. return seo
  63. }
  64. // SetName set seo name
  65. func (seo *SEO) SetName(name string) *SEO {
  66. seo.name = name
  67. return seo
  68. }
  69. // RegisterContextVariables register context variables. the registered variables will be rendered to the page
  70. func (seo *SEO) RegisterContextVariables(key string, f contextVariablesFunc) *SEO {
  71. if seo.contextVariables == nil {
  72. seo.contextVariables = map[string]contextVariablesFunc{}
  73. }
  74. seo.contextVariables[key] = f
  75. return seo
  76. }
  77. // RegisterSettingVaribles register a setting variable
  78. func (seo *SEO) RegisterSettingVaribles(setting interface{}) *SEO {
  79. seo.settingVariables = setting
  80. return seo
  81. }
  82. func (collection *Collection) SetGlobalName(name string) *Collection {
  83. collection.globalName = name
  84. if globalSeo := collection.GetSEOByName(GlobalSEO); globalSeo != nil {
  85. globalSeo.SetName(name)
  86. }
  87. return collection
  88. }
  89. func (collection *Collection) NewSettingModelInstance() interface{} {
  90. return reflect.New(reflect.Indirect(reflect.ValueOf(collection.settingModel)).Type()).Interface()
  91. }
  92. func (collection *Collection) NewSettingModelSlice() interface{} {
  93. sliceType := reflect.SliceOf(reflect.PtrTo(reflect.Indirect(reflect.ValueOf(collection.settingModel)).Type()))
  94. slice := reflect.New(sliceType)
  95. slice.Elem().Set(reflect.MakeSlice(sliceType, 0, 0))
  96. return slice.Interface()
  97. }
  98. // RegisterVariblesSetting register variables setting
  99. func (collection *Collection) SetInherited(b bool) *Collection {
  100. collection.inherited = b
  101. return collection
  102. }
  103. // RegisterVariblesSetting register variables setting
  104. func (collection *Collection) SetSettingModel(s interface{}) *Collection {
  105. collection.settingModel = s
  106. return collection
  107. }
  108. // RegisterDBContextKey register a key to get db from context
  109. func (collection *Collection) SetDBContextKey(key interface{}) *Collection {
  110. collection.dbContextKey = key
  111. return collection
  112. }
  113. // RegisterSEOByNames register mutiple seo by names
  114. func (collection *Collection) RegisterSEOByNames(names ...string) *Collection {
  115. for index := range names {
  116. collection.registeredSEO = append(collection.registeredSEO, &SEO{name: names[index]})
  117. }
  118. return collection
  119. }
  120. // RegisterSEO register a seo
  121. func (collection *Collection) RegisterSEO(obj interface{}) (seo *SEO) {
  122. if name, ok := obj.(string); ok {
  123. seo = &SEO{name: name}
  124. } else {
  125. typ := reflect.Indirect(reflect.ValueOf(obj)).Type()
  126. seo = &SEO{name: typ.Name(), modelTyp: typ}
  127. }
  128. collection.registeredSEO = append(collection.registeredSEO, seo)
  129. return
  130. }
  131. // RegisterSEO remove a seo
  132. func (collection *Collection) RemoveSEO(obj interface{}) *Collection {
  133. var name string
  134. if n, ok := obj.(string); ok {
  135. name = n
  136. } else {
  137. name = reflect.Indirect(reflect.ValueOf(obj)).Type().Name()
  138. }
  139. for index, s := range collection.registeredSEO {
  140. if s.name == name {
  141. collection.registeredSEO = append(collection.registeredSEO[:index], collection.registeredSEO[index+1:]...)
  142. break
  143. }
  144. }
  145. return collection
  146. }
  147. // GetSEO get a Seo
  148. func (collection *Collection) GetSEO(obj interface{}) *SEO {
  149. if name, ok := obj.(string); ok {
  150. return collection.GetSEOByName(name)
  151. } else {
  152. return collection.GetSEOByModel(obj)
  153. }
  154. }
  155. // GetSEO get a Seo by name
  156. func (collection *Collection) GetSEOByName(name string) *SEO {
  157. for _, s := range collection.registeredSEO {
  158. if s.name == name {
  159. return s
  160. }
  161. }
  162. return nil
  163. }
  164. // GetSEOByModel get a seo by model
  165. func (collection *Collection) GetSEOByModel(model interface{}) *SEO {
  166. for _, s := range collection.registeredSEO {
  167. if reflect.Indirect(reflect.ValueOf(model)).Type() == s.modelTyp {
  168. return s
  169. }
  170. }
  171. return nil
  172. }
  173. // AfterSave set the hook called after saving
  174. func (collection *Collection) AfterSave(v func(ctx context.Context, settingName string, locale string) error) *Collection {
  175. collection.afterSave = v
  176. return collection
  177. }
  178. // RenderGlobal render global seo
  179. func (collection Collection) RenderGlobal(req *http.Request) h.HTMLComponent {
  180. return collection.Render(collection.globalName, req)
  181. }
  182. // Render render seo tags
  183. func (collection Collection) Render(obj interface{}, req *http.Request) h.HTMLComponent {
  184. var (
  185. db = collection.getDBFromContext(req.Context())
  186. sortedSEOs []*SEO
  187. sortedSeoNames []string
  188. sortedDBSettings []QorSEOSettingInterface
  189. sortedSettings []Setting
  190. setting Setting
  191. locale string
  192. )
  193. // sort all SEOs
  194. globalSeo := collection.GetSEO(collection.globalName)
  195. if globalSeo == nil {
  196. return h.RawHTML("")
  197. }
  198. sortedSEOs = append(sortedSEOs, globalSeo)
  199. if name, ok := obj.(string); !ok || name != collection.globalName {
  200. if seo := collection.GetSEO(obj); seo != nil {
  201. sortedSeoNames = append(sortedSeoNames, seo.name)
  202. sortedSEOs = append(sortedSEOs, seo)
  203. }
  204. }
  205. sortedSeoNames = append(sortedSeoNames, globalSeo.name)
  206. if v, ok := obj.(l10n.L10nInterface); ok {
  207. locale = v.GetLocale()
  208. }
  209. // sort all QorSEOSettingInterface
  210. var settingModelSlice = collection.NewSettingModelSlice()
  211. if db.Find(settingModelSlice, "name in (?) AND locale_code = ?", sortedSeoNames, locale).Error != nil {
  212. return h.RawHTML("")
  213. }
  214. reflectVlaue := reflect.Indirect(reflect.ValueOf(settingModelSlice))
  215. for _, name := range sortedSeoNames {
  216. for i := 0; i < reflectVlaue.Len(); i++ {
  217. if modelSetting, ok := reflectVlaue.Index(i).Interface().(QorSEOSettingInterface); ok && modelSetting.GetName() == name {
  218. sortedDBSettings = append(sortedDBSettings, modelSetting)
  219. }
  220. }
  221. }
  222. // sort all settings
  223. if _, ok := obj.(string); !ok {
  224. if value := reflect.Indirect(reflect.ValueOf(obj)); value.IsValid() && value.Kind() == reflect.Struct {
  225. for i := 0; i < value.NumField(); i++ {
  226. if value.Field(i).Type() == reflect.TypeOf(Setting{}) {
  227. if setting := value.Field(i).Interface().(Setting); setting.EnabledCustomize {
  228. sortedSettings = append(sortedSettings, setting)
  229. }
  230. break
  231. }
  232. }
  233. }
  234. }
  235. for _, s := range sortedDBSettings {
  236. sortedSettings = append(sortedSettings, s.GetSEOSetting())
  237. }
  238. // get the final setting from sortedSettings
  239. for i, s := range sortedSettings {
  240. if !collection.inherited && i >= 1 {
  241. break
  242. }
  243. if s.Title != "" && setting.Title == "" {
  244. setting.Title = s.Title
  245. }
  246. if s.Description != "" && setting.Description == "" {
  247. setting.Description = s.Description
  248. }
  249. if s.Keywords != "" && setting.Keywords == "" {
  250. setting.Keywords = s.Keywords
  251. }
  252. if s.OpenGraphTitle != "" && setting.OpenGraphTitle == "" {
  253. setting.OpenGraphTitle = s.OpenGraphTitle
  254. }
  255. if s.OpenGraphDescription != "" && setting.OpenGraphDescription == "" {
  256. setting.OpenGraphDescription = s.OpenGraphDescription
  257. }
  258. if s.OpenGraphURL != "" && setting.OpenGraphURL == "" {
  259. setting.OpenGraphURL = s.OpenGraphURL
  260. }
  261. if s.OpenGraphType != "" && setting.OpenGraphType == "" {
  262. setting.OpenGraphType = s.OpenGraphType
  263. }
  264. if s.OpenGraphImageURL != "" && setting.OpenGraphImageURL == "" {
  265. setting.OpenGraphImageURL = s.OpenGraphImageURL
  266. }
  267. if s.OpenGraphImageFromMediaLibrary.URL("og") != "" && setting.OpenGraphImageURL == "" {
  268. setting.OpenGraphImageURL = s.OpenGraphImageFromMediaLibrary.URL("og")
  269. }
  270. if len(s.OpenGraphMetadata) > 0 && len(setting.OpenGraphMetadata) == 0 {
  271. setting.OpenGraphMetadata = s.OpenGraphMetadata
  272. }
  273. }
  274. if setting.OpenGraphURL != "" && !isAbsoluteURL(setting.OpenGraphURL) {
  275. var u url.URL
  276. u.Host = req.Host
  277. if req.URL.Scheme != "" {
  278. u.Scheme = req.URL.Scheme
  279. } else {
  280. u.Scheme = "http"
  281. }
  282. setting.OpenGraphURL = path.Join(u.String(), setting.OpenGraphURL)
  283. }
  284. // fetch all variables and tags from context
  285. var (
  286. variables = map[string]string{}
  287. tags = map[string]string{}
  288. )
  289. for _, s := range sortedDBSettings {
  290. for key, val := range s.GetVariables() {
  291. variables[key] = val
  292. }
  293. }
  294. for i, seo := range sortedSEOs {
  295. for key, f := range seo.contextVariables {
  296. value := f(obj, &setting, req)
  297. if strings.Contains(key, ":") && collection.inherited {
  298. tags[key] = value
  299. } else if strings.Contains(key, ":") && !collection.inherited && i == 0 {
  300. tags[key] = value
  301. } else {
  302. variables[key] = f(obj, &setting, req)
  303. }
  304. }
  305. }
  306. setting = replaceVariables(setting, variables)
  307. return setting.HTMLComponent(tags)
  308. }
  309. // GetDB get db from context
  310. func (collection Collection) getDBFromContext(ctx context.Context) *gorm.DB {
  311. if contextdb := ctx.Value(collection.dbContextKey); contextdb != nil {
  312. return contextdb.(*gorm.DB)
  313. }
  314. return GlobalDB
  315. }
  316. var regex = regexp.MustCompile("{{([a-zA-Z0-9]*)}}")
  317. func replaceVariables(setting Setting, values map[string]string) Setting {
  318. replace := func(str string) string {
  319. matches := regex.FindAllStringSubmatch(str, -1)
  320. for _, match := range matches {
  321. str = strings.Replace(str, match[0], values[match[1]], 1)
  322. }
  323. return str
  324. }
  325. setting.Title = replace(setting.Title)
  326. setting.Description = replace(setting.Description)
  327. setting.Keywords = replace(setting.Keywords)
  328. setting.OpenGraphTitle = replace(setting.OpenGraphTitle)
  329. setting.OpenGraphDescription = replace(setting.OpenGraphDescription)
  330. setting.OpenGraphURL = replace(setting.OpenGraphURL)
  331. setting.OpenGraphType = replace(setting.OpenGraphType)
  332. setting.OpenGraphImageURL = replace(setting.OpenGraphImageURL)
  333. var metadata []OpenGraphMetadata
  334. for _, m := range setting.OpenGraphMetadata {
  335. metadata = append(metadata, OpenGraphMetadata{
  336. Property: m.Property,
  337. Content: replace(m.Content),
  338. })
  339. }
  340. setting.OpenGraphMetadata = metadata
  341. return setting
  342. }
  343. func isAbsoluteURL(str string) bool {
  344. if u, err := url.Parse(str); err == nil && u.IsAbs() {
  345. return true
  346. }
  347. return false
  348. }
  349. func ContextWithDB(ctx context.Context, db *gorm.DB) context.Context {
  350. return context.WithValue(ctx, DBContextKey, db)
  351. }