[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:
committed by
Jeremy Kersten
parent
05a9a095fb
commit
156955de6b
@@ -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)
|
||||
|
||||
@@ -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):
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"/>
|
||||
|
||||
Reference in New Issue
Block a user