[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
This commit is contained in:
Kishan Gajjar
2018-08-22 14:01:32 +02:00
committed by Jeremy Kersten
parent 05a9a095fb
commit 156955de6b
6 changed files with 285 additions and 14 deletions
+3
View File
@@ -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)
+69
View File
@@ -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):
+94 -1
View File
@@ -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 = $('<img/>');
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');
}
@@ -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 {
@@ -124,4 +124,39 @@
</div>
</li>
</t>
<div t-name="website.seo_meta_image_selector" class="o_seo_og_image">
<t t-call="website.og_image_body"/>
</div>
<t t-name="website.og_image_body">
<div class="row">
<div class="col-md-6">
<h5 class="mt16 mb4">Image for Social Share</h5>
<div class="text-muted mb32">Select an image to use in social share.</div>
<t t-foreach="widget.images" t-as="image">
<div t-attf-class="o_meta_img mt4 #{image === widget.activeMetaImg and ' o_active_image' or ''}">
<img t-att-src="image"/>
</div>
</t>
<div t-if="widget.customImgUrl" t-attf-class="o_meta_img mt4 #{widget.customImgUrl === widget.activeMetaImg and ' o_active_image' or ''}">
<span class="o-custom-label w-100 text-white text-center">Custom</span>
<img t-att-src="widget.customImgUrl"/>
</div>
<div class="o_meta_img_upload mt4" title="Click to choose more images">
<i class="fa fa-upload"/>
</div>
</div>
<div class="col-md-6">
<h6 class="mt16">Preview <small>how your page will be displayed on social media</small></h6>
<div class="card p-0">
<img class="card-img-top o_meta_active_img" t-att-src="widget.activeMetaImg"/>
<div class="card-body px-3 py-2">
<h6 class="text-alpha card-title mb0"><t t-esc="widget.metaTitle"/></h6>
<small class="card-subtitle text-muted"><t t-esc="widget.serverUrl"/></small>
</div>
</div>
</div>
</div>
</t>
</templates>
+21 -13
View File
@@ -154,15 +154,22 @@
and main_object.website_meta_keywords or website_meta_keywords"/>
<meta t-if="meta_description or editable" name="description" t-att-content="meta_description"/>
<meta t-if="meta_keywords or editable" name="keywords" t-att-content="meta_keywords"/>
<!-- OpenGraph tags for Facebook sharing -->
<meta property="og:title" t-att-content="additional_title" />
<meta property="og:site_name" t-att-content="res_company.name" />
<t t-if="main_object and 'plain_content' in main_object and main_object.plain_content">
<t t-set="og_description" t-value="main_object.plain_content[0:500]"/>
<meta property="og:description" t-att-content="og_description" />
<meta property='og:image' t-att-content="request.httprequest.url_root+'logo.png'"/>
<meta property='og:url' t-att-content="request.httprequest.url_root+request.httprequest.path[1:end]"/>
<t t-if="main_object">
<t t-set="website_meta" t-value="main_object.get_website_meta()"/>
<!-- OpenGraph tags for Facebook sharing -->
<t t-set="opengraph_meta" t-value="website_meta.get('opengraph_meta')"/>
<t t-if="opengraph_meta">
<t t-foreach="opengraph_meta" t-as="og_meta">
<meta t-att-property="og_meta" t-att-content="opengraph_meta[og_meta]"/>
</t>
</t>
<!-- Twitter tags for sharing -->
<t t-set="twitter_meta" t-value="website_meta.get('twitter_meta')"/>
<t t-if="opengraph_meta">
<t t-foreach="twitter_meta" t-as="t_meta">
<meta t-att-name="t_meta" t-att-content="twitter_meta[t_meta]"/>
</t>
</t>
</t>
<t t-set="languages" t-value="website.get_languages() if website else None"/>
@@ -173,10 +180,6 @@
</t>
<script type="text/javascript">
<t groups="website.group_website_publisher">
odoo.snippetsURL = '/website/snippets';
</t>
odoo.session_info = {
is_superuser: <t t-esc="json.dumps(request.env.user._is_superuser())"/>,
is_system: <t t-esc="json.dumps(request.env.user._is_system())"/>,
@@ -185,6 +188,11 @@
is_website_user: <t t-esc="json.dumps(request.env.user.id == request.website.user_id.id)" />,
user_id: <t t-esc="json.dumps(request.env.user.id)" />
};
<t groups="website.group_website_publisher">
odoo.snippetsURL = '/website/snippets';
odoo.session_info.website_id = <t t-esc="json.dumps(request.website.id)" />;
odoo.session_info.website_company_id = <t t-esc="json.dumps(request.website.company_id.id)" />;
</t>
</script>
<t t-call-assets="web.assets_common" t-js="false"/>