admin.go 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443
  1. package seo
  2. import (
  3. "encoding/json"
  4. "errors"
  5. "fmt"
  6. "net/http"
  7. "reflect"
  8. "strings"
  9. "github.com/qor5/admin/l10n"
  10. "github.com/qor5/admin/media"
  11. "github.com/qor5/admin/media/media_library"
  12. "github.com/qor5/admin/media/views"
  13. "github.com/qor5/admin/presets"
  14. . "github.com/qor5/ui/vuetify"
  15. "github.com/qor5/web"
  16. "github.com/qor5/x/i18n"
  17. "github.com/qor5/x/perm"
  18. "github.com/sunfmin/reflectutils"
  19. h "github.com/theplant/htmlgo"
  20. "golang.org/x/text/language"
  21. "gorm.io/gorm"
  22. )
  23. const (
  24. saveEvent = "seo_save_collection"
  25. I18nSeoKey i18n.ModuleKey = "I18nSeoKey"
  26. )
  27. var permVerifier *perm.Verifier
  28. func (collection *Collection) Configure(b *presets.Builder, db *gorm.DB) {
  29. if err := db.AutoMigrate(collection.settingModel); err != nil {
  30. panic(err)
  31. }
  32. if GlobalDB == nil {
  33. GlobalDB = db
  34. }
  35. b.GetWebBuilder().RegisterEventFunc(saveEvent, collection.save)
  36. b.Model(collection.settingModel).PrimaryField("Name").Label("SEO").Listing().PageFunc(collection.pageFunc)
  37. b.FieldDefaults(presets.WRITE).
  38. FieldType(Setting{}).
  39. ComponentFunc(collection.EditingComponentFunc).
  40. SetterFunc(EditSetterFunc)
  41. b.I18n().
  42. RegisterForModule(language.English, I18nSeoKey, Messages_en_US).
  43. RegisterForModule(language.SimplifiedChinese, I18nSeoKey, Messages_zh_CN)
  44. b.ExtraAsset("/vue-seo.js", "text/javascript", SeoJSComponentsPack())
  45. permVerifier = perm.NewVerifier("seo", b.GetPermission())
  46. }
  47. func EditSetterFunc(obj interface{}, field *presets.FieldContext, ctx *web.EventContext) (err error) {
  48. var setting Setting
  49. var mediaBox = media_library.MediaBox{}
  50. for fieldWithPrefix := range ctx.R.Form {
  51. // make sure OpenGraphImageFromMediaLibrary.Description set after OpenGraphImageFromMediaLibrary.Values
  52. if fieldWithPrefix == fmt.Sprintf("%s.%s", field.Name, "OpenGraphImageFromMediaLibrary.Values") {
  53. err = mediaBox.Scan(ctx.R.FormValue(fieldWithPrefix))
  54. if err != nil {
  55. return
  56. }
  57. break
  58. }
  59. }
  60. for fieldWithPrefix := range ctx.R.Form {
  61. if strings.HasPrefix(fieldWithPrefix, fmt.Sprintf("%s.%s", field.Name, "OpenGraphImageFromMediaLibrary")) {
  62. if fieldWithPrefix == fmt.Sprintf("%s.%s", field.Name, "OpenGraphImageFromMediaLibrary.Description") {
  63. mediaBox.Description = ctx.R.Form.Get(fieldWithPrefix)
  64. reflectutils.Set(&setting, "OpenGraphImageFromMediaLibrary", mediaBox)
  65. }
  66. continue
  67. }
  68. if fieldWithPrefix == fmt.Sprintf("%s.%s", field.Name, "OpenGraphMetadataString") {
  69. metadata := GetOpenGraphMetadata(ctx.R.Form.Get(fieldWithPrefix))
  70. reflectutils.Set(&setting, "OpenGraphMetadata", metadata)
  71. continue
  72. }
  73. if strings.HasPrefix(fieldWithPrefix, fmt.Sprintf("%s.", field.Name)) {
  74. reflectutils.Set(&setting, strings.TrimPrefix(fieldWithPrefix, fmt.Sprintf("%s.", field.Name)), ctx.R.Form.Get(fieldWithPrefix))
  75. }
  76. }
  77. return reflectutils.Set(obj, field.Name, setting)
  78. }
  79. func (collection *Collection) EditingComponentFunc(obj interface{}, field *presets.FieldContext, ctx *web.EventContext) h.HTMLComponent {
  80. var (
  81. msgr = i18n.MustGetModuleMessages(ctx.R, I18nSeoKey, Messages_en_US).(*Messages)
  82. fieldPrefix string
  83. setting Setting
  84. db = collection.getDBFromContext(ctx.R.Context())
  85. locale, _ = l10n.IsLocalizableFromCtx(ctx.R.Context())
  86. )
  87. seo := collection.GetSEOByModel(obj)
  88. if seo == nil {
  89. return h.Div()
  90. }
  91. value := reflect.Indirect(reflect.ValueOf(obj))
  92. for i := 0; i < value.NumField(); i++ {
  93. if s, ok := value.Field(i).Interface().(Setting); ok {
  94. setting = s
  95. fieldPrefix = value.Type().Field(i).Name
  96. }
  97. }
  98. if setting.IsEmpty() {
  99. modelSetting := collection.NewSettingModelInstance().(QorSEOSettingInterface)
  100. db.Where("name = ? AND locale_code = ?", seo.name, locale).First(modelSetting)
  101. setting.Title = modelSetting.GetTitle()
  102. setting.Description = modelSetting.GetDescription()
  103. setting.Keywords = modelSetting.GetKeywords()
  104. setting.OpenGraphTitle = modelSetting.GetOpenGraphTitle()
  105. setting.OpenGraphDescription = modelSetting.GetOpenGraphDescription()
  106. setting.OpenGraphURL = modelSetting.GetOpenGraphURL()
  107. setting.OpenGraphType = modelSetting.GetOpenGraphType()
  108. setting.OpenGraphImageURL = modelSetting.GetOpenGraphImageURL()
  109. setting.OpenGraphImageFromMediaLibrary = modelSetting.GetOpenGraphImageFromMediaLibrary()
  110. setting.OpenGraphMetadata = modelSetting.GetOpenGraphMetadata()
  111. }
  112. openCustomizePanel := "1"
  113. hideActions := false
  114. o := ctx.R.FormValue("openCustomizePanel")
  115. if setting.EnabledCustomize && o != "" {
  116. openCustomizePanel = o
  117. }
  118. if o != "" {
  119. hideActions = true
  120. }
  121. return web.Scope(
  122. h.Div(
  123. h.Label(msgr.Seo).Class("v-label theme--light"),
  124. VExpansionPanels(
  125. VExpansionPanel(
  126. VExpansionPanelHeader(
  127. h.HTMLComponents{
  128. VSwitch().Label(msgr.UseDefaults).Attr("v-model", "locals.userDefaults").On("change", "locals.enabledCustomize = !locals.userDefaults;$refs.customize.$emit('change', locals.enabledCustomize);if((locals.openCustomizePanel=='0'&&locals.enabledCustomize)||(locals.openCustomizePanel!='0'&&!locals.enabledCustomize)){event.stopPropagation();}"),
  129. VSwitch().FieldName(fmt.Sprintf("%s.%s", fieldPrefix, "EnabledCustomize")).Value(setting.EnabledCustomize).Attr(":input-value", "locals.enabledCustomize").Attr("ref", "customize").Attr("style", "display:none;"),
  130. },
  131. ).Attr("style", "padding: 0px 24px;").HideActions(hideActions),
  132. VExpansionPanelContent(
  133. VCardText(
  134. collection.vseo(fieldPrefix, seo, &setting, ctx.R),
  135. ),
  136. ).Eager(true),
  137. ),
  138. ).Flat(true).Attr("v-model", "locals.openCustomizePanel"),
  139. ).Class("pb-4"),
  140. ).Init(fmt.Sprintf(`{enabledCustomize: %t, userDefaults: %t, openCustomizePanel: %s, }`, setting.EnabledCustomize, !setting.EnabledCustomize, openCustomizePanel)).
  141. VSlot("{ locals }")
  142. }
  143. func (collection *Collection) pageFunc(ctx *web.EventContext) (_ web.PageResponse, err error) {
  144. var (
  145. msgr = i18n.MustGetModuleMessages(ctx.R, I18nSeoKey, Messages_en_US).(*Messages)
  146. db = collection.getDBFromContext(ctx.R.Context())
  147. locale, _ = l10n.IsLocalizableFromCtx(ctx.R.Context())
  148. )
  149. var seoComponents h.HTMLComponents
  150. for _, seo := range collection.registeredSEO {
  151. modelSetting := collection.NewSettingModelInstance().(QorSEOSettingInterface)
  152. err := db.Where("name = ? AND locale_code = ?", seo.name, locale).First(modelSetting).Error
  153. if errors.Is(err, gorm.ErrRecordNotFound) {
  154. modelSetting.SetName(seo.name)
  155. modelSetting.SetLocale(locale)
  156. if err := db.Save(modelSetting).Error; err != nil {
  157. panic(err)
  158. }
  159. }
  160. var variablesComps h.HTMLComponents
  161. if seo.settingVariables != nil {
  162. variables := reflect.Indirect(reflect.New(reflect.Indirect(reflect.ValueOf(seo.settingVariables)).Type()))
  163. variableValues := modelSetting.GetVariables()
  164. for i := 0; i < variables.NumField(); i++ {
  165. fieldName := variables.Type().Field(i).Name
  166. if variableValues[fieldName] != "" {
  167. fmt.Println(fieldName, variableValues[fieldName])
  168. variables.Field(i).Set(reflect.ValueOf(variableValues[fieldName]))
  169. }
  170. }
  171. if variables.Type().NumField() > 0 {
  172. variablesComps = append(variablesComps, h.H3(msgr.Variable).Style("margin-top:15px;font-weight: 500"))
  173. }
  174. for i := 0; i < variables.Type().NumField(); i++ {
  175. field := variables.Type().Field(i)
  176. variablesComps = append(variablesComps, VTextField().FieldName(fmt.Sprintf("%s.Variables.%s", seo.name, field.Name)).Label(i18n.PT(ctx.R, presets.ModelsI18nModuleKey, "Seo Variable", field.Name)).Value(variables.Field(i).String()))
  177. }
  178. }
  179. var (
  180. label string
  181. setting = modelSetting.GetSEOSetting()
  182. loadingName = strings.ReplaceAll(strings.ToLower(seo.name), " ", "_")
  183. )
  184. if seo.name == collection.globalName {
  185. label = msgr.GlobalName
  186. } else {
  187. label = i18n.PT(ctx.R, presets.ModelsI18nModuleKey, "Seo", seo.name)
  188. }
  189. comp := VExpansionPanel(
  190. VExpansionPanelHeader(h.H4(label).Style("font-weight: 500;")),
  191. VExpansionPanelContent(
  192. VCardText(
  193. variablesComps,
  194. collection.vseo(modelSetting.GetName(), seo, &setting, ctx.R),
  195. ),
  196. VCardActions(
  197. VSpacer(),
  198. VBtn(msgr.Save).Bind("loading", fmt.Sprintf("vars.%s", loadingName)).Color("primary").Large(true).
  199. Attr("@click", web.Plaid().
  200. EventFunc(saveEvent).
  201. Query("name", seo.name).
  202. Query("loadingName", loadingName).
  203. BeforeScript(fmt.Sprintf("vars.%s = true;", loadingName)).Go()),
  204. ).Attr(web.InitContextVars, fmt.Sprintf(`{%s: false}`, loadingName)),
  205. ),
  206. )
  207. seoComponents = append(seoComponents, comp)
  208. }
  209. return web.PageResponse{
  210. PageTitle: msgr.PageTitle,
  211. Body: h.If(editIsAllowed(ctx.R) == nil, VContainer(
  212. VSnackbar(h.Text(msgr.SavedSuccessfully)).
  213. Attr("v-model", "vars.seoSnackbarShow").
  214. Top(true).
  215. Color("primary").
  216. Timeout(2000),
  217. VRow(
  218. VCol(
  219. VContainer(
  220. h.H3(msgr.PageMetadataTitle).Attr("style", "font-weight: 500"),
  221. h.P().Text(msgr.PageMetadataDescription)),
  222. ).Cols(3),
  223. VCol(
  224. VExpansionPanels(
  225. seoComponents,
  226. ).Focusable(true),
  227. ).Cols(9),
  228. ),
  229. ).Attr("style", "background-color: #f5f5f5;max-width:100%").Attr(web.InitContextVars, `{seoSnackbarShow: false}`)),
  230. }, nil
  231. }
  232. func (collection *Collection) vseo(fieldPrefix string, seo *SEO, setting *Setting, req *http.Request) h.HTMLComponent {
  233. var (
  234. seos []*SEO
  235. msgr = i18n.MustGetModuleMessages(req, I18nSeoKey, Messages_en_US).(*Messages)
  236. db = collection.getDBFromContext(req.Context())
  237. )
  238. if seo.name == collection.globalName {
  239. seos = append(seos, seo)
  240. } else {
  241. seos = append(seos, collection.GetSEO(collection.globalName), seo)
  242. }
  243. var (
  244. variablesEle []h.HTMLComponent
  245. variables []string
  246. )
  247. for _, seo := range seos {
  248. if seo.settingVariables != nil {
  249. value := reflect.Indirect(reflect.ValueOf(seo.settingVariables)).Type()
  250. for i := 0; i < value.NumField(); i++ {
  251. fieldName := value.Field(i).Name
  252. variables = append(variables, fieldName)
  253. }
  254. }
  255. for key := range seo.contextVariables {
  256. if !strings.Contains(key, ":") {
  257. variables = append(variables, key)
  258. }
  259. }
  260. }
  261. for _, variable := range variables {
  262. variablesEle = append(variablesEle, VCol(
  263. VBtn("").Width(100).Icon(true).Attr("style", "text-transform: none").Children(VIcon("add_box"), h.Text(i18n.PT(req, presets.ModelsI18nModuleKey, "Seo Variable", variable))).Attr("@click", fmt.Sprintf("$refs.seo.addTags('%s')", variable)),
  264. ).Cols(2))
  265. }
  266. image := &setting.OpenGraphImageFromMediaLibrary
  267. if image.ID.String() == "0" {
  268. image.ID = json.Number("")
  269. }
  270. refPrefix := strings.ReplaceAll(strings.ToLower(fieldPrefix), " ", "_")
  271. return VSeo(
  272. h.H4(msgr.Basic).Style("margin-top:15px;font-weight: 500"),
  273. VRow(
  274. variablesEle...,
  275. ),
  276. VCard(
  277. VCardText(
  278. VTextField().Counter(65).FieldName(fmt.Sprintf("%s.%s", fieldPrefix, "Title")).Label(msgr.Title).Value(setting.Title).Attr("@focus", fmt.Sprintf("$refs.seo.tagInputsFocus($refs.%s)", fmt.Sprintf("%s_title", refPrefix))).Attr("ref", fmt.Sprintf("%s_title", refPrefix)),
  279. VTextField().Counter(150).FieldName(fmt.Sprintf("%s.%s", fieldPrefix, "Description")).Label(msgr.Description).Value(setting.Description).Attr("@focus", fmt.Sprintf("$refs.seo.tagInputsFocus($refs.%s)", fmt.Sprintf("%s_description", refPrefix))).Attr("ref", fmt.Sprintf("%s_description", refPrefix)),
  280. VTextarea().Counter(255).Rows(2).AutoGrow(true).FieldName(fmt.Sprintf("%s.%s", fieldPrefix, "Keywords")).Label(msgr.Keywords).Value(setting.Keywords).Attr("@focus", fmt.Sprintf("$refs.seo.tagInputsFocus($refs.%s)", fmt.Sprintf("%s_keywords", refPrefix))).Attr("ref", fmt.Sprintf("%s_keywords", refPrefix)),
  281. ),
  282. ).Outlined(true).Flat(true),
  283. h.H4(msgr.OpenGraphInformation).Style("margin-top:15px;margin-bottom:15px;font-weight: 500"),
  284. VCard(
  285. VCardText(
  286. VRow(
  287. VCol(VTextField().FieldName(fmt.Sprintf("%s.%s", fieldPrefix, "OpenGraphTitle")).Label(msgr.OpenGraphTitle).Value(setting.OpenGraphTitle).Attr("@focus", fmt.Sprintf("$refs.seo.tagInputsFocus($refs.%s)", fmt.Sprintf("%s_og_title", refPrefix))).Attr("ref", fmt.Sprintf("%s_og_title", refPrefix))).Cols(6),
  288. VCol(VTextField().FieldName(fmt.Sprintf("%s.%s", fieldPrefix, "OpenGraphDescription")).Label(msgr.OpenGraphDescription).Value(setting.OpenGraphDescription).Attr("@focus", fmt.Sprintf("$refs.seo.tagInputsFocus($refs.%s)", fmt.Sprintf("%s_og_description", refPrefix))).Attr("ref", fmt.Sprintf("%s_og_description", refPrefix))).Cols(6),
  289. ),
  290. VRow(
  291. VCol(VTextField().FieldName(fmt.Sprintf("%s.%s", fieldPrefix, "OpenGraphURL")).Label(msgr.OpenGraphURL).Value(setting.OpenGraphURL).Attr("@focus", fmt.Sprintf("$refs.seo.tagInputsFocus($refs.%s)", fmt.Sprintf("%s_og_url", refPrefix))).Attr("ref", fmt.Sprintf("%s_og_url", refPrefix))).Cols(6),
  292. VCol(VTextField().FieldName(fmt.Sprintf("%s.%s", fieldPrefix, "OpenGraphType")).Label(msgr.OpenGraphType).Value(setting.OpenGraphType).Attr("@focus", fmt.Sprintf("$refs.seo.tagInputsFocus($refs.%s)", fmt.Sprintf("%s_og_type", refPrefix))).Attr("ref", fmt.Sprintf("%s_og_type", refPrefix))).Cols(6),
  293. ),
  294. VRow(
  295. VCol(VTextField().FieldName(fmt.Sprintf("%s.%s", fieldPrefix, "OpenGraphImageURL")).Label(msgr.OpenGraphImageURL).Value(setting.OpenGraphImageURL).Attr("@focus", fmt.Sprintf("$refs.seo.tagInputsFocus($refs.%s)", fmt.Sprintf("%s_og_imageurl", refPrefix))).Attr("ref", fmt.Sprintf("%s_og_imageurl", refPrefix))).Cols(12),
  296. ),
  297. VRow(
  298. VCol(views.QMediaBox(db).Label(msgr.OpenGraphImage).
  299. FieldName(fmt.Sprintf("%s.%s", fieldPrefix, "OpenGraphImageFromMediaLibrary")).
  300. Value(image).
  301. Config(&media_library.MediaBoxConfig{
  302. AllowType: "image",
  303. Sizes: map[string]*media.Size{
  304. "og": {
  305. Width: 1200,
  306. Height: 630,
  307. },
  308. "twitter-large": {
  309. Width: 1200,
  310. Height: 600,
  311. },
  312. "twitter-small": {
  313. Width: 630,
  314. Height: 630,
  315. },
  316. },
  317. })).Cols(12)),
  318. VRow(
  319. VCol(VTextarea().FieldName(fmt.Sprintf("%s.%s", fieldPrefix, "OpenGraphMetadataString")).Label(msgr.OpenGraphMetadata).Value(GetOpenGraphMetadataString(setting.OpenGraphMetadata)).Attr("@focus", fmt.Sprintf("$refs.seo.tagInputsFocus($refs.%s)", fmt.Sprintf("%s_og_metadata", refPrefix))).Attr("ref", fmt.Sprintf("%s_og_metadata", refPrefix))).Cols(12),
  320. ),
  321. ),
  322. ).Outlined(true).Flat(true),
  323. ).Attr("ref", "seo")
  324. }
  325. func (collection *Collection) save(ctx *web.EventContext) (r web.EventResponse, err error) {
  326. var (
  327. db = collection.getDBFromContext(ctx.R.Context())
  328. name = ctx.R.FormValue("name")
  329. setting = collection.NewSettingModelInstance().(QorSEOSettingInterface)
  330. locale, _ = l10n.IsLocalizableFromCtx(ctx.R.Context())
  331. )
  332. if err = db.Where("name = ? AND locale_code = ?", name, locale).First(setting).Error; err != nil {
  333. return
  334. }
  335. var (
  336. variables = map[string]string{}
  337. settingVals = map[string]interface{}{}
  338. mediaBox = media_library.MediaBox{}
  339. )
  340. for fieldWithPrefix := range ctx.R.Form {
  341. if !strings.HasPrefix(fieldWithPrefix, name) {
  342. continue
  343. }
  344. field := strings.Replace(fieldWithPrefix, fmt.Sprintf("%s.", name), "", -1)
  345. // make sure OpenGraphImageFromMediaLibrary.Description set after OpenGraphImageFromMediaLibrary.Values
  346. if field == "OpenGraphImageFromMediaLibrary.Values" {
  347. err = mediaBox.Scan(ctx.R.FormValue(fieldWithPrefix))
  348. if err != nil {
  349. return
  350. }
  351. break
  352. }
  353. }
  354. for fieldWithPrefix := range ctx.R.Form {
  355. if !strings.HasPrefix(fieldWithPrefix, name) {
  356. continue
  357. }
  358. field := strings.Replace(fieldWithPrefix, fmt.Sprintf("%s.", name), "", -1)
  359. if strings.HasPrefix(field, "OpenGraphImageFromMediaLibrary") {
  360. if field == "OpenGraphImageFromMediaLibrary.Description" {
  361. mediaBox.Description = ctx.R.FormValue(fieldWithPrefix)
  362. if err != nil {
  363. return
  364. }
  365. settingVals["OpenGraphImageFromMediaLibrary"] = mediaBox
  366. }
  367. continue
  368. }
  369. if strings.HasPrefix(field, "Variables") {
  370. key := strings.Replace(field, "Variables.", "", -1)
  371. variables[key] = ctx.R.FormValue(fieldWithPrefix)
  372. } else {
  373. settingVals[field] = ctx.R.Form.Get(fieldWithPrefix)
  374. }
  375. }
  376. s := setting.GetSEOSetting()
  377. for k, v := range settingVals {
  378. if k == "OpenGraphMetadataString" {
  379. metadata := GetOpenGraphMetadata(v.(string))
  380. err = reflectutils.Set(&s, "OpenGraphMetadata", metadata)
  381. if err != nil {
  382. return
  383. }
  384. continue
  385. }
  386. err = reflectutils.Set(&s, k, v)
  387. if err != nil {
  388. return
  389. }
  390. }
  391. setting.SetSEOSetting(s)
  392. setting.SetVariables(variables)
  393. setting.SetLocale(locale)
  394. if err = db.Save(setting).Error; err != nil {
  395. return
  396. }
  397. r.VarsScript = fmt.Sprintf(`vars.seoSnackbarShow = true;vars.%s = false;`, ctx.R.FormValue("loadingName"))
  398. if collection.afterSave != nil {
  399. if err = collection.afterSave(ctx.R.Context(), name, locale); err != nil {
  400. return
  401. }
  402. }
  403. return
  404. }