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 @@