Skip to content

Commit

Permalink
Add post preview via uuid (/p/:uuid)
Browse files Browse the repository at this point in the history
Refs TryGhost#5097

- All drafts will show a preview link (this needs real css)
- Published posts will redirect
- Powered by ~10 pints between the two of us (@ErisDS, @novaugust)
  • Loading branch information
novaugust committed Apr 30, 2015
1 parent 18638d3 commit 76e9a85
Show file tree
Hide file tree
Showing 11 changed files with 133 additions and 27 deletions.
11 changes: 11 additions & 0 deletions core/client/app/models/post.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,17 @@ var Post = DS.Model.extend(NProgressSaveMixin, ValidationEngine, {
return this.get('ghostPaths.url').join(blogUrl, postUrl);
}),

previewUrl: Ember.computed('uuid', 'ghostPaths.url', 'config.blogUrl', 'config.routeKeywords.preview', function () {
var blogUrl = this.get('config.blogUrl'),
uuid = this.get('uuid'),
previewKeyword = this.get('config.routeKeywords.preview');
// New posts don't have a preview
if (!uuid) {
return '';
}
return this.get('ghostPaths.url').join(blogUrl, previewKeyword, uuid);
}),

scratch: null,
titleScratch: null,

Expand Down
7 changes: 7 additions & 0 deletions core/client/app/styles/layouts/editor.scss
Original file line number Diff line number Diff line change
Expand Up @@ -692,6 +692,13 @@ body.zen {
}
}//.post-settings-menu

.post-preview-link {
position: absolute;
top: 0;
right: 0;
font-size: 1.3rem;
}


//
// Post Settings Menu meta Data
Expand Down
5 changes: 5 additions & 0 deletions core/client/app/templates/post-settings-menu.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@
<form>
<div class="form-group">
<label for="url">Post URL</label>
{{#if model.isDraft}}
<a class="post-preview-link" target="_blank" href="{{model.previewUrl}}">
Preview<span class="icon-external"></span>
</a>
{{/if}}
<span class="input-icon icon-link">
{{gh-input class="post-setting-slug" id="url" value=slugValue name="post-setting-slug" focus-out="updateSlug" selectOnClick="true" stopEnterKeyDownPropagation="true"}}
</span>
Expand Down
2 changes: 2 additions & 0 deletions core/client/app/utils/config-parser.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ var isNumeric = function (num) {
return false;
} else if (isNumeric(val)) {
return +val;
} else if (val.indexOf('{') === 0) {
return JSON.parse(val);
} else {
return val;
}
Expand Down
3 changes: 2 additions & 1 deletion core/server/api/configuration.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ function getValidKeys() {
database: config.database.client,
mail: _.isObject(config.mail) ? config.mail.transport : '',
blogUrl: config.url.replace(/\/$/, ''),
blogTitle: config.theme.title
blogTitle: config.theme.title,
routeKeywords: JSON.stringify(config.routeKeywords)
};

return validKeys;
Expand Down
6 changes: 3 additions & 3 deletions core/server/api/posts.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,20 +64,20 @@ posts = {

/**
* ### Read
* Find a post, by ID or Slug
* Find a post, by ID, UUID, or Slug
*
* @public
* @param {{id_or_slug (required), context, status, include, ...}} options
* @return {Promise(Post)} Post
*/
read: function read(options) {
var attrs = ['id', 'slug', 'status'],
var attrs = ['id', 'slug', 'status', 'uuid'],
data = _.pick(options, attrs);

options = _.omit(options, attrs);

// only published posts if no user is present
if (!(options.context && options.context.user)) {
if (!data.uuid && !(options.context && options.context.user)) {
data.status = 'published';
}

Expand Down
3 changes: 2 additions & 1 deletion core/server/config/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,8 @@ ConfigManager.prototype.set = function (config) {
routeKeywords: {
tag: 'tag',
author: 'author',
page: 'page'
page: 'page',
preview: 'p'
},
slugs: {
// Used by generateSlug to generate slugs for posts, tags, users, ..
Expand Down
57 changes: 47 additions & 10 deletions core/server/controllers/frontend.js
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,25 @@ function getActiveThemePaths() {
});
}

/*
* Sets the response context around a post and renders it
* with the current theme's post view. Used by post preview
* and single post methods.
* Returns a function that takes the post to be rendered.
*/
function renderPost(req, res) {
return function (post) {
return getActiveThemePaths().then(function (paths) {
var view = template.getThemeViewForPost(paths, post),
response = formatResponse(post);

setResponseContext(req, res, response);

res.render(view, response);
});
};
}

frontendControllers = {
homepage: function (req, res, next) {
// Parse the page number
Expand Down Expand Up @@ -271,6 +290,32 @@ frontendControllers = {
}).catch(handleError(next));
},

preview: function (req, res, next) {
var params = {
uuid: req.params.uuid,
status: 'all',
include: 'author,tags,fields'
};

// Query database to find post
api.posts.read(params).then(function (result) {
var post = result.posts[0];

if (!post) {
return next();
}

if (post.status === 'published') {
return res.redirect(301, config.urlFor('post', {post: post}));
}

setReqCtx(req, post);

filters.doFilter('prePostsRender', post, res.locals)
.then(renderPost(req, res, post));
});
},

single: function (req, res, next) {
var path = req.path,
params,
Expand Down Expand Up @@ -336,16 +381,8 @@ frontendControllers = {

setReqCtx(req, post);

filters.doFilter('prePostsRender', post, res.locals).then(function (post) {
getActiveThemePaths().then(function (paths) {
var view = template.getThemeViewForPost(paths, post),
response = formatResponse(post);

setResponseContext(req, res, response);

res.render(view, response);
});
});
filters.doFilter('prePostsRender', post, res.locals)
.then(renderPost(req, res, post));
}

// If we've checked the path with the static permalink structure
Expand Down
24 changes: 14 additions & 10 deletions core/server/routes/frontend.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ var frontend = require('../controllers/frontend'),

frontendRoutes = function () {
var router = express.Router(),
subdir = config.paths.subdir;
subdir = config.paths.subdir,
routeKeywords = config.routeKeywords;

// ### Admin routes
router.get(/^\/(logout|signout)\/$/, function redirect(req, res) {
Expand Down Expand Up @@ -37,19 +38,22 @@ frontendRoutes = function () {
});

// Tags
router.get('/' + config.routeKeywords.tag + '/:slug/rss/', frontend.rss);
router.get('/' + config.routeKeywords.tag + '/:slug/rss/:page/', frontend.rss);
router.get('/' + config.routeKeywords.tag + '/:slug/' + config.routeKeywords.page + '/:page/', frontend.tag);
router.get('/' + config.routeKeywords.tag + '/:slug/', frontend.tag);
router.get('/' + routeKeywords.tag + '/:slug/rss/', frontend.rss);
router.get('/' + routeKeywords.tag + '/:slug/rss/:page/', frontend.rss);
router.get('/' + routeKeywords.tag + '/:slug/' + routeKeywords.page + '/:page/', frontend.tag);
router.get('/' + routeKeywords.tag + '/:slug/', frontend.tag);

// Authors
router.get('/' + config.routeKeywords.author + '/:slug/rss/', frontend.rss);
router.get('/' + config.routeKeywords.author + '/:slug/rss/:page/', frontend.rss);
router.get('/' + config.routeKeywords.author + '/:slug/' + config.routeKeywords.page + '/:page/', frontend.author);
router.get('/' + config.routeKeywords.author + '/:slug/', frontend.author);
router.get('/' + routeKeywords.author + '/:slug/rss/', frontend.rss);
router.get('/' + routeKeywords.author + '/:slug/rss/:page/', frontend.rss);
router.get('/' + routeKeywords.author + '/:slug/' + routeKeywords.page + '/:page/', frontend.author);
router.get('/' + routeKeywords.author + '/:slug/', frontend.author);

// Post Live Preview
router.get('/' + routeKeywords.preview + '/:uuid', frontend.preview);

// Default
router.get('/' + config.routeKeywords.page + '/:page/', frontend.homepage);
router.get('/' + routeKeywords.page + '/:page/', frontend.homepage);
router.get('/', frontend.homepage);
router.get('*', frontend.single);

Expand Down
36 changes: 36 additions & 0 deletions core/test/functional/routes/frontend_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,42 @@ describe('Frontend Routing', function () {
});
});

describe('Post preview', function () {
it('should display draft posts accessed via uuid', function (done) {
request.get('/p/d52c42ae-2755-455c-80ec-70b2ec55c903')
.expect('Content-Type', /html/)
.expect(200)
.end(function (err, res) {
if (err) {
return done(err);
}

var $ = cheerio.load(res.text);

should.not.exist(res.headers['x-cache-invalidate']);
should.not.exist(res.headers['X-CSRF-Token']);
should.not.exist(res.headers['set-cookie']);
should.exist(res.headers.date);

$('title').text().should.equal('Not finished yet');
$('.content .post').length.should.equal(1);
$('.poweredby').text().should.equal('Proudly published with Ghost');
$('body.post-template').length.should.equal(1);
$('body.tag-getting-started').length.should.equal(1);
$('article.post').length.should.equal(1);

done();
});
});

it('should redirect published posts to their live url', function (done) {
request.get('/p/2ac6b4f6-e1f3-406c-9247-c94a0496d39d')
.expect(301)
.expect('Location', '/short-and-sweet/')
.end(doEnd(done));
});
});

describe('Single post', function () {
it('should redirect without slash', function (done) {
request.get('/welcome-to-ghost')
Expand Down
6 changes: 4 additions & 2 deletions core/test/utils/fixtures/data-generator.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,15 @@ DataGenerator.Content = {
html: "<h2 id=\"testing\">testing</h2>\n\n<p>mctesters</p>\n\n<ul>\n<li>test</li>\n<li>line</li>\n<li>items</li>\n</ul>",
image: "http://placekitten.com/500/200",
meta_description: "test stuff",
published_at: new Date("2015-01-03")
published_at: new Date("2015-01-03"),
uuid: "2ac6b4f6-e1f3-406c-9247-c94a0496d39d"
},
{
title: "Not finished yet",
slug: "unfinished",
markdown: "<h1>HTML Ipsum Presents</h1><p><strong>Pellentesque habitant morbi tristique</strong> senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. <em>Aenean ultricies mi vitae est.</em> Mauris placerat eleifend leo. Quisque sit amet est et sapien ullamcorper pharetra. Vestibulum erat wisi, condimentum sed, <code>commodo vitae</code>, ornare sit amet, wisi. Aenean fermentum, elit eget tincidunt condimentum, eros ipsum rutrum orci, sagittis tempus lacus enim ac dui. <a href=\"#\">Donec non enim</a> in turpis pulvinar facilisis. Ut felis.</p><h2>Header Level 2</h2><ol><li>Lorem ipsum dolor sit amet, consectetuer adipiscing elit.</li><li>Aliquam tincidunt mauris eu risus.</li></ol><blockquote><p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus magna. Cras in mi at felis aliquet congue. Ut a est eget ligula molestie gravida. Curabitur massa. Donec eleifend, libero at sagittis mollis, tellus est malesuada tellus, at luctus turpis elit sit amet quam. Vivamus pretium ornare est.</p></blockquote><h3>Header Level 3</h3><ul><li>Lorem ipsum dolor sit amet, consectetuer adipiscing elit.</li><li>Aliquam tincidunt mauris eu risus.</li></ul><pre><code>#header h1 a{display: block;width: 300px;height: 80px;}</code></pre>",
status: "draft"
status: "draft",
uuid: "d52c42ae-2755-455c-80ec-70b2ec55c903"
},
{
title: "Not so short, bit complex",
Expand Down

0 comments on commit 76e9a85

Please sign in to comment.