From 156955de6b4e6288ac606d29b843081842154eff Mon Sep 17 00:00:00 2001 From: Kishan Gajjar Date: Fri, 8 Jun 2018 11:06:40 +0530 Subject: [PATCH] [ADD] website: Added a feature to set opengraph/twitter meta image from SEO dialog. With this commit now user can set custom meta image from the seo dialog. If the user sets such image then it will be displayed when the user shares that URL in social sites. By default, it will show the list of images used in the current page. Alternatively, user can use media dialog to select/upload images. By default, it will take the company logo as a meta-image. We also improved how custom meta tags are handled for different pages, Now we can set custom meta tags based on the model and we can override '_default_website_meta' method in case you want to change default meta values from the specific model. e.g. For products displayed on the shop, if we want to change the value of og:image to product specific images instead of default logo In future, it will be used to add model specific metas like og:price, og:currency We passed 'website_company_id' in frontend session info because we display company logo in dialog for all pages. We can't use /logo.png as it is not updating in multicompany and multiwebsite. task id: 1835370 Closes: #25430 --- addons/website/models/ir_ui_view.py | 3 + addons/website/models/website.py | 69 ++++++++++++++ addons/website/static/src/js/menu/seo.js | 95 ++++++++++++++++++- .../website/static/src/scss/website.ui.scss | 63 ++++++++++++ addons/website/static/src/xml/website.seo.xml | 35 +++++++ addons/website/views/website_templates.xml | 34 ++++--- 6 files changed, 285 insertions(+), 14 deletions(-) diff --git a/addons/website/models/ir_ui_view.py b/addons/website/models/ir_ui_view.py index 933417c236f..88a9943a005 100644 --- a/addons/website/models/ir_ui_view.py +++ b/addons/website/models/ir_ui_view.py @@ -204,6 +204,9 @@ class View(models.Model): new_context = dict(self._context, inherit_branding=True) elif request.env.user.has_group('website.group_website_publisher'): new_context = dict(self._context, inherit_branding_auto=True) + # Fallback incase main_object dont't inherit 'website.seo.metadata' + if values and 'main_object' in values and not hasattr(values['main_object'], 'get_website_meta'): + values['main_object'].get_website_meta = lambda: {} if self._context != new_context: self = self.with_context(new_context) diff --git a/addons/website/models/website.py b/addons/website/models/website.py index 4a98d57132b..90b7a107cd6 100644 --- a/addons/website/models/website.py +++ b/addons/website/models/website.py @@ -694,6 +694,71 @@ class SeoMetadata(models.AbstractModel): website_meta_title = fields.Char("Website meta title", translate=True) website_meta_description = fields.Text("Website meta description", translate=True) website_meta_keywords = fields.Char("Website meta keywords", translate=True) + website_meta_og_img = fields.Char("Website opengraph image") + + def _default_website_meta(self): + """ This method will return default meta information. It return the dict + contains meta property as a key and meta content as a value. + e.g. 'og:type': 'website'. + + Override this method in case you want to change default value + from any model. e.g. change value of og:image to product specific + images instead of default images + """ + self.ensure_one() + company = request.website.company_id.sudo() + title = (request.website or company).name + if 'name' in self: + title = '%s | %s' % (self.name, title) + # Default meta for OpenGraph + default_opengraph = { + 'og:type': 'website', + 'og:title': title, + 'og:site_name': company.name, + 'og:url': request.httprequest.url, + 'og:image': '/web/image/res.company/%s/logo' % company.id, + } + # Default meta for Twitter + default_twitter = { + 'twitter:card': 'summary_large_image', + 'twitter:title': title, + 'twitter:image': '/web/image/res.company/%s/logo' % company.id, + } + if company.social_twitter: + default_twitter['twitter:site'] = "@%s" % company.social_twitter.split('/')[-1] + + return { + 'default_opengraph': default_opengraph, + 'default_twitter': default_twitter + } + + def get_website_meta(self): + """ This method will return final meta information. It will replace + default values with user's custom value (if user modified it from + the seo popup of fronted) + + This method is not meant for overridden. To customize meta values + override `_default_website_meta` method instead of this method. This + method only replaces user custom values in defaults. + """ + root_url = request.httprequest.url_root.strip('/') + default_meta = self._default_website_meta() + opengraph_meta, twitter_meta = default_meta['default_opengraph'], default_meta['default_twitter'] + if self.website_meta_title: + opengraph_meta['og:title'] = self.website_meta_title + twitter_meta['twitter:title'] = self.website_meta_title + if self.website_meta_description: + opengraph_meta['og:description'] = self.website_meta_description + twitter_meta['twitter:description'] = self.website_meta_description + meta_image = self.website_meta_og_img or opengraph_meta['og:image'] + if meta_image.startswith('/'): + meta_image = "%s%s" % (root_url, meta_image) + opengraph_meta['og:image'] = meta_image + twitter_meta['twitter:image'] = meta_image + return { + 'opengraph_meta': opengraph_meta, + 'twitter_meta': twitter_meta + } class WebsiteMultiMixin(models.AbstractModel): @@ -966,6 +1031,10 @@ class Page(models.Model): vals['url'] = '/' + vals['url'] return super(Page, self).write(vals) + def get_website_meta(self): + self.ensure_one() + return self.view_id.get_website_meta() + class Menu(models.Model): diff --git a/addons/website/static/src/js/menu/seo.js b/addons/website/static/src/js/menu/seo.js index 180bd561df1..0f1c268dbb8 100644 --- a/addons/website/static/src/js/menu/seo.js +++ b/addons/website/static/src/js/menu/seo.js @@ -8,6 +8,7 @@ var mixins = require('web.mixins'); var rpc = require('web.rpc'); var Widget = require('web.Widget'); var weContext = require('web_editor.context'); +var weWidgets = require('web_editor.widget'); var websiteNavbarData = require('website.navbar'); var _t = core._t; @@ -273,6 +274,14 @@ var HtmlPage = Class.extend(mixins.PropertiesMixin, { return $(this).text(); }); }, + getOgMeta: function () { + var ogImageUrl = $('meta[property="og:image"]').attr('content'); + var title = $('meta[property="og:title"]').attr('content'); + return { + ogImageUrl: ogImageUrl && ogImageUrl.replace(window.location.origin, ''), + metaTitle: title, + }; + }, images: function () { return $('#wrap img').map(function () { var $img = $(this); @@ -315,6 +324,81 @@ var Tip = Widget.extend({ }, }); +var metaImageSelector = Widget.extend({ + template: 'website.seo_meta_image_selector', + xmlDependencies: ['/website/static/src/xml/website.seo.xml'], + events: { + 'click .o_meta_img_upload': '_onClickUploadImg', + 'click .o_meta_img': '_onClickSelectImg', + }, + /** + * @override + * @param {widget} parent + * @param {Object} data + */ + init: function (parent, data) { + this.metaTitle = data.title; + this.activeMetaImg = data.metaImg; + this.serverUrl = window.location.origin; + data.pageImages.unshift(_.str.sprintf('/web/image/res.company/%s/logo', odoo.session_info.website_company_id)); + this.images = _.uniq(data.pageImages); + this.customImgUrl = _.contains(data.pageImages, data.metaImg) ? false : data.metaImg; + this._super(parent); + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * Update template. + * + * @private + */ + _updateTemplateBody: function () { + this.$el.empty(); + this.images = _.uniq(this.images); + this.$el.append(core.qweb.render('website.og_image_body', {widget: this})); + }, + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + /** + * Called when a select image from list -> change the preview accordingly. + * + * @private + * @param {MouseEvent} ev + */ + _onClickSelectImg: function (ev) { + var $img = $(ev.currentTarget); + this.activeMetaImg = $img.find('img').attr('src'); + this._updateTemplateBody(); + }, + /** + * Open a mediaDialog to select/upload image. + * + * @private + * @param {MouseEvent} ev + */ + _onClickUploadImg: function (ev) { + var self = this; + var $image = $(''); + var mediaDialog = new weWidgets.MediaDialog(this, { + onlyImages: true, + res_model: 'ir.ui.view', + }, null, $image); + mediaDialog.open(); + mediaDialog.on('save', this, function (image) { + var src = image.attr('src'); + self.activeMetaImg = src; + self.customImgUrl = src; + self._updateTemplateBody(); + }); + }, +}); + var SeoConfigurator = Dialog.extend({ template: 'website.seo_configuration', xmlDependencies: Dialog.prototype.xmlDependencies.concat( @@ -377,6 +461,12 @@ var SeoConfigurator = Dialog.extend({ this.keywordList.insertAfter(this.$('.table thead')); this.disableUnsavableFields().then(function(){ self.renderPreview(); + self.metaImageSelector = new metaImageSelector(self, { + title: self.htmlPage.getOgMeta().metaTitle, + metaImg : self.metaImg || self.htmlPage.getOgMeta().ogImageUrl, + pageImages : _.pluck(self.htmlPage.images().get(), 'src'), + }); + self.metaImageSelector.appendTo(self.$('.js_seo_keywords_list')); }); this.getLanguages(); @@ -404,6 +494,7 @@ var SeoConfigurator = Dialog.extend({ self.canEditTitle = data && ('website_meta_title' in data); self.canEditDescription = data && ('website_meta_description' in data); self.canEditKeywords = data && ('website_meta_keywords' in data); + self.metaImg = data.website_meta_og_img; if (!self.canEditTitle) { self.$('input[name=seo_page_title]').attr('disabled', true); } @@ -454,6 +545,7 @@ var SeoConfigurator = Dialog.extend({ if (this.canEditKeywords) { data.website_meta_keywords = this.keywordList.keywords().join(', '); } + data.website_meta_og_img = this.metaImageSelector.activeMetaImg; this.saveMetaData(data).then(function () { self.htmlPage.changeKeywords(self.keywordList.keywords()); self.close(); @@ -478,7 +570,8 @@ var SeoConfigurator = Dialog.extend({ // return $.Deferred().reject(new Error("No main_object was found.")); def.resolve(null); } else { - var fields = ['website_meta_title', 'website_meta_description', 'website_meta_keywords']; + var fields = ['website_meta_title', 'website_meta_description', 'website_meta_keywords' + ,'website_meta_og_img']; if (obj.model == 'website.page'){ fields.push('website_indexed'); } diff --git a/addons/website/static/src/scss/website.ui.scss b/addons/website/static/src/scss/website.ui.scss index eb459ee1503..093a0da8524 100644 --- a/addons/website/static/src/scss/website.ui.scss +++ b/addons/website/static/src/scss/website.ui.scss @@ -194,6 +194,69 @@ body .modal { background-color: white; margin-left: -1px; } + .o_seo_og_image { + .o_meta_img { + position: relative; + transition: border-color 200ms; + display: inline-block; + border: 2px solid gray('400'); + + > img { + width: 70px; + height: 70px; + object-fit: cover; + cursor: pointer; + } + + &:hover { + border-color: $o-brand-primary; + } + + &.o_active_image { + border-color: $o-brand-primary; + + &:before { + @include o-position-absolute($right: 0); + content: ''; + border: 16px solid rgba($o-brand-primary, 0.8); + border-left-color: transparent; + border-bottom-color: transparent; + } + + &:after { + @include o-position-absolute(2px, 3px); + display: inline-block; + content: "\f00c"; + font-family: FontAwesome; + color: white; + font-size: 12px; + } + } + .o-custom-label { + @include o-position-absolute($bottom: 0px); + background: rgba(gray('800'), 0.6); + font-size: 12px; + } + } + .o_meta_img_upload { + transition: 200ms; + display: inline-block; + padding: 23px 27px; + border: 2px dashed lighten(gray('600'), 30%); + vertical-align: top; + cursor: pointer; + color: lighten(gray('600'), 30%); + + &:hover { + border-color: $o-brand-primary; + color: $o-brand-primary; + } + } + .o_meta_active_img { + height: 240px; + object-fit: cover; + } + } .table { td { diff --git a/addons/website/static/src/xml/website.seo.xml b/addons/website/static/src/xml/website.seo.xml index d9cb1f38f80..8ada1a3088a 100644 --- a/addons/website/static/src/xml/website.seo.xml +++ b/addons/website/static/src/xml/website.seo.xml @@ -124,4 +124,39 @@ + +
+ +
+ + +
+
+
Image for Social Share
+
Select an image to use in social share.
+ +
+ +
+
+
+ Custom + +
+
+ +
+
+
+
Preview how your page will be displayed on social media
+
+ +
+
+ +
+
+
+
+
diff --git a/addons/website/views/website_templates.xml b/addons/website/views/website_templates.xml index d73932879d1..44355c528ac 100644 --- a/addons/website/views/website_templates.xml +++ b/addons/website/views/website_templates.xml @@ -154,15 +154,22 @@ and main_object.website_meta_keywords or website_meta_keywords"/> - - - - - - - - - + + + + + + + + + + + + + + + + @@ -173,10 +180,6 @@