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