0
Fork 0
mirror of https://github.com/TryGhost/Ghost.git synced 2025-03-18 02:21:47 -05:00

Add Tag Meta View

Closes #4509, Closes #4615

- Adds meta title & meta description input fields
- Autosaves meta data
- Shows SEO preview (falling back to tag name & description if meta not supplied)
- Adds `type="button"` to delete button (which closes 4651)
This commit is contained in:
Paul Adam Davis 2014-12-04 10:37:30 +00:00
parent 823b5b4ea9
commit ce395998ff
5 changed files with 172 additions and 28 deletions

View file

@ -4,13 +4,14 @@ import boundOneWay from 'ghost/utils/bound-one-way';
var TagsController = Ember.ArrayController.extend(PaginationMixin, {
tags: Ember.computed.alias('model'),
needs: 'application',
activeTag: null,
activeTagNameScratch: boundOneWay('activeTag.name'),
activeTagSlugScratch: boundOneWay('activeTag.slug'),
activeTagDescriptionScratch: boundOneWay('activeTag.description'),
// Tag properties that should not be set to the empty string
requiredTagProperties: ['name', 'slug'],
activeTagMetaTitleScratch: boundOneWay('activeTag.meta_title'),
activeTagMetaDescriptionScratch: boundOneWay('activeTag.meta_description'),
init: function (options) {
options = options || {};
@ -18,18 +19,30 @@ var TagsController = Ember.ArrayController.extend(PaginationMixin, {
this._super(options);
},
isViewingSubview: Ember.computed('controllers.application.showSettingsMenu', function (key, value) {
// Not viewing a subview if we can't even see the PSM
if (!this.get('controllers.application.showSettingsMenu')) {
return false;
}
if (arguments.length > 1) {
return value;
}
return false;
}),
showErrors: function (errors) {
errors = Ember.isArray(errors) ? errors : [errors];
this.notifications.showErrors(errors);
},
saveActiveTagProperty: function (propKey, newValue) {
var activeTag = this.get('activeTag'),
currentValue = activeTag.get(propKey),
requiredTagProps = this.get('requiredTagProperties'),
self = this,
tagName;
self = this;
newValue = newValue.trim();
// Quit if value is empty for a required property
if (!newValue && requiredTagProps.contains(propKey)) {
return;
}
// Quit if there was no change
if (newValue === currentValue) {
return;
@ -37,19 +50,59 @@ var TagsController = Ember.ArrayController.extend(PaginationMixin, {
activeTag.set(propKey, newValue);
tagName = activeTag.get('name');
// don't save a new tag until it has a name
if (!tagName) {
return;
}
this.notifications.closePassive();
activeTag.save().then(function () {
self.notifications.showSuccess('Saved ' + tagName);
}).catch(function (error) {
self.notifications.showAPIError(error);
activeTag.save().catch(function (errors) {
self.showErrors(errors);
});
},
seoTitle: Ember.computed('scratch', 'activeTagNameScratch', 'activeTagMetaTitleScratch', function () {
var metaTitle = this.get('activeTagMetaTitleScratch') || '';
metaTitle = metaTitle.length > 0 ? metaTitle : this.get('activeTagNameScratch');
if (metaTitle && metaTitle.length > 70) {
metaTitle = metaTitle.substring(0, 70).trim();
metaTitle = Ember.Handlebars.Utils.escapeExpression(metaTitle);
metaTitle = new Ember.Handlebars.SafeString(metaTitle + '…');
}
return metaTitle;
}),
seoURL: Ember.computed('activeTagSlugScratch', function () {
var blogUrl = this.get('config').blogUrl,
seoSlug = this.get('activeTagSlugScratch') ? this.get('activeTagSlugScratch') : '',
seoURL = blogUrl + '/tag/' + seoSlug;
// only append a slash to the URL if the slug exists
if (seoSlug) {
seoURL += '/';
}
if (seoURL.length > 70) {
seoURL = seoURL.substring(0, 70).trim();
seoURL = new Ember.Handlebars.SafeString(seoURL + '…');
}
return seoURL;
}),
seoDescription: Ember.computed('scratch', 'activeTagDescriptionScratch', 'activeTagMetaDescriptionScratch', function () {
var metaDescription = this.get('activeTagMetaDescriptionScratch') || '';
metaDescription = metaDescription.length > 0 ? metaDescription : this.get('activeTagDescriptionScratch');
if (metaDescription && metaDescription.length > 156) {
metaDescription = metaDescription.substring(0, 156).trim();
metaDescription = Ember.Handlebars.Utils.escapeExpression(metaDescription);
metaDescription = new Ember.Handlebars.SafeString(metaDescription + '…');
}
return metaDescription;
}),
actions: {
newTag: function () {
this.set('activeTag', this.store.createRecord('tag'));
@ -84,6 +137,22 @@ var TagsController = Ember.ArrayController.extend(PaginationMixin, {
saveActiveTagDescription: function (description) {
this.saveActiveTagProperty('description', description);
},
saveActiveTagMetaTitle: function (metaTitle) {
this.saveActiveTagProperty('meta_title', metaTitle);
},
saveActiveTagMetaDescription: function (metaDescription) {
this.saveActiveTagProperty('meta_description', metaDescription);
},
showSubview: function () {
this.set('isViewingSubview', true);
},
closeSubview: function () {
this.set('isViewingSubview', false);
}
}
});

View file

@ -9,6 +9,7 @@ import ForgotValidator from 'ghost/validators/forgotten';
import SettingValidator from 'ghost/validators/setting';
import ResetValidator from 'ghost/validators/reset';
import UserValidator from 'ghost/validators/user';
import TagSettingsValidator from 'ghost/validators/tag-settings';
// our extensions to the validator library
ValidatorExtensions.init();
@ -71,7 +72,8 @@ var ValidationEngine = Ember.Mixin.create({
forgotten: ForgotValidator,
setting: SettingValidator,
reset: ResetValidator,
user: UserValidator
user: UserValidator,
tag: TagSettingsValidator
},
/**

View file

@ -1,6 +1,9 @@
import ValidationEngine from 'ghost/mixins/validation-engine';
import NProgressSaveMixin from 'ghost/mixins/nprogress-save';
var Tag = DS.Model.extend(NProgressSaveMixin, {
var Tag = DS.Model.extend(NProgressSaveMixin, ValidationEngine, {
validationType: 'tag',
uuid: DS.attr('string'),
name: DS.attr('string'),
slug: DS.attr('string'),

View file

@ -1,6 +1,6 @@
<div class="content-cover" {{action "closeSettingsMenu"}}></div>
<div class="settings-menu-container">
<div class="settings-menu settings-menu-pane settings-menu-pane-in">
{{#gh-tabs-manager selected="showSubview" class="settings-menu-container"}}
<div {{bind-attr class="isViewingSubview:settings-menu-pane-out-left:settings-menu-pane-in :settings-menu :settings-menu-pane"}}>
<div class="settings-menu-header">
<h4>Tag Settings</h4>
<button class="close icon-x settings-menu-header-action" {{action "closeSettingsMenu"}}>
@ -11,12 +11,12 @@
<form>
<div class="form-group">
<label>Tag Name</label>
{{gh-input type="text" placeholder=activeTag.name value=activeTagNameScratch focus-out="saveActiveTagName"}}
{{gh-input type="text" value=activeTagNameScratch focus-out="saveActiveTagName"}}
</div>
<div class="form-group">
<label>Slug</label>{{!--@TODO show full url preview, not just slug--}}
{{gh-input type="text" placeholder=activeTag.slug value=activeTagSlugScratch focus-out="saveActiveTagSlug"}}
{{gh-input type="text" value=activeTagSlugScratch focus-out="saveActiveTagSlug"}}
</div>
<div class="form-group">
@ -24,10 +24,52 @@
{{gh-textarea value=activeTagDescriptionScratch focus-out="saveActiveTagDescription"}}
</div>
<ul class="nav-list nav-list-block">
{{#gh-tab tagName="li" classNames="nav-list-item"}}
<button type="button">
<b>Meta Data</b>
<span>Extra content for SEO and social media.</span>
</button>
{{/gh-tab}}
</ul>
{{#unless activeTag.isNew}}
<button class="btn btn-red icon-trash" {{action "deleteTag" activeTag}}>Delete Tag</button>
<button type="button" class="btn btn-red icon-trash" {{action "deleteTag" activeTag}}>Delete Tag</button>
{{/unless}}
</form>
</div>
</div>
</div>
</div>{{! .settings-menu-pane }}
<div {{bind-attr class="isViewingSubview:settings-menu-pane-in:settings-menu-pane-out-right :settings-menu :settings-menu-pane"}}>
{{#gh-tab-pane}}
<div class="settings-menu-header subview">
<button {{action "closeSubview"}} class="back icon-chevron-left settings-menu-header-action"><span class="hidden">Back</span></button>
<h4>Meta Data</h4>
</div>
<div class="settings-menu-content">
<form>
<div class="form-group">
<label for="meta-title">Meta Title</label>
{{gh-input type="text" value=activeTagMetaTitleScratch focus-out="saveActiveTagMetaTitle"}}
<p>Recommended: <b>70</b> characters. Youve used {{gh-count-down-characters activeTagMetaTitleScratch 70}}</p>
</div>
<div class="form-group">
<label for="meta-description">Meta Description</label>
{{gh-textarea value=activeTagMetaDescriptionScratch focus-out="saveActiveTagMetaDescription"}}
<p>Recommended: <b>156</b> characters. Youve used {{gh-count-down-characters activeTagMetaDescriptionScratch 156}}</p>
</div>
<div class="form-group">
<label>Search Engine Result Preview</label>
<div class="seo-preview">
<div class="seo-preview-title">{{seoTitle}}</div>
<div class="seo-preview-link">{{seoURL}}</div>
<div class="seo-preview-description">{{seoDescription}}</div>
</div>
</form>
</div>{{! .settings-menu-content }}
{{/gh-tab-pane}}
</div>{{! .settings-menu-pane }}
{{/gh-tabs-manager}}

View file

@ -0,0 +1,28 @@
var TagSettingsValidator = Ember.Object.create({
check: function (model) {
var validationErrors = [],
data = model.getProperties('name', 'meta_title', 'meta_description');
if (validator.empty(data.name)) {
validationErrors.push({
message: 'You must specify a name for the tag.'
});
}
if (!validator.isLength(data.meta_title, 0, 150)) {
validationErrors.push({
message: 'Meta Title cannot be longer than 150 characters.'
});
}
if (!validator.isLength(data.meta_description, 0, 200)) {
validationErrors.push({
message: 'Meta Description cannot be longer than 200 characters.'
});
}
return validationErrors;
}
});
export default TagSettingsValidator;