admin.go 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396
  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. for key := range ctx.R.Form {
  50. if strings.HasPrefix(key, fmt.Sprintf("%s.", field.Name)) {
  51. reflectutils.Set(&setting, strings.TrimPrefix(key, fmt.Sprintf("%s.", field.Name)), ctx.R.Form.Get(key))
  52. }
  53. }
  54. return reflectutils.Set(obj, field.Name, setting)
  55. }
  56. func (collection *Collection) EditingComponentFunc(obj interface{}, field *presets.FieldContext, ctx *web.EventContext) h.HTMLComponent {
  57. var (
  58. msgr = i18n.MustGetModuleMessages(ctx.R, I18nSeoKey, Messages_en_US).(*Messages)
  59. fieldPrefix string
  60. setting Setting
  61. db = collection.getDBFromContext(ctx.R.Context())
  62. locale, _ = l10n.IsLocalizableFromCtx(ctx.R.Context())
  63. )
  64. seo := collection.GetSEOByModel(obj)
  65. if seo == nil {
  66. return h.Div()
  67. }
  68. value := reflect.Indirect(reflect.ValueOf(obj))
  69. for i := 0; i < value.NumField(); i++ {
  70. if s, ok := value.Field(i).Interface().(Setting); ok {
  71. setting = s
  72. fieldPrefix = value.Type().Field(i).Name
  73. }
  74. }
  75. if setting.IsEmpty() {
  76. modelSetting := collection.NewSettingModelInstance().(QorSEOSettingInterface)
  77. db.Where("name = ? AND locale_code = ?", seo.name, locale).First(modelSetting)
  78. setting.Title = modelSetting.GetTitle()
  79. setting.Description = modelSetting.GetDescription()
  80. setting.Keywords = modelSetting.GetKeywords()
  81. setting.OpenGraphURL = modelSetting.GetOpenGraphURL()
  82. setting.OpenGraphType = modelSetting.GetOpenGraphType()
  83. setting.OpenGraphImageURL = modelSetting.GetOpenGraphImageURL()
  84. setting.OpenGraphImageFromMediaLibrary = modelSetting.GetOpenGraphImageFromMediaLibrary()
  85. setting.OpenGraphMetadata = modelSetting.GetOpenGraphMetadata()
  86. }
  87. openCustomizePanel := "1"
  88. hideActions := false
  89. o := ctx.R.FormValue("openCustomizePanel")
  90. if setting.EnabledCustomize && o != "" {
  91. openCustomizePanel = o
  92. }
  93. if o != "" {
  94. hideActions = true
  95. }
  96. return web.Scope(
  97. h.Div(
  98. h.Label(msgr.Seo).Class("v-label theme--light"),
  99. VExpansionPanels(
  100. VExpansionPanel(
  101. VExpansionPanelHeader(
  102. h.HTMLComponents{
  103. 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();}"),
  104. VSwitch().FieldName(fmt.Sprintf("%s.%s", fieldPrefix, "EnabledCustomize")).Value(setting.EnabledCustomize).Attr(":input-value", "locals.enabledCustomize").Attr("ref", "customize").Attr("style", "display:none;"),
  105. },
  106. ).Attr("style", "padding: 0px 24px;").HideActions(hideActions),
  107. VExpansionPanelContent(
  108. VCardText(
  109. collection.vseo(fieldPrefix, seo, &setting, ctx.R),
  110. ),
  111. ).Eager(true),
  112. ),
  113. ).Flat(true).Attr("v-model", "locals.openCustomizePanel"),
  114. ).Class("pb-4"),
  115. ).Init(fmt.Sprintf(`{enabledCustomize: %t, userDefaults: %t, openCustomizePanel: %s, }`, setting.EnabledCustomize, !setting.EnabledCustomize, openCustomizePanel)).
  116. VSlot("{ locals }")
  117. }
  118. func (collection *Collection) pageFunc(ctx *web.EventContext) (_ web.PageResponse, err error) {
  119. var (
  120. msgr = i18n.MustGetModuleMessages(ctx.R, I18nSeoKey, Messages_en_US).(*Messages)
  121. db = collection.getDBFromContext(ctx.R.Context())
  122. locale, _ = l10n.IsLocalizableFromCtx(ctx.R.Context())
  123. )
  124. var seoComponents h.HTMLComponents
  125. for _, seo := range collection.registeredSEO {
  126. modelSetting := collection.NewSettingModelInstance().(QorSEOSettingInterface)
  127. err := db.Where("name = ? AND locale_code = ?", seo.name, locale).First(modelSetting).Error
  128. if errors.Is(err, gorm.ErrRecordNotFound) {
  129. modelSetting.SetName(seo.name)
  130. modelSetting.SetLocale(locale)
  131. if err := db.Save(modelSetting).Error; err != nil {
  132. panic(err)
  133. }
  134. }
  135. var variablesComps h.HTMLComponents
  136. if seo.settingVariables != nil {
  137. variables := reflect.Indirect(reflect.New(reflect.Indirect(reflect.ValueOf(seo.settingVariables)).Type()))
  138. variableValues := modelSetting.GetVariables()
  139. for i := 0; i < variables.NumField(); i++ {
  140. fieldName := variables.Type().Field(i).Name
  141. if variableValues[fieldName] != "" {
  142. fmt.Println(fieldName, variableValues[fieldName])
  143. variables.Field(i).Set(reflect.ValueOf(variableValues[fieldName]))
  144. }
  145. }
  146. if variables.Type().NumField() > 0 {
  147. variablesComps = append(variablesComps, h.H3(msgr.Variable).Style("margin-top:15px;font-weight: 500"))
  148. }
  149. for i := 0; i < variables.Type().NumField(); i++ {
  150. field := variables.Type().Field(i)
  151. 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()))
  152. }
  153. }
  154. var (
  155. label string
  156. setting = modelSetting.GetSEOSetting()
  157. loadingName = strings.ReplaceAll(strings.ToLower(seo.name), " ", "_")
  158. )
  159. if seo.name == collection.globalName {
  160. label = msgr.GlobalName
  161. } else {
  162. label = i18n.PT(ctx.R, presets.ModelsI18nModuleKey, "Seo", seo.name)
  163. }
  164. comp := VExpansionPanel(
  165. VExpansionPanelHeader(h.H4(label).Style("font-weight: 500;")),
  166. VExpansionPanelContent(
  167. VCardText(
  168. variablesComps,
  169. collection.vseo(modelSetting.GetName(), seo, &setting, ctx.R),
  170. ),
  171. VCardActions(
  172. VSpacer(),
  173. VBtn(msgr.Save).Bind("loading", fmt.Sprintf("vars.%s", loadingName)).Color("primary").Large(true).
  174. Attr("@click", web.Plaid().
  175. EventFunc(saveEvent).
  176. Query("name", seo.name).
  177. Query("loadingName", loadingName).
  178. BeforeScript(fmt.Sprintf("vars.%s = true;", loadingName)).Go()),
  179. ).Attr(web.InitContextVars, fmt.Sprintf(`{%s: false}`, loadingName)),
  180. ),
  181. )
  182. seoComponents = append(seoComponents, comp)
  183. }
  184. return web.PageResponse{
  185. PageTitle: msgr.PageTitle,
  186. Body: h.If(editIsAllowed(ctx.R) == nil, VContainer(
  187. VSnackbar(h.Text(msgr.SavedSuccessfully)).
  188. Attr("v-model", "vars.seoSnackbarShow").
  189. Top(true).
  190. Color("primary").
  191. Timeout(2000),
  192. VRow(
  193. VCol(
  194. VContainer(
  195. h.H3(msgr.PageMetadataTitle).Attr("style", "font-weight: 500"),
  196. h.P().Text(msgr.PageMetadataDescription)),
  197. ).Cols(3),
  198. VCol(
  199. VExpansionPanels(
  200. seoComponents,
  201. ).Focusable(true),
  202. ).Cols(9),
  203. ),
  204. ).Attr("style", "background-color: #f5f5f5;max-width:100%").Attr(web.InitContextVars, `{seoSnackbarShow: false}`)),
  205. }, nil
  206. }
  207. func (collection *Collection) vseo(fieldPrefix string, seo *SEO, setting *Setting, req *http.Request) h.HTMLComponent {
  208. var (
  209. seos []*SEO
  210. msgr = i18n.MustGetModuleMessages(req, I18nSeoKey, Messages_en_US).(*Messages)
  211. db = collection.getDBFromContext(req.Context())
  212. )
  213. if seo.name == collection.globalName {
  214. seos = append(seos, seo)
  215. } else {
  216. seos = append(seos, collection.GetSEO(collection.globalName), seo)
  217. }
  218. var (
  219. variablesEle []h.HTMLComponent
  220. variables []string
  221. )
  222. for _, seo := range seos {
  223. if seo.settingVariables != nil {
  224. value := reflect.Indirect(reflect.ValueOf(seo.settingVariables)).Type()
  225. for i := 0; i < value.NumField(); i++ {
  226. fieldName := value.Field(i).Name
  227. variables = append(variables, fieldName)
  228. }
  229. }
  230. for key := range seo.contextVariables {
  231. if !strings.Contains(key, ":") {
  232. variables = append(variables, key)
  233. }
  234. }
  235. }
  236. for _, variable := range variables {
  237. variablesEle = append(variablesEle, VCol(
  238. 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)),
  239. ).Cols(2))
  240. }
  241. image := &setting.OpenGraphImageFromMediaLibrary
  242. if image.ID.String() == "0" {
  243. image.ID = json.Number("")
  244. }
  245. refPrefix := strings.ReplaceAll(strings.ToLower(fieldPrefix), " ", "_")
  246. return VSeo(
  247. h.H4(msgr.Basic).Style("margin-top:15px;font-weight: 500"),
  248. VRow(
  249. variablesEle...,
  250. ),
  251. VCard(
  252. VCardText(
  253. VTextField().Counter(65).FieldName(fmt.Sprintf("%s.%s", fieldPrefix, "Title")).Label(msgr.Title).Value(setting.Title).Attr("@click", fmt.Sprintf("$refs.seo.tagInputsFocus($refs.%s)", fmt.Sprintf("%s_title", refPrefix))).Attr("ref", fmt.Sprintf("%s_title", refPrefix)),
  254. VTextField().Counter(150).FieldName(fmt.Sprintf("%s.%s", fieldPrefix, "Description")).Label(msgr.Description).Value(setting.Description).Attr("@click", fmt.Sprintf("$refs.seo.tagInputsFocus($refs.%s)", fmt.Sprintf("%s_description", refPrefix))).Attr("ref", fmt.Sprintf("%s_description", refPrefix)),
  255. VTextarea().Counter(255).Rows(2).AutoGrow(true).FieldName(fmt.Sprintf("%s.%s", fieldPrefix, "Keywords")).Label(msgr.Keywords).Value(setting.Keywords).Attr("@click", fmt.Sprintf("$refs.seo.tagInputsFocus($refs.%s)", fmt.Sprintf("%s_keywords", refPrefix))).Attr("ref", fmt.Sprintf("%s_keywords", refPrefix)),
  256. ),
  257. ).Outlined(true).Flat(true),
  258. h.H4(msgr.OpenGraphInformation).Style("margin-top:15px;margin-bottom:15px;font-weight: 500"),
  259. VCard(
  260. VCardText(
  261. VRow(
  262. VCol(VTextField().FieldName(fmt.Sprintf("%s.%s", fieldPrefix, "OpenGraphURL")).Label(msgr.OpenGraphURL).Value(setting.OpenGraphURL)).Cols(6),
  263. VCol(VTextField().FieldName(fmt.Sprintf("%s.%s", fieldPrefix, "OpenGraphType")).Label(msgr.OpenGraphType).Value(setting.OpenGraphType)).Cols(6),
  264. ),
  265. VRow(
  266. VCol(VTextField().FieldName(fmt.Sprintf("%s.%s", fieldPrefix, "OpenGraphImageURL")).Label(msgr.OpenGraphImageURL).Value(setting.OpenGraphImageURL)).Cols(12),
  267. ),
  268. VRow(
  269. VCol(views.QMediaBox(db).Label(msgr.OpenGraphImage).
  270. FieldName(fmt.Sprintf("%s.%s", fieldPrefix, "OpenGraphImageFromMediaLibrary")).
  271. Value(image).
  272. Config(&media_library.MediaBoxConfig{
  273. AllowType: "image",
  274. Sizes: map[string]*media.Size{
  275. "og": {
  276. Width: 1200,
  277. Height: 630,
  278. },
  279. "twitter-large": {
  280. Width: 1200,
  281. Height: 600,
  282. },
  283. "twitter-small": {
  284. Width: 630,
  285. Height: 630,
  286. },
  287. },
  288. })).Cols(12)),
  289. ),
  290. ).Outlined(true).Flat(true),
  291. ).Attr("ref", "seo")
  292. }
  293. func (collection *Collection) save(ctx *web.EventContext) (r web.EventResponse, err error) {
  294. var (
  295. db = collection.getDBFromContext(ctx.R.Context())
  296. name = ctx.R.FormValue("name")
  297. setting = collection.NewSettingModelInstance().(QorSEOSettingInterface)
  298. locale, _ = l10n.IsLocalizableFromCtx(ctx.R.Context())
  299. )
  300. if err = db.Where("name = ? AND locale_code = ?", name, locale).First(setting).Error; err != nil {
  301. return
  302. }
  303. var (
  304. variables = map[string]string{}
  305. settingVals = map[string]interface{}{}
  306. mediaBox = media_library.MediaBox{}
  307. )
  308. for fieldWithPrefix := range ctx.R.Form {
  309. if !strings.HasPrefix(fieldWithPrefix, name) {
  310. continue
  311. }
  312. field := strings.Replace(fieldWithPrefix, fmt.Sprintf("%s.", name), "", -1)
  313. if strings.HasPrefix(field, "OpenGraphImageFromMediaLibrary") {
  314. if field == "OpenGraphImageFromMediaLibrary.Values" {
  315. err = mediaBox.Scan(ctx.R.FormValue(fieldWithPrefix))
  316. if err != nil {
  317. return
  318. }
  319. settingVals["OpenGraphImageFromMediaLibrary"] = mediaBox
  320. }
  321. if field == "OpenGraphImageFromMediaLibrary.Description" {
  322. mediaBox.Description = ctx.R.FormValue(fieldWithPrefix)
  323. if err != nil {
  324. return
  325. }
  326. }
  327. } else if strings.HasPrefix(field, "Variables") {
  328. key := strings.Replace(field, "Variables.", "", -1)
  329. variables[key] = ctx.R.FormValue(fieldWithPrefix)
  330. } else {
  331. settingVals[field] = ctx.R.Form.Get(fieldWithPrefix)
  332. }
  333. }
  334. s := setting.GetSEOSetting()
  335. for k, v := range settingVals {
  336. err = reflectutils.Set(&s, k, v)
  337. if err != nil {
  338. return
  339. }
  340. }
  341. setting.SetSEOSetting(s)
  342. setting.SetVariables(variables)
  343. setting.SetLocale(locale)
  344. if err = db.Save(setting).Error; err != nil {
  345. return
  346. }
  347. r.VarsScript = fmt.Sprintf(`vars.seoSnackbarShow = true;vars.%s = false;`, ctx.R.FormValue("loadingName"))
  348. if collection.afterSave != nil {
  349. if err = collection.afterSave(ctx.R.Context(), name, locale); err != nil {
  350. return
  351. }
  352. }
  353. return
  354. }